diff --git a/.circleci/config.yml b/.circleci/config.yml
index 497b17f72..f204e57c8 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -84,7 +84,7 @@ jobs:
- run:
name: Clone source on lightsail
command: |
- ssh lightsail 'export CIRCLE_BRANCH='"'$CIRCLE_BRANCH'"'; git clone -b $CIRCLE_BRANCH --recurse-submodules https://github.com/cvisionai/tator';
+ ssh lightsail 'export CIRCLE_BRANCH='"'$CIRCLE_BRANCH'"'; git clone -b ${CIRCLE_BRANCH:-stable} --recurse-submodules https://github.com/cvisionai/tator';
- persist_to_workspace:
root: ~/
paths:
@@ -127,7 +127,7 @@ jobs:
front-end-tests:
machine:
image: ubuntu-2004:202010-01
- resource_class: large
+ resource_class: xlarge
steps:
- attach_workspace:
at: ~/
@@ -141,7 +141,7 @@ jobs:
wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb;
sudo -E apt-get -yq --no-install-suggests --no-install-recommends install ./google-chrome-stable_current_amd64.deb;
sudo -E apt-get update && sudo -E apt-get -yq --no-install-suggests --no-install-recommends install tesseract-ocr;
- pip3 install playwright==1.21.0 pytest-playwright==0.1.2 pytesseract==0.3.9 opencv-python;
+ pip3 install playwright==1.17.2 pytest-playwright==0.1.2 pytesseract==0.3.9 opencv-python;
export PATH=$PATH:$HOME/.local/bin:/snap/bin;
playwright install;
scp -r lightsail:/tmp/tator_py_whl/tator*.whl /tmp;
@@ -217,23 +217,23 @@ workflows:
filters:
tags:
only: /.*/
- - rest-tests:
+ - front-end-tests:
requires:
- - install-tator
+ - install-tator
context: cvisionai
filters:
tags:
only: /.*/
- - front-end-tests:
+ - rest-tests:
requires:
- - install-tator
+ - front-end-tests
context: cvisionai
filters:
tags:
only: /.*/
- tator-py-tests:
requires:
- - install-tator
+ - front-end-tests
context: cvisionai
filters:
tags:
diff --git a/main/backup.py b/main/backup.py
index b73f394e9..a80517e2d 100644
--- a/main/backup.py
+++ b/main/backup.py
@@ -4,7 +4,7 @@
import json
import os
from uuid import uuid4
-from typing import Dict, Tuple
+from typing import Generator
from django.db import transaction
@@ -240,30 +240,32 @@ def get_store_info(self, project) -> bool:
return success, store_info
- def backup_resources(self, resource_qs) -> Tuple[int, Dict[int, set]]:
+ def backup_resources(self, resource_qs) -> Generator[tuple, None, None]:
"""
- Copies the resources in the given queryset from the live store to the backup store for their
- respective projects. Returns a tuple where the first element is the number of resources
- that were successfully backed up and the second is a dict that maps project ids to lists of
- media ids with at least one resource that failed to back up properly.
+ Creates a generator that copies the resources in the given queryset from the live store to
+ the backup store for their respective projects. Yields a tuple with the first element being
+ the success of the backup operation for the current resource and the second element being
+ the resource in question, so the calling function can iterate over the queryset and keep
+ track of its progress.
If there is no backup bucket for the given project (or a site-wide default), this will
- return `False`.
+ yield `(False, resource)`.
:param resource_qs: The resources to back up
:type resource_qs: Queryset
- :rtype: Tuple[int, Dict[int, set]]
+ :rtype: Generator[tuple, None, None]
"""
successful_backups = set()
for resource in resource_qs.iterator():
project = self.project_from_resource(resource)
path = resource.path
success, store_info = self.get_store_info(project)
+ success = success and "backup" in store_info
- if success and "backup" in store_info:
+ if success:
if store_info["backup"]["store"].check_key(path):
logger.info(f"Resource {path} already backed up")
- return True
+ continue
# Get presigned url from the live bucket, set to expire in 1h
download_url = store_info["live"]["store"].get_download_url(path, 3600)
diff --git a/main/models.py b/main/models.py
index 78c843676..450efa033 100644
--- a/main/models.py
+++ b/main/models.py
@@ -1473,7 +1473,7 @@ class Leaf(Model, ModelDiffMixin):
modified_by = ForeignKey(User, on_delete=SET_NULL, null=True, blank=True,
related_name='leaf_modified_by', db_column='modified_by')
parent=ForeignKey('self', on_delete=SET_NULL, blank=True, null=True, db_column='parent')
- path=PathField(unique=True)
+ path=PathField()
name = CharField(max_length=255)
deleted = BooleanField(default=False)
diff --git a/main/rest/_attribute_query.py b/main/rest/_attribute_query.py
index 3f85b2343..c936f0c0d 100644
--- a/main/rest/_attribute_query.py
+++ b/main/rest/_attribute_query.py
@@ -15,6 +15,17 @@
logger = logging.getLogger(__name__)
+
+def format_query_string(query_str: str) -> str:
+ """
+ Preformatting before passing the query to ElasticSearch.
+
+ :param query_str: The raw query string
+ :type query_str: str
+ """
+ return query_str.replace("/", "\\/")
+
+
def get_attribute_es_query(query_params, query, bools, project,
is_media=True, annotation_bools=None, modified=None):
""" TODO: add documentation for this """
@@ -93,10 +104,10 @@ def get_attribute_es_query(query_params, query, bools, project,
if section_object.lucene_search:
attr_query['media']['filter'].append({'bool': {
'should': [
- {'query_string': {'query': section_object.lucene_search}},
+ {'query_string': {'query': format_query_string(section_object.lucene_search)}},
{'has_child': {
'type': 'annotation',
- 'query': {'query_string': {'query': section_object.lucene_search}},
+ 'query': {'query_string': {'query': format_query_string(section_object.lucene_search)}},
},
},
],
@@ -131,13 +142,13 @@ def get_attribute_es_query(query_params, query, bools, project,
search = query_params.get('search')
if search is not None:
- search_query = {'query_string': {'query': search}}
+ search_query = {'query_string': {'query': format_query_string(search)}}
query['query']['bool']['filter'].append(search_query)
annotation_search = query_params.get('annotation_search')
if annotation_search is not None:
annotation_search_query = {'has_child': {'type': 'annotation',
- 'query': {'query_string': {'query': annotation_search}}}}
+ 'query': {'query_string': {'query': format_query_string(annotation_search)}}}}
query['query']['bool']['filter'].append(annotation_search_query)
else:
@@ -172,13 +183,13 @@ def get_attribute_es_query(query_params, query, bools, project,
search = query_params.get('search', None)
if search is not None:
- search_query = {'query_string': {'query': search}}
+ search_query = {'query_string': {'query': format_query_string(search)}}
query['query']['bool']['filter'].append(search_query)
media_search = query_params.get('media_search')
if media_search is not None:
media_search_query = {'has_parent': {'parent_type': 'media',
- 'query': {'query_string': {'query': media_search}}}}
+ 'query': {'query_string': {'query': format_query_string(media_search)}}}}
query['query']['bool']['filter'].append(media_search_query)
if modified is not None:
diff --git a/main/rest/_util.py b/main/rest/_util.py
index 7366ec5a4..968586887 100644
--- a/main/rest/_util.py
+++ b/main/rest/_util.py
@@ -3,15 +3,16 @@
import logging
from urllib.parse import urlparse
+from django.contrib.contenttypes.models import ContentType
from django.utils.http import urlencode
from django.db.models.expressions import Subquery
from rest_framework.reverse import reverse
from rest_framework.exceptions import APIException
from rest_framework.exceptions import PermissionDenied
-from ..models import type_to_obj
+from ..models import type_to_obj, ChangeLog, ChangeToObject, Project
-from ._attributes import convert_attribute
+from ._attributes import bulk_patch_attributes, convert_attribute
logger = logging.getLogger(__name__)
@@ -174,3 +175,146 @@ def url_to_key(url, project_obj):
path = '/'.join(parsed.path.split('/')[-num_tokens:])
return path, bucket, upload
+
+def bulk_update_and_log_changes(queryset, project, user, update_kwargs=None, new_attributes=None):
+ """
+ Performs a bulk update and creates a single changelog referenced by all changed objects
+
+ :param queryset: The queryset to update
+ :param project: The project the request originates from
+ :param user: The user making the requests
+ :param update_kwargs: The dictionary of arguments for queryset.update(), will be used like this:
+ `queryset.update(**update_kwargs)`
+ :param new_attributes: The validated attributes returned by `validate_attributes`, if any, will
+ be used like this: `bulk_patch_attributes(new_attributes, queryset)`
+ """
+ if not queryset.exists():
+ logger.info("Queryset empty, not performing any updates")
+ return
+
+ if update_kwargs is None and new_attributes is None:
+ raise ValueError(
+ "Must specify at least one of the following arguments: update_kwargs, new_attributes"
+ )
+
+ if type(project) != Project:
+ project = Project.objects.get(pk=project)
+
+ # Get prior state data for ChangeLog creation
+ updated_ids = list(queryset.values_list("id", flat=True))
+ first_obj = queryset.first()
+ ref_table = ContentType.objects.get_for_model(first_obj)
+ model_dict = first_obj.model_dict
+
+ # Perform queryset update
+ if update_kwargs is not None:
+ queryset.update(**update_kwargs)
+ if new_attributes is not None:
+ bulk_patch_attributes(new_attributes, queryset)
+
+ # Create ChangeLog
+ first_obj = type(first_obj).objects.get(pk=first_obj.id)
+ cl = ChangeLog(
+ project=project,
+ user=user,
+ description_of_change=first_obj.change_dict(model_dict),
+ )
+ cl.save()
+ objs = (
+ ChangeToObject(ref_table=ref_table, ref_id=obj_id, change_id=cl) for obj_id in updated_ids
+ )
+ bulk_create_from_generator(objs, ChangeToObject)
+
+
+def bulk_delete_and_log_changes(queryset, project, user):
+ """
+ Performs a bulk delete and creates a changelog for it.
+
+ :param queryset: The queryset to mark for deletion
+ :param project: The project the request originates from
+ :param user: The user making the requests
+ """
+ delete_kwargs = {
+ "deleted": True,
+ "modified_datetime": datetime.datetime.now(datetime.timezone.utc),
+ "modified_by": user,
+ }
+ bulk_update_and_log_changes(queryset, project, user, update_kwargs=delete_kwargs)
+
+
+def log_changes(obj, model_dict, project, user):
+ """
+ Creates a changelog for a single updated object.
+
+ :param obj: The object to compare and create a change log for.
+ :param model_dict: The state retrieved from `obj.model_dict` **before updating**.
+ :param project: The project the request originates from
+ :param user: The user making the requests
+ """
+ if type(project) != Project:
+ project = Project.objects.get(pk=project)
+
+ ref_table = ContentType.objects.get_for_model(obj)
+ cl = ChangeLog(project=project, user=user, description_of_change=obj.change_dict(model_dict))
+ cl.save()
+ ChangeToObject(ref_table=ref_table, ref_id=obj.id, change_id=cl).save()
+
+
+def delete_and_log_changes(obj, project, user):
+ """
+ Deletes a single object and creates a changelog for it.
+
+ :param obj: The object to delete and create a change log for.
+ :param project: The project the request originates from
+ :param user: The user making the requests
+ """
+ model_dict = obj.model_dict
+ obj.deleted = True
+ obj.modified_datetime = datetime.datetime.now(datetime.timezone.utc)
+ obj.modified_by = user
+ obj.save()
+
+ log_changes(obj, model_dict, project, user)
+
+
+def log_creation(obj, project, user):
+ """
+ Creates changelogs for a new object.
+
+ :param obj: The new object to create a change log for.
+ :param project: The project the request originates from
+ :param user: The user making the requests
+ """
+ if type(project) != Project:
+ project = Project.objects.get(pk=project)
+
+ ref_table = ContentType.objects.get_for_model(obj)
+ cl = ChangeLog(project=project, user=user, description_of_change=obj.create_dict)
+ cl.save()
+ ChangeToObject(ref_table=ref_table, ref_id=obj.id, change_id=cl).save()
+
+
+def bulk_log_creation(objects, project, user):
+ """
+ Creates changelogs for multiple new objects.
+
+ :param obj: The new object to create a change log for.
+ :param project: The project the request originates from
+ :param user: The user making the requests
+ """
+ # Create ChangeLogs
+ objs = (
+ ChangeLog(project=project, user=user, description_of_change=obj.create_dict)
+ for obj in objects
+ )
+ change_logs = bulk_create_from_generator(objs, ChangeLog)
+
+ # Associate ChangeLogs with created objects
+ ref_table = ContentType.objects.get_for_model(objects[0])
+ ids = [obj.id for obj in objects]
+ objs = (
+ ChangeToObject(ref_table=ref_table, ref_id=ref_id, change_id=cl)
+ for ref_id, cl in zip(ids, change_logs)
+ )
+ bulk_create_from_generator(objs, ChangeToObject)
+ return ids
diff --git a/main/rest/change_log.py b/main/rest/change_log.py
index 7c4f2423a..b90d5586b 100644
--- a/main/rest/change_log.py
+++ b/main/rest/change_log.py
@@ -10,7 +10,7 @@
logger = logging.getLogger(__name__)
-CHANGE_LOG_PROPERTIES = ["id", "project", "user", "description_of_change"]
+CHANGE_LOG_PROPERTIES = ["id", "project", "user", "description_of_change", "modified_datetime"]
class ChangeLogListAPI(BaseListView):
diff --git a/main/rest/download_info.py b/main/rest/download_info.py
index a558ec4fb..24d15cd7a 100644
--- a/main/rest/download_info.py
+++ b/main/rest/download_info.py
@@ -44,7 +44,8 @@ def _post(self, params):
if url is None:
upload = key.startswith('_uploads')
bucket = project_obj.get_bucket(upload=upload)
- store_default = get_tator_store(bucket, upload=upload)
+ use_upload_bucket = upload and not bucket
+ store_default = get_tator_store(bucket, upload=use_upload_bucket)
tator_store = store_lookup.get(key, store_default)
# Make sure the key corresponds to the correct project.
diff --git a/main/rest/leaf.py b/main/rest/leaf.py
index 341cf9498..7e3756c3a 100644
--- a/main/rest/leaf.py
+++ b/main/rest/leaf.py
@@ -6,8 +6,6 @@
from django.db import transaction
from django.http import Http404
-from ..models import ChangeLog
-from ..models import ChangeToObject
from ..models import Leaf
from ..models import LeafType
from ..models import Project
@@ -26,9 +24,16 @@
from ._attributes import patch_attributes
from ._attributes import bulk_patch_attributes
from ._attributes import validate_attributes
-from ._util import bulk_create_from_generator
-from ._util import computeRequiredFields
-from ._util import check_required_fields
+from ._util import (
+ bulk_create_from_generator,
+ bulk_delete_and_log_changes,
+ bulk_log_creation,
+ bulk_update_and_log_changes,
+ computeRequiredFields,
+ check_required_fields,
+ delete_and_log_changes,
+ log_changes,
+)
from ._permissions import ProjectViewOnlyPermission
from ._permissions import ProjectFullControlPermission
@@ -169,23 +174,7 @@ def _post(self, params):
documents = []
ts.bulk_add_documents(documents)
- # Create ChangeLogs
- objs = (
- ChangeLog(
- project=project, user=self.request.user, description_of_change=leaf.create_dict
- )
- for leaf in leaves
- )
- change_logs = bulk_create_from_generator(objs, ChangeLog)
-
- # Associate ChangeLogs with created objects
- ref_table = ContentType.objects.get_for_model(leaves[0])
- ids = [leaf.id for leaf in leaves]
- objs = (
- ChangeToObject(ref_table=ref_table, ref_id=ref_id, change_id=cl)
- for ref_id, cl in zip(ids, change_logs)
- )
- bulk_create_from_generator(objs, ChangeToObject)
+ ids = bulk_log_creation(leaves, project, self.request.user)
# Return created IDs.
return {'message': f'Successfully created {len(ids)} leaves!', 'id': ids}
@@ -194,63 +183,25 @@ def _delete(self, params):
qs = get_leaf_queryset(params['project'], params)
count = qs.count()
if count > 0:
- # Get info to populate ChangeLog entry
- first_obj = qs.first()
- project = first_obj.project
- ref_table = ContentType.objects.get_for_model(first_obj)
- delete_dicts = []
- ref_ids = []
- for obj in qs:
- delete_dicts.append(obj.delete_dict)
- ref_ids.append(obj.id)
-
- qs.update(deleted=True,
- modified_datetime=datetime.datetime.now(datetime.timezone.utc),
- modified_by=self.request.user)
+ bulk_delete_and_log_changes(qs, params["project"], self.request.user)
query = get_leaf_es_query(params)
TatorSearch().delete(self.kwargs['project'], query)
- # Create ChangeLogs
- objs = (
- ChangeLog(project=project, user=self.request.user, description_of_change=dd)
- for dd in delete_dicts
- )
- change_logs = bulk_create_from_generator(objs, ChangeLog)
-
- # Associate ChangeLogs with deleted objects
- objs = (
- ChangeToObject(ref_table=ref_table, ref_id=ref_id, change_id=cl)
- for ref_id, cl in zip(ref_ids, change_logs)
- )
- bulk_create_from_generator(objs, ChangeToObject)
-
return {'message': f'Successfully deleted {count} leaves!'}
def _patch(self, params):
qs = get_leaf_queryset(params['project'], params)
count = qs.count()
if count > 0:
- # Get the current representation of the object for comparison
- original_dict = qs.first().model_dict
new_attrs = validate_attributes(params, qs[0])
+ bulk_update_and_log_changes(
+ qs, params["project"], self.request.user, new_attributes=new_attrs
+ )
bulk_patch_attributes(new_attrs, qs)
- # Get one object from the queryset to create the change log
- obj = qs.first()
- change_dict = obj.change_dict(original_dict)
- ref_table = ContentType.objects.get_for_model(obj)
-
query = get_leaf_es_query(params)
TatorSearch().update(self.kwargs['project'], qs[0].meta, query, new_attrs)
- # Create the ChangeLog entry and associate it with all objects in the queryset
- cl = ChangeLog(
- project=obj.project, user=self.request.user, description_of_change=change_dict
- )
- cl.save()
- objs = (ChangeToObject(ref_table=ref_table, ref_id=o.id, change_id=cl) for o in qs)
- bulk_create_from_generator(objs, ChangeToObject)
-
return {'message': f'Successfully updated {count} leaves!'}
def _put(self, params):
@@ -277,7 +228,7 @@ def _get(self, params):
@transaction.atomic
def _patch(self, params):
obj = Leaf.objects.get(pk=params['id'], deleted=False)
- original_dict = obj.model_dict
+ model_dict = obj.model_dict
# Patch common attributes.
if 'name' in params:
@@ -285,38 +236,17 @@ def _patch(self, params):
obj.save()
new_attrs = validate_attributes(params, obj)
obj = patch_attributes(new_attrs, obj)
-
obj.save()
- cl = ChangeLog(
- project=obj.project,
- user=self.request.user,
- description_of_change=obj.change_dict(original_dict),
- )
- cl.save()
- ChangeToObject(
- ref_table=ContentType.objects.get_for_model(obj),
- ref_id=obj.id,
- change_id=cl,
- ).save()
-
+ log_changes(obj, model_dict, obj.project, self.request.user)
return {'message': 'Leaf {params["id"]} successfully updated!'}
def _delete(self, params):
leaf = Leaf.objects.get(pk=params['id'], deleted=False)
project = leaf.project
- delete_dict = leaf.delete_dict
- ref_table = ContentType.objects.get_for_model(leaf)
- ref_id = leaf.id
- leaf.deleted = True
- leaf.modified_datetime = datetime.datetime.now(datetime.timezone.utc)
- leaf.modified_by = self.request.user
- leaf.save()
+ model_dict = leaf.model_dict
+ delete_and_log_changes(leaf, project, self.request.user)
TatorSearch().delete_document(leaf)
- cl = ChangeLog(project=project, user=self.request.user, description_of_change=delete_dict)
- cl.save()
- ChangeToObject(ref_table=ref_table, ref_id=ref_id, change_id=cl).save()
return {'message': 'Leaf {params["id"]} successfully deleted!'}
def get_queryset(self):
return Leaf.objects.all()
-
diff --git a/main/rest/localization.py b/main/rest/localization.py
index 549e872c7..e7c60839f 100644
--- a/main/rest/localization.py
+++ b/main/rest/localization.py
@@ -5,8 +5,6 @@
from django.contrib.contenttypes.models import ContentType
from django.http import Http404
-from ..models import ChangeLog
-from ..models import ChangeToObject
from ..models import Localization
from ..models import LocalizationType
from ..models import Media
@@ -31,9 +29,16 @@
from ._attributes import patch_attributes
from ._attributes import bulk_patch_attributes
from ._attributes import validate_attributes
-from ._util import bulk_create_from_generator
-from ._util import computeRequiredFields
-from ._util import check_required_fields
+from ._util import (
+ bulk_create_from_generator,
+ bulk_delete_and_log_changes,
+ bulk_log_creation,
+ bulk_update_and_log_changes,
+ computeRequiredFields,
+ check_required_fields,
+ delete_and_log_changes,
+ log_changes,
+)
from ._permissions import ProjectEditPermission
logger = logging.getLogger(__name__)
@@ -198,23 +203,7 @@ def _post(self, params):
documents = []
ts.bulk_add_documents(documents)
- # Create ChangeLogs
- objs = (
- ChangeLog(
- project=project, user=self.request.user, description_of_change=loc.create_dict
- )
- for loc in localizations
- )
- change_logs = bulk_create_from_generator(objs, ChangeLog)
-
- # Associate ChangeLogs with created objects
- ref_table = ContentType.objects.get_for_model(localizations[0])
- ids = [loc.id for loc in localizations]
- objs = (
- ChangeToObject(ref_table=ref_table, ref_id=ref_id, change_id=cl)
- for ref_id, cl in zip(ids, change_logs)
- )
- bulk_create_from_generator(objs, ChangeToObject)
+ ids = bulk_log_creation(localizations, project, self.request.user)
# Return created IDs.
return {'message': f'Successfully created {len(ids)} localizations!', 'id': ids}
@@ -223,37 +212,11 @@ def _delete(self, params):
qs = get_annotation_queryset(params['project'], params, 'localization')
count = qs.count()
if count > 0:
- # Get info to populate ChangeLog entry
- first_obj = qs.first()
- project = first_obj.project
- ref_table = ContentType.objects.get_for_model(first_obj)
- delete_dicts = []
- ref_ids = []
- for obj in qs:
- delete_dicts.append(obj.delete_dict)
- ref_ids.append(obj.id)
-
# Delete the localizations.
- qs.update(deleted=True,
- modified_datetime=datetime.datetime.now(datetime.timezone.utc),
- modified_by=self.request.user)
+ bulk_delete_and_log_changes(qs, params["project"], self.request.user)
query = get_annotation_es_query(params['project'], params, 'localization')
TatorSearch().delete(self.kwargs['project'], query)
- # Create ChangeLogs
- objs = (
- ChangeLog(project=project, user=self.request.user, description_of_change=dd)
- for dd in delete_dicts
- )
- change_logs = bulk_create_from_generator(objs, ChangeLog)
-
- # Associate ChangeLogs with deleted objects
- objs = (
- ChangeToObject(ref_table=ref_table, ref_id=ref_id, change_id=cl)
- for ref_id, cl in zip(ref_ids, change_logs)
- )
- bulk_create_from_generator(objs, ChangeToObject)
-
return {'message': f'Successfully deleted {count} localizations!'}
def _patch(self, params):
@@ -263,31 +226,24 @@ def _patch(self, params):
if count > 0:
# Get the current representation of the object for comparison
obj = qs.first()
- original_dict = obj.model_dict
first_id = obj.id
entity_type = obj.meta
new_attrs = validate_attributes(params, qs[0])
- bulk_patch_attributes(new_attrs, qs)
+ update_kwargs = {"modified_by": self.request.user}
if patched_version is not None:
- qs.update(version=patched_version)
- qs.update(modified_by=self.request.user)
-
- # Get one object from the queryset to create the change log
- obj = Localization.objects.get(pk=first_id)
- change_dict = obj.change_dict(original_dict)
- ref_table = ContentType.objects.get_for_model(obj)
+ update_kwargs["version"] = patched_version
+
+ bulk_update_and_log_changes(
+ qs,
+ params["project"],
+ self.request.user,
+ update_kwargs=update_kwargs,
+ new_attributes=new_attrs,
+ )
query = get_annotation_es_query(params['project'], params, 'localization')
TatorSearch().update(self.kwargs['project'], entity_type, query, new_attrs)
- # Create the ChangeLog entry and associate it with all objects in the queryset
- cl = ChangeLog(
- project=obj.project, user=self.request.user, description_of_change=change_dict
- )
- cl.save()
- objs = (ChangeToObject(ref_table=ref_table, ref_id=o.id, change_id=cl) for o in qs)
- bulk_create_from_generator(objs, ChangeToObject)
-
return {'message': f'Successfully updated {count} localizations!'}
def _put(self, params):
@@ -316,7 +272,7 @@ def _get(self, params):
@transaction.atomic
def _patch(self, params):
obj = Localization.objects.get(pk=params['id'], deleted=False)
- original_dict = obj.model_dict
+ model_dict = obj.model_dict
# Patch common attributes.
frame = params.get("frame", None)
@@ -392,17 +348,7 @@ def _patch(self, params):
obj.thumbnail_image.save()
obj.save()
- cl = ChangeLog(
- project=obj.project,
- user=self.request.user,
- description_of_change=obj.change_dict(original_dict),
- )
- cl.save()
- ChangeToObject(
- ref_table=ContentType.objects.get_for_model(obj),
- ref_id=obj.id,
- change_id=cl,
- ).save()
+ log_changes(obj, model_dict, obj.project, self.request.user)
return {'message': f'Localization {params["id"]} successfully updated!'}
@@ -411,17 +357,8 @@ def _delete(self, params):
if not qs.exists():
raise Http404
obj = qs[0]
- project = obj.project
- delete_dict = obj.delete_dict
- ref_table = ContentType.objects.get_for_model(obj)
- ref_id = obj.id
+ delete_and_log_changes(obj, obj.project, self.request.user)
TatorSearch().delete_document(obj)
- qs.update(deleted=True,
- modified_datetime=datetime.datetime.now(datetime.timezone.utc),
- modified_by=self.request.user)
- cl = ChangeLog(project=project, user=self.request.user, description_of_change=delete_dict)
- cl.save()
- ChangeToObject(ref_table=ref_table, ref_id=ref_id, change_id=cl).save()
return {'message': f'Localization {params["id"]} successfully deleted!'}
def get_queryset(self):
diff --git a/main/rest/media.py b/main/rest/media.py
index 98bb67fb5..9632c2b18 100644
--- a/main/rest/media.py
+++ b/main/rest/media.py
@@ -1,5 +1,6 @@
import logging
import datetime
+from itertools import chain
import os
import shutil
import mimetypes
@@ -15,8 +16,6 @@
from PIL import Image
from ..models import (
- ChangeLog,
- ChangeToObject,
Media,
MediaType,
Section,
@@ -37,7 +36,15 @@
from ..cache import TatorCache
from ._util import url_to_key
-from ._util import bulk_create_from_generator, computeRequiredFields, check_required_fields
+from ._util import (
+ bulk_update_and_log_changes,
+ bulk_delete_and_log_changes,
+ delete_and_log_changes,
+ log_changes,
+ log_creation,
+ computeRequiredFields,
+ check_required_fields,
+)
from ._base_views import BaseListView, BaseDetailView
from ._media_query import get_media_queryset, get_media_es_query
from ._attributes import bulk_patch_attributes, patch_attributes, validate_attributes
@@ -346,17 +353,11 @@ def _create_media(params, user):
if url:
path, bucket, upload = url_to_key(url, project_obj)
if path is not None:
- tator_store = get_tator_store(bucket, upload=upload)
+ use_upload_bucket = upload and not bucket
+ tator_store = get_tator_store(bucket, upload=use_upload_bucket)
tator_store.put_media_id_tag(path, media_obj.id)
- cl = ChangeLog(
- project=media_obj.project,
- user=user,
- description_of_change=media_obj.create_dict,
- )
- cl.save()
- ref_table = ContentType.objects.get_for_model(media_obj)
- ChangeToObject(ref_table=ref_table, ref_id=media_obj.id, change_id=cl).save()
+ log_creation(media_obj, media_obj.project, user)
return media_obj, response
@@ -413,20 +414,7 @@ def _delete(self, params):
media_ids = list(qs.values_list('pk', flat=True).distinct())
count = qs.count()
if count > 0:
- # Get info to populate ChangeLog entry
- first_obj = qs.first()
- project = first_obj.project
- ref_table = ContentType.objects.get_for_model(first_obj)
- delete_dicts = []
- ref_ids = []
- for obj in qs:
- delete_dicts.append(obj.delete_dict)
- ref_ids.append(obj.id)
-
- # Mark media for deletion.
- qs.update(deleted=True,
- modified_datetime=datetime.datetime.now(datetime.timezone.utc),
- modified_by=self.request.user)
+ bulk_delete_and_log_changes(qs, project, self.request.user)
# Any states that are only associated to deleted media should also be marked
# for deletion.
@@ -436,15 +424,11 @@ def _delete(self, params):
.values_list('id', flat=True)
all_deleted = set(deleted) - set(not_deleted)
state_qs = State.objects.filter(pk__in=all_deleted)
- state_qs.update(deleted=True,
- modified_datetime=datetime.datetime.now(datetime.timezone.utc),
- modified_by=self.request.user)
+ bulk_delete_and_log_changes(state_qs, project, self.request.user)
# Delete any localizations associated to this media
loc_qs = Localization.objects.filter(project=project, media__in=media_ids)
- loc_qs.update(deleted=True,
- modified_datetime=datetime.datetime.now(datetime.timezone.utc),
- modified_by=self.request.user)
+ bulk_delete_and_log_changes(loc_qs, project, self.request.user)
# Clear elasticsearch entries for both media and its children.
# Note that clearing children cannot be done using has_parent because it does
@@ -467,19 +451,6 @@ def _delete(self, params):
},
})
- # Create ChangeLogs
- objs = (
- ChangeLog(project=project, user=self.request.user, description_of_change=dd)
- for dd in delete_dicts
- )
- change_logs = bulk_create_from_generator(objs, ChangeLog)
-
- # Associate ChangeLogs with deleted objects
- objs = (
- ChangeToObject(ref_table=ref_table, ref_id=ref_id, change_id=cl)
- for ref_id, cl in zip(ref_ids, change_logs)
- )
- bulk_create_from_generator(objs, ChangeToObject)
return {'message': f'Successfully deleted {count} medias!'}
def _patch(self, params):
@@ -506,21 +477,11 @@ def _patch(self, params):
new_attrs = validate_attributes(params, obj)
if new_attrs is not None:
attr_count = len(ids_to_update)
- ref_table = ContentType.objects.get_for_model(obj)
- original_dict = obj.model_dict
- bulk_patch_attributes(new_attrs, qs)
+ bulk_update_and_log_changes(
+ qs, params["project"], self.request.user, new_attributes=new_attrs
+ )
query = get_media_es_query(params["project"], params)
ts.update(self.kwargs["project"], obj.meta, query, new_attrs)
- qs = Media.objects.filter(pk__in=ids_to_update)
- obj = Media.objects.get(id=original_dict["id"])
- change_dict = obj.change_dict(original_dict)
- # Create the ChangeLog entry and associate it with all objects in the queryset
- cl = ChangeLog(
- project=obj.project, user=self.request.user, description_of_change=change_dict
- )
- cl.save()
- objs = (ChangeToObject(ref_table=ref_table, ref_id=o.id, change_id=cl) for o in qs)
- bulk_create_from_generator(objs, ChangeToObject)
count = max(count, attr_count)
if desired_archive_state is not None:
@@ -559,18 +520,23 @@ def _patch(self, params):
# Get the original dict for creating the change log
archive_objs = list(archive_qs)
obj = archive_objs[0]
- ref_table = ContentType.objects.get_for_model(obj)
- original_dict = obj.model_dict
+ model_dict = obj.model_dict
# Store the list of ids updated for this state and update them
archive_ids_to_update = [o.id for o in archive_objs]
archive_count += len(archive_ids_to_update)
previously_updated += archive_ids_to_update
dt_now = datetime.datetime.now(datetime.timezone.utc)
- archive_qs.update(
- archive_status_date=dt_now,
- archive_state=next_archive_state,
- modified_datetime=dt_now,
+ update_kwargs = {
+ "archive_status_date": dt_now,
+ "archive_state": next_archive_state,
+ "modified_datetime": dt_now,
+ }
+ bulk_update_and_log_changes(
+ archive_qs,
+ params["project"],
+ self.request.user,
+ update_kwargs=update_kwargs,
)
archive_qs = Media.objects.filter(pk__in=archive_ids_to_update)
@@ -580,19 +546,6 @@ def _patch(self, params):
documents += ts.build_document(entity)
ts.bulk_add_documents(documents)
- # Create the ChangeLog entry and associate it with all updated objects
- obj = Media.objects.get(id=original_dict["id"])
- cl = ChangeLog(
- project=obj.project,
- user=self.request.user,
- description_of_change=obj.change_dict(original_dict),
- )
- cl.save()
- objs = (
- ChangeToObject(ref_table=ref_table, ref_id=obj_id, change_id=cl)
- for obj_id in archive_ids_to_update
- )
- bulk_create_from_generator(objs, ChangeToObject)
count = max(count, archive_count)
return {"message": f"Successfully patched {count} medias!"}
@@ -636,9 +589,10 @@ def _patch(self, params):
"""
with transaction.atomic():
qs = Media.objects.select_for_update().filter(pk=params['id'], deleted=False)
- original_dict = qs[0].model_dict
+ media = qs[0]
+ model_dict = media.model_dict
if 'attributes' in params:
- new_attrs = validate_attributes(params, qs[0])
+ new_attrs = validate_attributes(params, media)
bulk_patch_attributes(new_attrs, qs)
if 'name' in params:
@@ -669,7 +623,7 @@ def _patch(self, params):
qs.update(summaryLevel=params['summaryLevel'])
if 'multi' in params:
- media_files = qs[0].media_files
+ media_files = media.media_files
# If this object already contains non-multi media definitions, raise an exception.
if media_files:
for role in ['streaming', 'archival', 'image', 'live']:
@@ -678,10 +632,10 @@ def _patch(self, params):
raise ValueError(f"Cannot set a multi definition on a Media that contains "
"individual media!")
# Check values of IDs (that they exist and are part of the same project).
- sub_media = Media.objects.filter(project=qs[0].project, pk__in=params['multi']['ids'])
+ sub_media = Media.objects.filter(project=media.project, pk__in=params['multi']['ids'])
if len(params['multi']['ids']) != sub_media.count():
raise ValueError(f"One or more media IDs in multi definition is not part of "
- "project {qs[0].project.pk} or does not exist!")
+ "project {media.project.pk} or does not exist!")
if media_files is None:
media_files = {}
for key in ['ids', 'layout', 'quality']:
@@ -690,7 +644,7 @@ def _patch(self, params):
qs.update(media_files=media_files)
if 'live' in params:
- media_files = qs[0].media_files
+ media_files = media.media_files
# If this object already contains non-live media definitions, raise an exception.
if media_files:
for role in ['streaming', 'archival', 'image', 'ids']:
@@ -706,10 +660,14 @@ def _patch(self, params):
if "archive_state" in params:
next_archive_state = _get_next_archive_state(
- params["archive_state"], qs[0].archive_state
+ params["archive_state"], media.archive_state
)
if next_archive_state is not None:
+ project = media.project
+ user = self.request.user
+
+ # Update the archive state of all videos if this is a multiview
multi_constituent_ids = _single_ids_from_multi_qs(
qs.filter(meta__dtype="multi")
)
@@ -718,10 +676,14 @@ def _patch(self, params):
pk__in=multi_constituent_ids
)
dt_now = datetime.datetime.now(datetime.timezone.utc)
- archive_state_qs.update(
- archive_status_date=dt_now,
- archive_state=next_archive_state,
- modified_datetime=dt_now,
+ update_kwargs = {
+ "archive_status_date": dt_now,
+ "archive_state": next_archive_state,
+ "modified_datetime": dt_now,
+ "modified_by": user,
+ }
+ bulk_update_and_log_changes(
+ archive_state_qs, project, user, update_kwargs=update_kwargs
)
obj = Media.objects.get(pk=params['id'], deleted=False)
@@ -732,18 +694,7 @@ def _patch(self, params):
localization = patch_attributes(new_attrs, localization)
localization.save()
- cl = ChangeLog(
- project=obj.project,
- user=self.request.user,
- description_of_change=obj.change_dict(original_dict),
- )
- cl.save()
- ChangeToObject(
- ref_table=ContentType.objects.get_for_model(obj),
- ref_id=obj.id,
- change_id=cl,
- ).save()
-
+ log_changes(obj, model_dict, obj.project, self.request.user)
return {'message': f'Media {params["id"]} successfully updated!'}
def _delete(self, params):
@@ -754,17 +705,9 @@ def _delete(self, params):
"""
media = Media.objects.get(pk=params['id'], deleted=False)
project = media.project
- delete_dict = media.delete_dict
- ref_table = ContentType.objects.get_for_model(media)
- ref_id = media.id
- media.deleted = True
- media.modified_datetime = datetime.datetime.now(datetime.timezone.utc)
- media.modified_by = self.request.user
- media.save()
+ modified_datetime = datetime.datetime.now(datetime.timezone.utc)
+ delete_and_log_changes(media, project, self.request.user)
TatorSearch().delete_document(media)
- cl = ChangeLog(project=project, user=self.request.user, description_of_change=delete_dict)
- cl.save()
- ChangeToObject(ref_table=ref_table, ref_id=ref_id, change_id=cl).save()
# Any states that are only associated to deleted media should also be marked
# for deletion.
@@ -774,22 +717,20 @@ def _delete(self, params):
.values_list('id', flat=True)
all_deleted = set(deleted) - set(not_deleted)
state_qs = State.objects.filter(pk__in=all_deleted)
- state_qs.update(deleted=True,
- modified_datetime=datetime.datetime.now(datetime.timezone.utc),
- modified_by=self.request.user)
+ bulk_delete_and_log_changes(state_qs, project, self.request.user)
# Delete any localizations associated to this media
loc_qs = Localization.objects.filter(project=project, media__in=[media.id])
- loc_qs.update(deleted=True,
- modified_datetime=datetime.datetime.now(datetime.timezone.utc),
- modified_by=self.request.user)
+ bulk_delete_and_log_changes(loc_qs, project, self.request.user)
# Clear elasticsearch entries for both media and its children.
# Note that clearing children cannot be done using has_parent because it does
# not accept queries with size, and has_parent also does not accept ids queries.
- loc_ids = [f'box_{val.id}' for val in loc_qs.iterator()] \
- + [f'line_{val.id}' for val in loc_qs.iterator()] \
- + [f'dot_{val.id}' for val in loc_qs.iterator()]
+ loc_types = ["box", "line", "dot"]
+ loc_id_iterator = chain(
+ *([f"{loc_type}_{val.id}" for loc_type in loc_types] for val in loc_qs.iterator())
+ )
+ loc_ids = list(loc_id_iterator)
TatorSearch().delete(project.id, {'query': {'ids': {'values': loc_ids}}})
state_ids = [val.id for val in state_qs.iterator()]
TatorSearch().delete(project.id, {
diff --git a/main/rest/project.py b/main/rest/project.py
index 3ca683d2d..59f9690e7 100644
--- a/main/rest/project.py
+++ b/main/rest/project.py
@@ -149,7 +149,9 @@ def _patch(self, params):
fname = tokens[-1]
# Set up S3 clients.
- upload_store = get_tator_store(project.get_bucket(upload=True), upload=True)
+ project_upload_bucket = project.get_bucket(upload=True)
+ use_default_upload_bucket = project_upload_bucket is None
+ upload_store = get_tator_store(project_upload_bucket, upload=use_default_upload_bucket)
generic_store = get_tator_store()
# Check prefix.
diff --git a/main/rest/state.py b/main/rest/state.py
index a780bb37d..24b805cc7 100644
--- a/main/rest/state.py
+++ b/main/rest/state.py
@@ -8,8 +8,6 @@
from django.http import Http404
import numpy as np
-from ..models import ChangeLog
-from ..models import ChangeToObject
from ..models import State
from ..models import StateType
from ..models import Media
@@ -35,9 +33,16 @@
from ._attributes import patch_attributes
from ._attributes import bulk_patch_attributes
from ._attributes import validate_attributes
-from ._util import bulk_create_from_generator
-from ._util import computeRequiredFields
-from ._util import check_required_fields
+from ._util import (
+ bulk_create_from_generator,
+ bulk_delete_and_log_changes,
+ bulk_log_creation,
+ bulk_update_and_log_changes,
+ computeRequiredFields,
+ check_required_fields,
+ delete_and_log_changes,
+ log_changes,
+)
from ._permissions import ProjectEditPermission
logger = logging.getLogger(__name__)
@@ -294,88 +299,33 @@ def _post(self, params):
documents = []
ts.bulk_add_documents(documents)
- # Create ChangeLogs
- objs = (
- ChangeLog(
- project=project, user=self.request.user, description_of_change=state.create_dict
- )
- for state in states
- )
- change_logs = bulk_create_from_generator(objs, ChangeLog)
-
- # Associate ChangeLogs with created objects
- ref_table = ContentType.objects.get_for_model(states[0])
- ids = [state.id for state in states]
- objs = (
- ChangeToObject(ref_table=ref_table, ref_id=ref_id, change_id=cl)
- for ref_id, cl in zip(ids, change_logs)
- )
- bulk_create_from_generator(objs, ChangeToObject)
+ ids = bulk_log_creation(states, project, self.request.user)
- # Return created IDs.
- ids = [state.id for state in states]
return {'message': f'Successfully created {len(ids)} states!', 'id': ids}
def _delete(self, params):
qs = get_annotation_queryset(params['project'], params, 'state')
count = qs.count()
if count > 0:
- # Get info to populate ChangeLog entry
- obj = qs.first()
- project = obj.project
- delete_dicts = [obj.delete_dict for obj in qs]
- ref_table = ContentType.objects.get_for_model(obj)
- ref_ids = [o.id for o in qs]
-
# Delete states.
- qs.update(deleted=True,
- modified_datetime=datetime.datetime.now(datetime.timezone.utc),
- modified_by=self.request.user)
+ bulk_delete_and_log_changes(qs, params["project"], self.request.user)
query = get_annotation_es_query(params['project'], params, 'state')
TatorSearch().delete(self.kwargs['project'], query)
- # Create ChangeLogs
- objs = (
- ChangeLog(project=project, user=self.request.user, description_of_change=dd)
- for dd in delete_dicts
- )
- change_logs = bulk_create_from_generator(objs, ChangeLog)
-
- # Associate ChangeLogs with deleted objects
- objs = (
- ChangeToObject(ref_table=ref_table, ref_id=ref_id, change_id=cl)
- for ref_id, cl in zip(ref_ids, change_logs)
- )
- bulk_create_from_generator(objs, ChangeToObject)
-
return {'message': f'Successfully deleted {count} states!'}
def _patch(self, params):
qs = get_annotation_queryset(params['project'], params, 'state')
count = qs.count()
if count > 0:
- # Get the current representation of the object for comparison
- original_dict = qs.first().model_dict
new_attrs = validate_attributes(params, qs[0])
- bulk_patch_attributes(new_attrs, qs)
- qs.update(modified_by=self.request.user)
-
- # Get one object from the queryset to create the change log
- obj = qs.first()
- change_dict = obj.change_dict(original_dict)
- ref_table = ContentType.objects.get_for_model(obj)
+ bulk_update_and_log_changes(
+ qs, params["project"], self.request.user, new_attributes=new_attrs
+ )
query = get_annotation_es_query(params['project'], params, 'state')
TatorSearch().update(self.kwargs['project'], qs[0].meta, query, new_attrs)
- # Create the ChangeLog entry and associate it with all objects in the queryset
- cl = ChangeLog(
- project=obj.project, user=self.request.user, description_of_change=change_dict
- )
- cl.save()
- objs = (ChangeToObject(ref_table=ref_table, ref_id=o.id, change_id=cl) for o in qs)
- bulk_create_from_generator(objs, ChangeToObject)
-
return {'message': f'Successfully updated {count} states!'}
def _put(self, params):
@@ -415,7 +365,7 @@ def _get(self, params):
@transaction.atomic
def _patch(self, params):
obj = State.objects.get(pk=params['id'], deleted=False)
- original_dict = obj.model_dict
+ model_dict = obj.model_dict
if 'frame' in params:
obj.frame = params['frame']
@@ -462,26 +412,14 @@ def _patch(self, params):
obj.modified_by = self.request.user
obj.save()
- cl = ChangeLog(
- project=obj.project,
- user=self.request.user,
- description_of_change=obj.change_dict(original_dict),
- )
- cl.save()
- ChangeToObject(
- ref_table=ContentType.objects.get_for_model(obj),
- ref_id=obj.id,
- change_id=cl,
- ).save()
+ log_changes(obj, model_dict, obj.project, self.request.user)
return {'message': f'State {params["id"]} successfully updated!'}
def _delete(self, params):
state = State.objects.get(pk=params['id'], deleted=False)
project = state.project
- delete_dict = state.delete_dict
- ref_table = ContentType.objects.get_for_model(state)
- ref_id = state.id
+ model_dict = state.model_dict
delete_localizations = []
if state.meta.delete_child_localizations:
@@ -493,19 +431,11 @@ def _delete(self, params):
if not loc_qs.exists():
delete_localizations.append(loc.id)
- state.deleted=True
- state.modified_datetime=datetime.datetime.now(datetime.timezone.utc)
- state.modified_by=self.request.user
- state.save()
+ delete_and_log_changes(state, project, self.request.user)
TatorSearch().delete_document(state)
- cl = ChangeLog(project=project, user=self.request.user, description_of_change=delete_dict)
- cl.save()
- ChangeToObject(ref_table=ref_table, ref_id=ref_id, change_id=cl).save()
qs = Localization.objects.filter(pk__in=delete_localizations)
- qs.update(deleted=True,
- modified_datetime=datetime.datetime.now(datetime.timezone.utc),
- modified_by=self.request.user)
+ bulk_delete_and_log_changes(qs, project, self.request.user)
for loc in qs.iterator():
TatorSearch().delete_document(loc)
diff --git a/main/rest/transcode.py b/main/rest/transcode.py
index 487938335..9a9996c07 100644
--- a/main/rest/transcode.py
+++ b/main/rest/transcode.py
@@ -60,7 +60,8 @@ def _post(self, params):
path, bucket, upload = url_to_key(url, project_obj)
if path is not None:
logger.info(f"Attempting to retrieve size for object key {path}...")
- tator_store = get_tator_store(bucket, upload=upload)
+ use_upload_bucket = upload and not bucket
+ tator_store = get_tator_store(bucket, upload=use_upload_bucket)
upload_size = tator_store.get_size(path)
# If we have the media ID, tag the object with the media ID.
if media_id:
diff --git a/main/rest/upload_completion.py b/main/rest/upload_completion.py
index 479d58414..d6a7cd819 100644
--- a/main/rest/upload_completion.py
+++ b/main/rest/upload_completion.py
@@ -28,7 +28,8 @@ def _post(self, params):
# Complete the upload.
upload = key.startswith('_uploads')
bucket = project_obj.get_bucket(upload=upload)
- tator_store = get_tator_store(bucket, upload=upload)
+ use_upload_bucket = upload and not bucket
+ tator_store = get_tator_store(bucket, upload=use_upload_bucket)
tator_store.complete_multipart_upload(key, parts, upload_id)
return {'message': f"Upload completion for {key} successful!"}
diff --git a/main/rest/upload_info.py b/main/rest/upload_info.py
index 959b2ad51..a936e21ec 100644
--- a/main/rest/upload_info.py
+++ b/main/rest/upload_info.py
@@ -56,7 +56,9 @@ def _get(self, params):
today = datetime.datetime.now().strftime('%Y-%m-%d')
user = self.request.user.pk
key = f"_uploads/{today}/{organization}/{project}/{user}/{name}"
- tator_store = get_tator_store(project_obj.get_bucket(upload=True), upload=True)
+ upload_bucket = project_obj.get_bucket(upload=True)
+ use_upload = not upload_bucket
+ tator_store = get_tator_store(upload_bucket, upload=use_upload)
elif media_id is not None and file_id is not None:
raise ValueError(f"Both a file_id and media_id was provided!")
elif media_id is not None:
diff --git a/main/schema/components/change_log.py b/main/schema/components/change_log.py
index b5f19e1c0..1f0877012 100644
--- a/main/schema/components/change_log.py
+++ b/main/schema/components/change_log.py
@@ -16,6 +16,11 @@
"type": "integer",
"description": "Unique integer identifying project of this change log.",
},
+ "modified_datetime": {
+ 'type': 'string',
+ 'format': 'date-time',
+ 'description': 'Datetime this change occurred.',
+ },
"description_of_change": {
"type": "object",
"description": "The old and new values for the changed object",
diff --git a/main/schema/section.py b/main/schema/section.py
index 864877700..a9536a99e 100644
--- a/main/schema/section.py
+++ b/main/schema/section.py
@@ -26,9 +26,11 @@ def get_operation(self, path, method):
def get_description(self, path, method):
if method == 'GET':
short_desc = "Get section list."
+ long_desc = ""
elif method == 'POST':
short_desc = "Create section."
- return f"{short_desc}\n\n{boilerplate}"
+ long_desc = "Note: In order for a section to be interpreted properly, the tator_user_sections attribute of the SectionSpec cannot be None. The front end assigns a uuid1 string for this attribute, but it is not required to follow this pattern."
+ return f"{short_desc}\n\n{boilerplate}\n\n{long_desc}"
def get_path_parameters(self, path, method):
return [{
@@ -40,13 +42,16 @@ def get_path_parameters(self, path, method):
}]
def get_filter_parameters(self, path, method):
- return [{
- 'name': 'name',
- 'in': 'query',
- 'required': False,
- 'description': 'Name of the section.',
- 'schema': {'type': 'string'},
- }]
+ params = []
+ if method == 'GET':
+ params = [{
+ 'name': 'name',
+ 'in': 'query',
+ 'required': False,
+ 'description': 'Name of the section.',
+ 'schema': {'type': 'string'},
+ }]
+ return params
def get_request_body(self, path, method):
body = {}
diff --git a/main/search.py b/main/search.py
index 44049d836..1e9c86ea5 100644
--- a/main/search.py
+++ b/main/search.py
@@ -66,9 +66,10 @@ def _get_mapping_values(entity_type, attributes):
value = attributes.get(name)
if value is not None:
mapping_values[name] = str(value).replace("\\", "\\\\")
+ mapping_types[name] = "text"
if entity_type.attribute_types is None:
- return mapping_values
+ return mapping_values, mapping_types
for attribute_type in entity_type.attribute_types:
name = attribute_type['name']
diff --git a/main/store.py b/main/store.py
index a86920d58..bef5243f4 100644
--- a/main/store.py
+++ b/main/store.py
@@ -579,8 +579,10 @@ def get_tator_store(
gcs_key_info = json.loads(bucket.gcs_key_info)
gcs_project = gcs_key_info["project_id"]
client = storage.Client(gcs_project, Credentials.from_service_account_info(gcs_key_info))
- # TODO get rclone_config_create_params for GCP storage
- rclone_config_create_params = {}
+ rclone_config_create_params = {
+ "project_number": gcs_key_info["project_id"],
+ "service_account_credentials": bucket.gcs_key_info,
+ }
return TatorStorage.get_tator_store(
ObjectStore.GCP, bucket, client, bucket.name, rclone_config_create_params
)
diff --git a/main/tests.py b/main/tests.py
index 90ccd78f7..777bf6e02 100644
--- a/main/tests.py
+++ b/main/tests.py
@@ -2945,6 +2945,7 @@ def setUp(self):
attribute_types=create_test_attribute_types(),
)
self.store = get_tator_store()
+ self.backup_bucket = None
def tearDown(self):
self.project.delete()
@@ -3380,19 +3381,20 @@ def test_backup_lifecycle(self):
if success:
n_successful_backups += 1
- self.assertEqual(n_successful_backups, len(all_keys))
+ self.assertEqual(n_successful_backups, len(all_keys) if self.backup_bucket else 0)
# Check the value of each resource's `backed_up` flag is `True`
resource_qs = Resource.objects.filter(path__in=all_keys, backed_up=True)
- self.assertEqual(resource_qs.count(), len(all_keys))
+ self.assertEqual(resource_qs.count(), len(all_keys) if self.backup_bucket else 0)
# Check that each resource was copied to the backup bucket
- success, store_info = TatorBackupManager().get_store_info(self.project)
- self.assertTrue(success)
- success, store = TatorBackupManager.get_backup_store(store_info)
- self.assertTrue(success)
- for resource in resource_qs.iterator():
- self.assertTrue(store.check_key(resource.path))
+ if self.backup_bucket:
+ success, store_info = TatorBackupManager().get_store_info(self.project)
+ self.assertTrue(success)
+ success, store = TatorBackupManager.get_backup_store(store_info)
+ self.assertTrue(success)
+ for resource in resource_qs.iterator():
+ self.assertTrue(store.check_key(resource.path))
class ResourceWithBackupTestCase(ResourceTestCase):
diff --git a/scripts/packages/tator-py b/scripts/packages/tator-py
index 52ec0e3c8..85dc71f28 160000
--- a/scripts/packages/tator-py
+++ b/scripts/packages/tator-py
@@ -1 +1 @@
-Subproject commit 52ec0e3c81bc12ef358d73d41ea4dddf54a7bec3
+Subproject commit 85dc71f2801d4375dd04f23e8eb8af23e2d4c23c
diff --git a/test/conftest.py b/test/conftest.py
index 2a2bf060b..f2aa64df7 100644
--- a/test/conftest.py
+++ b/test/conftest.py
@@ -109,6 +109,7 @@ def token(request, page_factory):
page.click('input[type="submit"]')
page.wait_for_selector('text=Your API token is:')
token = page.text_content('modal-notify .modal__main p')
+ page.close()
assert(len(token) == 40)
yield token
@@ -139,6 +140,7 @@ def project(request, page_factory, launch_time, base_url, token):
href = link.get_attribute('href')
project_id = int(href.split('/')[-2])
break
+ page.close()
yield project_id
@pytest.fixture(scope='session')
@@ -151,6 +153,7 @@ def video_section(request, page_factory, project):
page.click('text="Save"')
page.click('text="Videos"')
section = int(page.url.split('=')[-1])
+ page.close()
yield section
@pytest.fixture(scope='session')
@@ -163,6 +166,7 @@ def slow_video_section(request, page_factory, project):
page.click('text="Save"')
page.click('text="Slow Videos"')
section = int(page.url.split('=')[-1])
+ page.close()
yield section
@pytest.fixture(scope='session')
@@ -175,6 +179,7 @@ def video_section2(request, page_factory, project):
page.click('text="Save"')
page.click('text="Videos 2"')
section = int(page.url.split('=')[-1])
+ page.close()
yield section
@pytest.fixture(scope='session')
@@ -187,6 +192,7 @@ def video_section3(request, page_factory, project):
page.click('text="Save"')
page.click('text="Videos 3"')
section = int(page.url.split('=')[-1])
+ page.close()
yield section
@pytest.fixture(scope='session')
@@ -199,6 +205,7 @@ def image_section(request, page_factory, project):
page.click('text="Save"')
page.click('text="Images"')
section = int(page.url.split('=')[-1])
+ page.close()
yield section
@pytest.fixture(scope='session')
@@ -241,8 +248,9 @@ def video(request, page_factory, project, video_section, video_file):
page.set_input_files('section-upload input', video_file)
page.query_selector('upload-dialog').query_selector('text=Close').click()
while True:
- page.click('reload-button')
- cards = page.query_selector_all('media-card')
+ page.locator('.project__header reload-button').click()
+ page.wait_for_load_state('networkidle')
+ cards = page.query_selector_all('entity-card')
if len(cards) == 0:
continue
href = cards[0].query_selector('a').get_attribute('href')
@@ -250,6 +258,7 @@ def video(request, page_factory, project, video_section, video_file):
print(f"Card href is {href}, media is ready...")
break
video = int(cards[0].get_attribute('media-id'))
+ page.close()
yield video
@pytest.fixture(scope='session')
@@ -269,8 +278,9 @@ def slow_video(request, page_factory, project, slow_video_section, slow_video_fi
page.set_input_files('section-upload input', slow_video_file)
page.query_selector('upload-dialog').query_selector('text=Close').click()
while True:
- page.click('reload-button')
- cards = page.query_selector_all('media-card')
+ page.locator('.project__header reload-button').click()
+ page.wait_for_load_state('networkidle')
+ cards = page.query_selector_all('entity-card')
if len(cards) == 0:
continue
href = cards[0].query_selector('a').get_attribute('href')
@@ -278,6 +288,7 @@ def slow_video(request, page_factory, project, slow_video_section, slow_video_fi
print(f"Card href is {href}, media is ready...")
break
video = int(cards[0].get_attribute('media-id'))
+ page.close()
yield video
@pytest.fixture(scope='session')
@@ -289,8 +300,9 @@ def video2(request, page_factory, project, video_section2, video_file):
page.set_input_files('section-upload input', video_file)
page.query_selector('upload-dialog').query_selector('text=Close').click()
while True:
- page.click('reload-button')
- cards = page.query_selector_all('media-card')
+ page.locator('.project__header reload-button').click()
+ page.wait_for_load_state('networkidle')
+ cards = page.query_selector_all('entity-card')
if len(cards) == 0:
continue
href = cards[0].query_selector('a').get_attribute('href')
@@ -298,6 +310,7 @@ def video2(request, page_factory, project, video_section2, video_file):
print(f"Card href is {href}, media is ready...")
break
video = int(cards[0].get_attribute('media-id'))
+ page.close()
yield video
@pytest.fixture(scope='session')
@@ -309,8 +322,9 @@ def video3(request, page_factory, project, video_section3, video_file):
page.set_input_files('section-upload input', video_file)
page.query_selector('upload-dialog').query_selector('text=Close').click()
while True:
- page.click('reload-button')
- cards = page.query_selector_all('media-card')
+ page.locator('.project__header reload-button').click()
+ page.wait_for_load_state('networkidle')
+ cards = page.query_selector_all('entity-card')
if len(cards) == 0:
continue
href = cards[0].query_selector('a').get_attribute('href')
@@ -318,6 +332,7 @@ def video3(request, page_factory, project, video_section3, video_file):
print(f"Card href is {href}, media is ready...")
break
video = int(cards[0].get_attribute('media-id'))
+ page.close()
yield video
@pytest.fixture(scope='session')
@@ -347,8 +362,9 @@ def image(request, page_factory, project, image_section, image_file):
page.set_input_files('section-upload input', image_file)
page.query_selector('upload-dialog').query_selector('text=Close').click()
while True:
- page.click('reload-button')
- cards = page.query_selector_all('media-card')
+ page.locator('.project__header reload-button').click()
+ page.wait_for_load_state('networkidle')
+ cards = page.query_selector_all('entity-card')
if len(cards) == 0:
continue
href = cards[0].query_selector('a').get_attribute('href')
@@ -356,6 +372,7 @@ def image(request, page_factory, project, image_section, image_file):
print(f"Card href is {href}, media is ready...")
break
image = int(cards[0].get_attribute('media-id'))
+ page.close()
yield image
@pytest.fixture(scope='session')
diff --git a/test/test_annotation.py b/test/test_annotation.py
index cb9d7f90f..96023dc4a 100644
--- a/test/test_annotation.py
+++ b/test/test_annotation.py
@@ -23,8 +23,11 @@ def common_annotation(page, canvas, bias=0):
width = 100
height = 100
page.mouse.move(x, y, steps=50)
+ time.sleep(1)
page.mouse.down()
+ time.sleep(1)
page.mouse.move(x + width, y + height, steps=50)
+ time.sleep(1)
page.mouse.up()
page.wait_for_selector('save-dialog.is-open')
save_dialog = page.query_selector('save-dialog.is-open')
@@ -41,11 +44,15 @@ def common_annotation(page, canvas, bias=0):
x += canvas_center_x
y += canvas_center_y
page.mouse.move(x+50, y+50, steps=50)
+ time.sleep(1)
page.mouse.click(x+50, y+50)
selector = page.query_selector('entity-selector:visible')
selector.wait_for_selector(f'#current-index :text("{idx+1+bias}")')
+ time.sleep(1)
page.mouse.down()
+ time.sleep(1)
page.mouse.move(x, y, steps=50)
+ time.sleep(1)
page.mouse.up()
light = page.query_selector('#tator-success-light')
light.wait_for_element_state('visible')
@@ -57,11 +64,15 @@ def common_annotation(page, canvas, bias=0):
x += canvas_center_x
y += canvas_center_y
page.mouse.move(x+45, y+45, steps=50)
+ time.sleep(1)
page.mouse.click(x+45, y+45)
selector = page.query_selector('entity-selector:visible')
selector.wait_for_selector(f'#current-index :text("{idx+1+bias}")')
+ time.sleep(1)
page.mouse.down()
+ time.sleep(1)
page.mouse.move(x+95, y+95, steps=50)
+ time.sleep(1)
page.mouse.up()
light = page.query_selector('#tator-success-light')
light.wait_for_element_state('visible')
diff --git a/test/test_project-detail.py b/test/test_project-detail.py
index 21f5e6e1e..60fedb916 100644
--- a/test/test_project-detail.py
+++ b/test/test_project-detail.py
@@ -1,8 +1,8 @@
import os
import time
import inspect
+import requests
import math
-import subprocess
from ._common import print_page_error
@@ -10,19 +10,20 @@
# Change pagination to 10
# Search & create a saved search section
# Optional- Go to media, bookmark it? or last visited?
-def test_features(request, page_factory, project):
- print("Project Detail Page Feature tests...")
+def test_basic(request, page_factory, project): #video
+ print("Project Detail Page tests...")
page = page_factory(
f"{os.path.basename(__file__)}__{inspect.stack()[0][3]}")
- page.goto(f"/{project}/project-detail", wait_until='networkidle')
+ page.goto(f"/{project}/project-detail")
page.on("pageerror", print_page_error)
+ print("Start: Test Pagination and image upload")
page.select_option('.pagination select.form-select', value="100")
# page.wait_for_selector('text="Page 1 of 1"')
- time.sleep(5)
+ page.wait_for_timeout(5000)
# Initial card length
- cards = page.query_selector_all('media-card[style="display: block;"]')
+ cards = page.query_selector_all('section-files entity-card[style="display: block;"]')
initialCardLength = len(cards)
newCardsLength = 15
totalCards = initialCardLength + newCardsLength
@@ -30,28 +31,41 @@ def test_features(request, page_factory, project):
nasa_space_photo_1 = '/tmp/hubble-sees-the-wings-of-a-butterfly.jpg'
if not os.path.exists(nasa_space_photo_1):
url = 'https://images-assets.nasa.gov/image/hubble-sees-the-wings-of-a-butterfly-the-twin-jet-nebula_20283986193_o/hubble-sees-the-wings-of-a-butterfly-the-twin-jet-nebula_20283986193_o~small.jpg'
- subprocess.run(['wget', '-O', nasa_space_photo_1, url])
+ with requests.get(url, stream=True) as r:
+ r.raise_for_status()
+ with open(nasa_space_photo_1, 'wb') as f:
+ for chunk in r.iter_content(chunk_size=8192):
+ if chunk:
+ f.write(chunk)
nasa_space_photo_2 = '/tmp/layers-in-galle-crater.jpg'
if not os.path.exists(nasa_space_photo_2):
url = 'https://images-assets.nasa.gov/image/PIA21575/PIA21575~medium.jpg'
- subprocess.run(['wget', '-O', nasa_space_photo_2, url])
+ with requests.get(url, stream=True) as r:
+ r.raise_for_status()
+ with open(nasa_space_photo_2, 'wb') as f:
+ for chunk in r.iter_content(chunk_size=8192):
+ if chunk:
+ f.write(chunk)
nasa_space_photo_3 = '/tmp/behemoth-black-hole.jpg'
if not os.path.exists(nasa_space_photo_3):
url = 'https://images-assets.nasa.gov/image/behemoth-black-hole-found-in-an-unlikely-place_26209716511_o/behemoth-black-hole-found-in-an-unlikely-place_26209716511_o~medium.jpg'
- subprocess.run(['wget', '-O', nasa_space_photo_3, url])
+ with requests.get(url, stream=True) as r:
+ r.raise_for_status()
+ with open(nasa_space_photo_3, 'wb') as f:
+ for chunk in r.iter_content(chunk_size=8192):
+ if chunk:
+ f.write(chunk)
page.set_input_files('section-upload input', [nasa_space_photo_1,nasa_space_photo_2,nasa_space_photo_3,nasa_space_photo_2,nasa_space_photo_2,nasa_space_photo_3,nasa_space_photo_1,nasa_space_photo_1,nasa_space_photo_1,nasa_space_photo_1,nasa_space_photo_1,nasa_space_photo_1,nasa_space_photo_1,nasa_space_photo_1,nasa_space_photo_1])
page.query_selector('upload-dialog').query_selector('text=Close').click()
- page.click('reload-button')
- page.wait_for_selector('text=hubble')
- page.wait_for_selector('text=galle')
- page.wait_for_selector('text=behemoth')
- time.sleep(5)
+ page.locator('.project__header reload-button').click()
+ page.wait_for_selector('section-files entity-card')
+ page.wait_for_timeout(5000)
- cards = page.query_selector_all('media-card[style="display: block;"]')
+ cards = page.query_selector_all('section-files entity-card[style="display: block;"]')
cardLength = len(cards) # existing + new cards
print(f"Length of cards {cardLength} == should match totalCards {totalCards}")
@@ -61,16 +75,16 @@ def test_features(request, page_factory, project):
page.select_option('.pagination select.form-select', value="10")
pages = int(math.ceil(totalCards / 10))
page.wait_for_selector(f'text="Page 1 of {str(pages)}"')
- time.sleep(5)
+ page.wait_for_timeout(5000)
- cardsHidden = page.query_selector_all('media-card[style="display: none;"]')
+ cardsHidden = page.query_selector_all('section-files entity-card[style="display: none;"]')
cardsHiddenLength = len(cardsHidden)
print(f"Length of cards hidden {cardsHiddenLength} == totalCards - 10 {totalCards - 10}")
totalMinus = totalCards - 10
assert cardsHiddenLength == totalMinus
- cards = page.query_selector_all('media-card[style="display: block;"]')
+ cards = page.query_selector_all('section-files entity-card[style="display: block;"]')
cardLength = len(cards)
print(f"Visible card length {cardLength} == 10")
@@ -80,9 +94,9 @@ def test_features(request, page_factory, project):
paginationLinks = page.query_selector_all('.pagination a')
paginationLinks[2].click()
page.wait_for_selector(f'text="Page 2 of {pages}"')
- time.sleep(5)
+ page.wait_for_timeout(5000)
- cards = page.query_selector_all('media-card[style="display: block;"]')
+ cards = page.query_selector_all('section-files entity-card[style="display: block;"]')
cardLength = len(cards)
totalOnSecond = totalCards - 10
if totalOnSecond > 10:
@@ -99,20 +113,22 @@ def test_features(request, page_factory, project):
cards[0].query_selector('a').click()
page.wait_for_selector('.annotation__panel h3')
page.go_back()
- time.sleep(1)
+ page.wait_for_timeout(5000)
- page.wait_for_selector('media-card')
+ page.wait_for_selector('section-files entity-card')
print(f"Is pagination preserved?")
- cards = page.query_selector_all('media-card[style="display: block;"]')
+ cards = page.query_selector_all('section-files entity-card[style="display: block;"]')
cardLength = len(cards)
totalOnSecond = totalCards - 10
if totalOnSecond > 10:
totalOnSecond = 10
print(f"(refreshed) Second page length of cards {cardLength} == {totalOnSecond}")
assert cardLength == totalOnSecond
+ print("Complete!")
# Test filtering
+ print("Start: Test Filtering")
page.click('text="Filter"')
page.wait_for_selector('filter-condition-group button.btn.btn-outline.btn-small')
page.click('filter-condition-group button.btn.btn-outline.btn-small')
@@ -127,13 +143,15 @@ def test_features(request, page_factory, project):
filterGroupButtons[0].click()
page.wait_for_selector('text="Page 1 of 1"')
- time.sleep(5)
+ # time.sleep(5)
+ page.wait_for_timeout(5000)
- cards = page.query_selector_all('media-card[style="display: block;"]')
+ cards = page.query_selector_all('section-files entity-card[style="display: block;"]')
cardLength = len(cards)
print(f"Cards length after search {cardLength} == 2")
assert cardLength == 2
+ print("Start: Test save search as section")
saveSearch = page.query_selector('text="Add current search"')
saveSearch.click()
@@ -142,8 +160,9 @@ def test_features(request, page_factory, project):
page.fill('.modal__main input[placeholder="Give it a name..."]', newSectionFromSearch)
saveButton = page.query_selector('text="Save"')
saveButton.click()
+
-
+
page.wait_for_selector(f'text="{newSectionFromSearch}"')
print(f'New section created named: {newSectionFromSearch}')
@@ -151,18 +170,114 @@ def test_features(request, page_factory, project):
clearSearch.click()
page.wait_for_selector(f'text="{totalCards} Files"')
- time.sleep(5)
+ page.wait_for_timeout(5000)
- cards = page.query_selector_all('media-card[style="display: block;"]')
+ cards = page.query_selector_all('section-files entity-card[style="display: block;"]')
cardLength = len(cards)
print(f"After search cleared cardLength {cardLength} == 10")
assert cardLength == 10
page.query_selector(f'text="{newSectionFromSearch}"').click()
page.wait_for_selector('text="2 Files"')
- time.sleep(5)
+ page.wait_for_timeout(5000)
- cards = page.query_selector_all('media-card[style="display: block;"]')
+ cards = page.query_selector_all('section-files entity-card[style="display: block;"]')
cardLength = len(cards)
print(f"Cards in saved section {cardLength} == 2")
assert cardLength == 2
+ print("Complete!")
+
+# def test_bulk_edit(request, page_factory, project): #video
+ # print("Project Detail Page tests...")
+ # page = page_factory(
+ # f"{os.path.basename(__file__)}__{inspect.stack()[0][3]}")
+ # page.goto(f"/{project}/project-detail")
+ # page.on("pageerror", print_page_error)
+
+ ## Test multiple edit.....
+ print("Start: Test media labels and mult edit")
+ # Show labels, and multiselect, close more menu
+ page.query_selector('#icon-more-horizontal').click()
+ page.wait_for_selector('text="Media Labels"')
+ page.query_selector('text="Media Labels"').click()
+
+ # select image labels & video (some in the card list and some not) @todo add video to this project
+ test_string = page.query_selector_all('.entity-gallery-labels .entity-gallery-labels--checkbox-div checkbox-input[name="Test String"]')
+ print(f'Label panel is open: found {len(test_string)} string labels....')
+ test_string[0].click() # for images
+ test_string[2].click() # for video
+
+ page.query_selector('.enitity-gallery__labels-div nav-close').click()
+
+ page.query_selector('#icon-more-horizontal').click()
+ page.wait_for_selector('text="Edit media attributes"')
+ page.locator('text="Edit media attributes"').click()
+
+ # Did label selection also preselect attributes?
+ ## There should be two input checked
+ selected = page.query_selector_all('.bulk-edit-attr-choices_bulk-edit input:checked')
+ print(f'Assert selected inputs {len(selected)} == Checked count 2')
+ assert len(selected) == 2
+
+ # Get a handle on shown cards
+ cards = page.query_selector_all('section-files entity-card[style="display: block;"]')
+ print(f"Selecting... {len(cards)} cards")
+
+ # Test select all
+ page.keyboard.press("Control+A")
+
+ ## There should be two cards selected (black hole filter still on)
+ editbutton = page.locator('.bulk-edit-submit-button .text-bold')
+ count = editbutton.all_inner_texts()
+
+ print(f'Assert selected cards {str(count)} == shown count {str(len(cards))}')
+ assert str(count[0]) == str(len(cards))
+
+ # Test escape
+ page.keyboard.press("Escape")
+
+ ## There should be 0 cards selected
+ count = editbutton.all_inner_texts()
+
+ print(f'Assert selected cards {str(count)} == 0')
+ assert str(count[0]) == '0'
+
+ # Test click selection of 1 card
+ cards[1].click()
+
+
+ ## There should be 1 card selected
+ count = editbutton.all_inner_texts()
+
+ print(f'Assert selected cards {str(count)} == 1')
+ assert str(count[0]) == '1'
+
+ ## Add some text
+ # TODO -- check the label says "not set", update it, check label is "updated"
+ # attributeShown = page.locator('section-files .entity-gallery-card__attribute span[display="block"]').innerHTML()
+ attributeShown = page.query_selector_all('.entity-gallery-card__attribute:not(.hidden)')
+ attributeShownText = attributeShown[1].text_content()
+ assert attributeShownText == ' Updates to ${this._currentMultiSelectionToId.get(Number(r.typeId)).size} Localizations with Type ID: ${r.typeId} - Updating attribute '${name}' to value: ${value} Update summary for ${this._currentMultiSelectionToId.get(Number(r.typeId)).size} ${typeText} with Type ID: ${r.typeId} - Change attribute '${name}' to value: ${value} No update for Type ID: ${r.typeId} `;
+ text += ` - No items selected to change '${name}' to value: ${value}Edit ${this._currentMultiSelection.size} Localizations?
`;
+ let typeText = this._editType == "media" ? 'Media(s)' : 'Localization(s)';
+ let text = `Edit ${this._currentMultiSelection.size} ${typeText}?
`;
let inputValueArray = this._editPanel.getValue();
-
+
let formData = [];
for (let r of inputValueArray) {
- // console.log(test);
- // if(r.typeId !== "" && typeof this._currentMultiSelectionToId.get(Number(r.typeId)) !== "undefined" && this._currentMultiSelectionToId.get(Number(r.typeId)).size > 0){
- // if (inputValueArray.length > 1) {
- // text += `
- No valid values to update
` + // no attribute, but cards are selected + if (mediaTypeInSelection) { + text += `Update summary for ${this._currentMultiSelectionToId.get(Number(r.typeId)).size} ${typeText} with Type ID: ${r.typeId}
` + text += `- Attribute does not exist on this type
`; + } else { + // no attribute and no cards -- do nothing + } } - + + + + // } else { + // return text += `- No valid values to update
` + // } + if (r.rejected !== {}) { for (let rej of Object.entries(r.rejected)) { text += `- Will not update attribute '${rej[0]}' - value is invalid, or null.
` } } - // } + } } + // console.log(`formData.length = ${formData.length}`) + // Save button is disabled if there are 0 total selected, so there should be formData - otherwise there was a bug if (formData.length == 0) { - return this.boxHelper._modalError("Error with update.", "Error"); + return this.boxHelper._modalError("Error with update: No selection found.", "Error"); } let buttonContinue = document.createElement("button"); @@ -554,63 +625,12 @@ export class GalleryBulkEdit extends TatorElement { let buttonExit = document.createElement("button"); buttonExit.setAttribute("class", "btn btn-charcoal btn-clear f1 text-semibold"); + let confirmTextExit = document.createTextNode("Exit Select Mode") buttonExit.appendChild(confirmTextExit); - button.addEventListener("click", (e) => { - e.preventDefault(); - this.boxHelper.modal._closeCallback(); - this._page.showDimmer(); - this._page.loading.showSpinner(); - let promise = Promise.resolve(); - let text = ""; - let errorText = ""; - let respCode = 0; - - for (let jsonData of formData) { - console.log(jsonData); - promise = promise.then(() => this._patchLocalizations(jsonData)).then((resp) => { - respCode = resp.status; - console.log(respCode); - return resp.json(); - }).then((data) => { - console.log("Then reading message"); - if (respCode == "200" ) { - text += `${data.message}${text}
${errorText}
`); + } + + // }); + }).catch(err => { + this._clearSelection(); + this._page.loading.hideSpinner(); + this._page.hideDimmer(); + return this.boxHelper._modalError("Error with update: " + err); + }); + + // }); + } + + _editLocalization(e, formData) { + // button.addEventListener("click", (e) => { + e.preventDefault(); + this.boxHelper.modal._closeCallback(); + this._page.showDimmer(); + this._page.loading.showSpinner(); + let promise = Promise.resolve(); + let text = ""; + let errorText = ""; + let respCode = 0; + + for (let jsonData of formData) { + // console.log(jsonData); + promise = promise.then(() => this._patchLocalizations(jsonData)).then((resp) => { + respCode = resp.status; + // console.log(respCode); + return resp.json(); + }).then((data) => { + // console.log("Then reading message"); + if (respCode == "200") { + text += `${data.message}${text}
${errorText}
`); + } + + // }); + }).catch(err => { + this._clearSelection(); + this._page.loading.hideSpinner(); + this._page.hideDimmer(); + return this.boxHelper._modalError("Error with update: " + err); + }); + + // }); } _updateShownAttributes({ typeId, values }) { - console.log(values); + // console.log(values); this._editPanel.setSelectionBoxValue({ typeId, values }); // this._comparisonPanel.newColumns({ typeId, values }); } @@ -660,9 +820,13 @@ export class GalleryBulkEdit extends TatorElement { newCardData.attributes[a] = b; } } - console.log(newCardData); - this._page._filterResults.updateCardData(newCardData); - this._page.cardData.updateBulkCache(newCardData); + // console.log(newCardData); + this._gallery.updateCardData(newCardData); + if (this._page.cardData) { + this._page.cardData.updateBulkCache(newCardData); + } else if (this._page._mediaSection) { + this._page._mediaSection.reload() + } } else { console.warn("Possibly an error with save. Could not find ID in currentSelectionObjects.") } @@ -706,7 +870,7 @@ export class GalleryBulkEdit extends TatorElement { } async _prefetch() { - console.log("PREFETCH"); + // console.log("PREFETCH"); // let condition = new FilterConditionData("", "results", "==", "true", "CACHED"); // this._page._filterView.addCachedPill(condition); @@ -718,7 +882,7 @@ export class GalleryBulkEdit extends TatorElement { if (!this._editMode) this.startEditMode(); } - prefetchWarning() { + prefetchWarning() { // if (this._editMode) { // // this._editPanel._warningShow(); // let buttonContinue = document.createElement("button") @@ -748,6 +912,6 @@ export class GalleryBulkEdit extends TatorElement { // this.boxHelper._modalWarningConfirm(`Current search results are filtered on ${names} attribute. Editing may change pagination.`, buttonExit, buttonContinue); // } } - + } customElements.define("entity-gallery-bulk-edit", GalleryBulkEdit); diff --git a/ui/src/js/components/entity-gallery/entity-gallery-card.js b/ui/src/js/components/entity-gallery/entity-gallery-card.js index b5497e7a3..dedec37dd 100644 --- a/ui/src/js/components/entity-gallery/entity-gallery-card.js +++ b/ui/src/js/components/entity-gallery/entity-gallery-card.js @@ -1,388 +1,878 @@ import { TatorElement } from "../tator-element.js"; +import { svgNamespace } from "../tator-element.js"; +import { hasPermission } from "../../util/has-permission.js"; +import { getCookie } from "../../util/get-cookie.js"; +import { fetchRetry } from "../../util/fetch-retry.js"; import Spinner from "../../../images/spinner-transparent.svg"; +import LiveThumb from "../../../images/live-thumb.png"; export class EntityCard extends TatorElement { - constructor() { - super(); - - // Entity Card - - // - @this._name Title text - // - Card is list element; Parent can be UL element, or see: EntityCardGallery - // - Card links out to one destination - // Optional: - // - @this.descDiv Div can contain anything else, desc or other (HIDDEN) - // - @this._ext Detail text - // - @this._pos_text Pagination position text - // - @this._more Menu (HIDDEN) - // - @this.getAttribute('thumb-gif') Gif for hover effect - - // List element (card) - this._li = document.createElement("li"); - this._li.setAttribute("class", "entity-card rounded-2 clickable"); - this._shadow.appendChild(this._li); - - // Link - this._link = document.createElement("a"); - this._link.setAttribute("class", "entity-card__link file__link d-flex flex-items-center text-white"); - this._link.setAttribute("href", "#"); - this._li.appendChild(this._link); - - // Image, spinner until SRC set - this._img = document.createElement("img"); - this._img.setAttribute("src", Spinner); - this._img.setAttribute("class", "entity-card__image rounded-1"); - this._link.appendChild(this._img); - - // containing div for li element (styling) - this._styledDiv = document.createElement("div"); - this._styledDiv.setAttribute("class", "entity-card__title__container py-2 px-2 lh-default"); - this._li.appendChild(this._styledDiv); - - // Title Div - this.titleDiv = document.createElement("div"); - this.titleDiv.setAttribute("class", "entity-card__title py-1"); - this._styledDiv.appendChild(this.titleDiv); - this.titleDiv.hidden = true; - - // Text for Title Div - this._name = document.createElement("a"); - this._name.setAttribute("class", "text-semibold text-white css-truncate"); - this._name.setAttribute("href", "#"); - this.titleDiv.appendChild(this._name); - - // OPTIONAL Description Div - this.descDiv = document.createElement("div"); - this.descDiv.setAttribute("class", "entity-card__description py-1 f2"); - this._styledDiv.appendChild(this.descDiv); - this.descDiv.hidden = true; // HIDDEN default - - // "More" (three dots) menu (OPTIONAL) - this._more = document.createElement("media-more"); - this._more.setAttribute("class", "entity-card__more position-relative"); + constructor() { + super(); + + // List element (card) + this._li = document.createElement("li"); + this._li.setAttribute("class", "entity-card rounded-2 clickable"); + this._shadow.appendChild(this._li); + + // Link + this._link = document.createElement("a"); + this._link.setAttribute("class", "entity-card__link file__link d-flex flex-items-center text-white"); + this._link.setAttribute("href", "#"); + this._li.appendChild(this._link); + + // Image, spinner until SRC set + this._img = document.createElement("img"); + this._img.setAttribute("src", Spinner); + this._img.setAttribute("class", "entity-card__image rounded-1"); + this._link.appendChild(this._img); + + // containing div for li element (styling) + this._styledDiv = document.createElement("div"); + this._styledDiv.setAttribute("class", "entity-card__title__container py-2 px-2 lh-default"); + this._li.appendChild(this._styledDiv); + + // Title Div + this.titleDiv = document.createElement("div"); + this.titleDiv.setAttribute("class", "entity-card__title py-1 d-flex flex-justify-between"); + this._styledDiv.appendChild(this.titleDiv); + this.titleDiv.hidden = true; + + // Section title - h2 + this._title = document.createElement("h2"); + this._title.setAttribute("class", "section__name text-hover-white text-gray py-1 px-1 css-truncate"); + this._link.appendChild(this._title); + + // Text for Title Div + this._name = document.createElement("a"); + this._name.setAttribute("class", "text-semibold text-white css-truncate"); + // this._name.setAttribute("href", "#"); + this.titleDiv.appendChild(this._name); + + // OPTIONAL Description Div + this.descDiv = document.createElement("div"); + this.descDiv.setAttribute("class", "entity-card__description py-1 f2"); + this._styledDiv.appendChild(this.descDiv); + this.descDiv.hidden = true; // HIDDEN default + + // "More" (three dots) menu (OPTIONAL) + this._more = document.createElement("media-more"); + this._more.setAttribute("class", "entity-card__more text-right "); + this._more.style.opacity = 0; + this.titleDiv.appendChild(this._more); + this._more.hidden = true; // HIDDEN default + + + + + // Lower div start + const lowerDiv = document.createElement("div"); + lowerDiv.setAttribute("class", ""); + this._styledDiv.appendChild(lowerDiv); + + const durationDiv = document.createElement("div"); + durationDiv.setAttribute("class", "d-flex flex-items-center"); + lowerDiv.appendChild(durationDiv); + + this._duration = document.createElement("span"); + this._duration.setAttribute("class", "f3 text-gray duration"); + durationDiv.appendChild(this._duration); + + // OPTIONAL bottom (contains pagination + id display) + this._bottom = document.createElement("div"); + this._bottom.setAttribute("class", "f3 d-flex flex-justify-between"); + this._styledDiv.appendChild(this._bottom); + + // OPTIONAL Detail text (ie file extension) + this._ext = document.createElement("span"); + this._ext.setAttribute("class", "f3 text-gray"); + this._ext.hidden = true; + this._bottom.appendChild(this._ext); + + // OPTIONAL Pagination position + this._pos_text = document.createElement("span"); + this._pos_text.setAttribute("class", "f3 text-gray pr-2"); + this._bottom.appendChild(this._pos_text); + + // Emblem div + this._emblemDiv = document.createElement("div"); + this._emblemDiv.setAttribute("class", "d-flex flex-items-center"); + lowerDiv.appendChild(this._emblemDiv); + + // Attachment button & emblem code + this._attachmentButton = document.createElement("button"); + this._attachmentButton.setAttribute("class", "px-1 btn-clear h2 text-gray hover-text-white"); + this._attachmentButton.style.display = "none"; + this._emblemDiv.appendChild(this._attachmentButton); + + var svg = document.createElementNS(svgNamespace, "svg"); + svg.setAttribute("width", "14"); + svg.setAttribute("height", "14"); + svg.setAttribute("viewBox", "0 0 24 24"); + svg.setAttribute("stroke", "currentColor"); + svg.setAttribute("stroke-width", "2"); + svg.setAttribute("stroke-linecap", "round"); + svg.setAttribute("stroke-linejoin", "round"); + svg.style.fill = "none"; + this._attachmentButton.appendChild(svg); + + const path = document.createElementNS(svgNamespace, "path"); + path.setAttribute("d", "M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"); + svg.appendChild(path); + + let archiveSvg = ``; + let archiveUpSvg = ``; + let archiveDownSvg = ``; + this._archiveEmblem = document.createElement("button"); + this._archiveEmblem.setAttribute("class", "px-1 btn-clear h2 text-gray hover-text-white d-flex"); + this._archiveEmblem.innerHTML = archiveSvg; + this._archiveEmblem.style.display = "none"; + this._emblemDiv.appendChild(this._archiveEmblem); + + this._archiveUpEmblem = document.createElement("button"); + this._archiveUpEmblem.setAttribute("class", "px-1 btn-clear h2 text-gray hover-text-white d-flex"); + this._archiveUpEmblem.innerHTML = archiveSvg + archiveUpSvg; + this._archiveUpEmblem.style.display = "none"; + this._emblemDiv.appendChild(this._archiveUpEmblem); + + this._archiveDownEmblem = document.createElement("button"); + this._archiveDownEmblem.setAttribute("class", "px-1 btn-clear h2 text-gray hover-text-white d-flex"); + this._archiveDownEmblem.innerHTML = archiveSvg + archiveDownSvg; + this._archiveDownEmblem.style.display = "none"; + this._emblemDiv.appendChild(this._archiveDownEmblem); + + // OPTIONAL ID data + this._id_text = document.createElement("span"); + this._id_text.setAttribute("class", "f3 text-gray px-2"); + this._bottom.appendChild(this._id_text); + + // More menu event listener (if included) + this.addEventListener("mouseenter", () => { + this._more.style.opacity = 1; + }); + + this.addEventListener("mouseleave", () => { this._more.style.opacity = 0; - this.titleDiv.appendChild(this._more); - this._more.hidden = true; // HIDDEN default - - // OPTIONAL pagination + id display - this._bottom = document.createElement("div"); - this._bottom.setAttribute("class", "f3 d-flex flex-justify-between"); - this._styledDiv.appendChild(this._bottom); - - // OPTIONAL Detail text (ie file extension) - this._ext = document.createElement("span"); - this._ext.setAttribute("class", "f3 text-gray"); - this._ext.hidden = true; - this._bottom.appendChild(this._ext); - - // OPTIONAL Pagination position - this._pos_text = document.createElement("span"); - this._pos_text.setAttribute("class", "f3 text-gray pr-2"); - - this._bottom.appendChild(this._pos_text); - - // OPTIONAL ID data - this._id_text = document.createElement("span"); - this._id_text.setAttribute("class", "f3 text-gray px-2"); - this._bottom.appendChild(this._id_text); - - // More menu styling (if included) - this.addEventListener("mouseenter", () => { - this._more.style.opacity = 1; - }); + }); + + + + this._more.addEventListener("algorithmMenu", evt => { + this.dispatchEvent( + new CustomEvent("runAlgorithm", + { + composed: true, + detail: { + algorithmName: evt.detail.algorithmName, + mediaIds: [Number(this.getAttribute("media-id"))], + projectId: this._more._project.id, + } + })); + }); + + this._more.addEventListener("annotations", evt => { + this.dispatchEvent(new CustomEvent("downloadAnnotations", { + detail: { + mediaIds: this.getAttribute("media-id"), + annotations: true + }, + composed: true + })); + }); - this.addEventListener("mouseleave", () => { - this._more.style.opacity = 0; + this._more.addEventListener("rename", evt => { + const input = document.createElement("input"); + input.setAttribute("class", "form-control input-sm1 f1"); + input.setAttribute("value", this._name.textContent); + titleDiv.replaceChild(input, this._name); + input.addEventListener("focus", evt => { + evt.target.select(); }); + input.addEventListener("keydown", evt => { + if (evt.keyCode == 13) { + evt.preventDefault(); + input.blur(); + } + }); + input.addEventListener("blur", evt => { + if (evt.target.value !== "") { + this._name.textContent = evt.target.value; + const full = evt.target.value + this._ext.textContent; + this._li.setAttribute("title", full); + } + fetch("/rest/Media/" + this.getAttribute("media-id"), { + method: "PATCH", + credentials: "same-origin", + headers: { + "X-CSRFToken": getCookie("csrftoken"), + "Accept": "application/json", + "Content-Type": "application/json" + }, + body: JSON.stringify({ + name: `${this._name.textContent}.${this._ext.textContent}`, + }), + }) + .catch(err => console.error("Failed to change name: " + err)); + titleDiv.replaceChild(this._name, evt.target); + }); + input.focus(); + }); - this.addEventListener("click", this.togglePanel.bind(this) ); + this._more.addEventListener("delete", evt => { + this.dispatchEvent(new CustomEvent("deleteFile", { + detail: { + mediaId: this.getAttribute("media-id"), + mediaName: this._name.textContent + }, + composed: true + })); + }); - // prep this var - this._tmpHidden = null; - this.attributeDivs = {}; - this._currentShownAttributes = ""; - this.multiEnabled = false; - this._multiSelectionToggle = false; - - /* Holds attributes for the card */ - this.attributesDiv = document.createElement('div'); - /* Sends events related to selection clicks */ - this.addEventListener('contextmenu', this.contextMenuHandler.bind(this)); - } - - static get observedAttributes() { - return ["thumb", "thumb-gif", "name", "processing", "pos-text"]; - } - - attributeChangedCallback(name, oldValue, newValue) { - switch (name) { - case "thumb": - if (this._thumb != newValue) { - this._img.setAttribute("src", newValue); - this._img.onload = () => {this.dispatchEvent(new Event("loaded"))}; - this._thumb = newValue; - } - break; - case "thumb-gif": - if (this._thumbGif != newValue) { - this._thumbGif = newValue; - this._li.addEventListener("mouseenter", () => { - if (this.hasAttribute("thumb-gif")) { - this._img.setAttribute("src", this.getAttribute("thumb-gif")); - } - }); - this._li.addEventListener("mouseleave", () => { - if (this.hasAttribute("thumb")) { - this._img.setAttribute("src", this.getAttribute("thumb")); - } - }); - } - break; - case "name": - const dot = Math.max(0, newValue.lastIndexOf(".") || Infinity); - const ext = newValue.slice(dot + 1); - this._ext.textContent = ext.toUpperCase(); - this._name.textContent = newValue.slice(0, dot); - this._li.setAttribute("title", newValue); - this._more.setAttribute("name", newValue); - break; - case "processing": - if (newValue === null) { - this._more.removeAttribute("processing"); - } else { - this._more.setAttribute("processing", ""); - } - break; - case "pos-text": - this._pos_text.textContent = newValue; + // Attachment button listener + this._attachmentButton.addEventListener("click", () => { + this.dispatchEvent(new CustomEvent("attachments", { + composed: true, + detail: this._attachments, + })); + }); + + + // Card click / List item click listener + this.addEventListener("click", this.togglePanel.bind(this)); + this._link.addEventListener("click", (e) => { + if (this._multiEnabled) { + e.preventDefault(); + } + }); + this._name.addEventListener("click", (e) => { + if (this._multiEnabled) { + e.preventDefault(); } + }); + + // prep this var + this._tmpHidden = null; + this.attributeDivs = {}; + this._currentShownAttributes = ""; + this.multiEnabled = false; + this._multiSelectionToggle = false; + + /* Holds attributes for the card */ + this.attributesDiv = document.createElement('div'); + + /* Sends events related to selection clicks */ + this.addEventListener('contextmenu', this.contextMenuHandler.bind(this)); + + // + this._sectionInit = false; + this._mediaInit = false; + } + + set multiEnabled(val) { + // console.log("multiEnabled set..."+val) + this._multiEnabled = val; + this._multiSelectionToggle = val; + + if (val) { + this._li.classList.add("multi-select"); + } else { + this._li.classList.remove("multi-select"); + } + } + + static get observedAttributes() { + return ["thumb", "thumb-gif", "name", "processing", "pos-text", "duration"]; + } + + attributeChangedCallback(name, oldValue, newValue) { + switch (name) { + case "thumb": + if (this._thumb != newValue) { + this._img.setAttribute("src", newValue); + this._img.onload = () => { this.dispatchEvent(new Event("loaded")) }; + this._thumb = newValue; + } + break; + case "thumb-gif": + if (this._thumbGif != newValue) { + this._thumbGif = newValue; + this._li.addEventListener("mouseenter", () => { + if (this.hasAttribute("thumb-gif")) { + this._img.setAttribute("src", this.getAttribute("thumb-gif")); + } + }); + this._li.addEventListener("mouseleave", () => { + if (this.hasAttribute("thumb")) { + this._img.setAttribute("src", this.getAttribute("thumb")); + } + }); + } + break; + case "name": + const dot = Math.max(0, newValue.lastIndexOf(".") || Infinity); + const ext = newValue.slice(dot + 1); + this._ext.textContent = ext.toUpperCase(); + this._name.textContent = newValue.slice(0, dot); + this._li.setAttribute("title", newValue); + this._more.setAttribute("name", newValue); + break; + case "processing": + if (newValue === null) { + this._more.removeAttribute("processing"); + } else { + this._more.setAttribute("processing", ""); + } + break; + case "duration": + if (newValue !== null && newValue !== 'null') { // + this._duration.textContent = newValue; + } else { + this._duration.textContent = ""; + } + case "pos-text": + this._pos_text.textContent = newValue; } - - + } + + - init({ obj, panelContainer, cardLabelsChosen, enableMultiselect = false, idx = null }) { + init({ obj, panelContainer = null, cardLabelsChosen = null, enableMultiselect = false, idx = null, mediaInit = false }) { // Give card access to panel this.panelContainer = panelContainer; this.cardObj = obj; this.multiEnabled = enableMultiselect; this._idx = idx; + this._mediaInit = mediaInit; if (this._idx !== null) { - console.log(`Tab index ${this._idx}`); - this._li.setAttribute("tabindex", this._idx ) + // console.log(`Tab index ${this._idx}`); + this._li.setAttribute("tabindex", this._idx) } - + + if (!mediaInit) { // ID is title this._id_text.innerHTML = `ID: ${this.cardObj.id}`; - - // Graphic - if(typeof this.cardObj.image !== "undefined" && this.cardObj.image !== null) { - //this.setAttribute("thumb", obj.image); - this.setImageStatic(obj.image); - } else if(typeof obj.graphic !== "undefined" && obj.graphic !== null) { - this.reader = new FileReader(); - this.reader.readAsDataURL(obj.graphic); // converts the blob to base64 - this.reader.addEventListener("load", this._setImgSrc.bind(this)); - } else { - //this.setAttribute("thumb", Spinner); - this.setImageStatic(Spinner); - } - + } + + + // Graphic + if (typeof this.cardObj.image !== "undefined" && this.cardObj.image !== null) { + //this.setAttribute("thumb", obj.image); + this.setImageStatic(obj.image); + } else if (typeof obj.graphic !== "undefined" && obj.graphic !== null) { + this.reader = new FileReader(); + this.reader.readAsDataURL(obj.graphic); // converts the blob to base64 + this.reader.addEventListener("load", this._setImgSrc.bind(this)); + } else if (!mediaInit) { + //this.setAttribute("thumb", Spinner); + this.setImageStatic(Spinner); + } + + if (obj.posText) { // Add position text related to pagination this.setAttribute("pos-text", obj.posText); - - - /** - * Attributes hidden on card are controlled by outer menu - */ + } + + + /** + * Attributes hidden on card are controlled by outer menu + */ if (obj.attributeOrder && obj.attributeOrder.length > 0) { - // Clear this in case of reuse / re-init - this.attributesDiv.innerHTML = ""; - for(const attr of obj.attributeOrder){ - let attrStyleDiv = document.createElement("div"); - attrStyleDiv.setAttribute("class", `entity-gallery-card__attribute`); - - let attrLabel = document.createElement("span"); - attrLabel.setAttribute("class", "f3 text-gray text-normal"); - attrStyleDiv.appendChild(attrLabel); - - let key = attr.name; - if(obj.attributes !== null && typeof obj.attributes[key] !== "undefined" && obj.attributes[key] !== null && obj.attributes[key] !== ""){ - attrLabel.appendChild( document.createTextNode(`${obj.attributes[key]}`) ); + // console.log("Setting up labels on card with this data:"); + // console.log(obj); + // Clear this in case of reuse / re-init + this.attributesDiv.innerHTML = ""; + for (const attr of obj.attributeOrder) { + let attrStyleDiv = document.createElement("div"); + attrStyleDiv.setAttribute("class", `entity-gallery-card__attribute`); + + let attrLabel = document.createElement("span"); + attrLabel.setAttribute("class", "f3 text-gray text-normal"); + attrStyleDiv.appendChild(attrLabel); + + let key = attr.name; + if (obj.attributes !== null && typeof obj.attributes[key] !== "undefined" && obj.attributes[key] !== null && obj.attributes[key] !== "") { + attrLabel.appendChild(document.createTextNode(`${obj.attributes[key]}`)); + } else { + attrLabel.innerHTML = `<not set>`; + } + + // add to the card & keep a list + this.attributeDivs[key] = {}; + this.attributeDivs[key].div = attrStyleDiv; + this.attributeDivs[key].value = attrLabel; + + if (cardLabelsChosen && Array.isArray(cardLabelsChosen) && cardLabelsChosen.length > 0) { + // If we have any preferences saved check against it + if (cardLabelsChosen.indexOf(key) > -1) { + // console.log("FOUND "+key+" at index "+cardLabelsChosen.indexOf(key)); } else { - attrLabel.innerHTML =`<not set>`; + attrStyleDiv.classList.add("hidden"); } - - // add to the card & keep a list - this.attributeDivs[key] = {}; - this.attributeDivs[key].div = attrStyleDiv; - this.attributeDivs[key].value = attrLabel; - - if(cardLabelsChosen && Array.isArray(cardLabelsChosen) && cardLabelsChosen.length > 0){ - // If we have any preferences saved check against it - if(cardLabelsChosen.indexOf(key) > -1) { - // console.log("FOUND "+key+" at index "+cardLabelsChosen.indexOf(key)); - } else { - attrStyleDiv.classList.add("hidden"); - } - } - - this.attributesDiv.appendChild(attrStyleDiv); + } else { + attrStyleDiv.classList.add("hidden"); } - - if(this.attributeDivs){ - // Show description div - this.descDiv.appendChild(this.attributesDiv); - this.descDiv.hidden = false; + + this.attributesDiv.appendChild(attrStyleDiv); + } + + if (this.attributeDivs) { + // Show description div + this.descDiv.appendChild(this.attributesDiv); + this.descDiv.hidden = false; + } + } + } + + /** + * Custom label display update + */ + _updateShownAttributes(evt) { + // console.log("_updateShownAttributes, evt.detail.value="); + // console.log(evt.detail); + // console.log(this.cardObj); + let labelValues = evt.detail.value; + + if (this.attributeDivs && evt.detail.typeId === this.cardObj.entityType.id) { + // show selected + for (let [key, value] of Object.entries(this.attributeDivs)) { + if (labelValues.includes(key)) { + value.div.classList.remove("hidden"); + } else { + value.div.classList.add("hidden"); } } } - - /** - * Custom label display update - */ - _updateShownAttributes(evt){ - let labelValues = evt.detail.value; - - if(this.attributeDivs){ - // show selected - for (let [key, value] of Object.entries(this.attributeDivs)) { - if(labelValues.includes(key)){ - value.div.classList.remove("hidden"); + } + + /** + * Update Attribute Values + * - If side panel is edited the card needs to update attributes + */ + _updateAttributeValues(data) { + if (data.entityType.id == this.cardObj.entityType.id) { + for (let [attr, value] of Object.entries(data.attributes)) { + if (typeof this.attributeDivs[attr] !== "undefined") { + if (this.attributeDivs[attr] != null) { + this.attributeDivs[attr].value.innerHTML = value; } else { - value.div.classList.add("hidden"); + this.attributeDivs[attr].value.innerHTML = `<not set>`; } - } + } } } - - /** - * Update Attribute Values - * - If side panel is edited the card needs to update attributes - */ - _updateAttributeValues(data) { - for (let [attr, value] of Object.entries(data.attributes)) { - if(this.attributeDivs[attr] != null){ - this.attributeDivs[attr].value.innerHTML = value; - } else { - attrLabel.innerHTML =`<not set>`; - } + } + + set posText(val) { + this.setAttribute("pos-text", val); + } + + set active(enabled) { + if (enabled) { + this._li.classList.add("is-active"); + } else { + this._li.classList.remove("is-active"); + } + } + + set project(val) { + if (!hasPermission(val.permission, "Can Edit")) { + this._more.style.display = "none"; + } + this._more.project = val; + } + + set algorithms(val) { + this._more.algorithms = val; + } + + set mediaParams(val) { + this._mediaParams = val; + } + + set media(val) { + this._media = val; + this._more.media = val; + let valid = false; + if (this._media.media_files) { + if ('streaming' in this._media.media_files || + 'layout' in this._media.media_files || + 'image' in this._media.media_files) { + valid = true; + } + if (!('thumbnail' in this._media.media_files) && 'live' in this._media.media_files) { + // Default to tator thumbnail + // TODO: Have some visual indication if stream is active. + this._img.setAttribute("src", LiveThumb); + } } - - set posText(val){ - this.setAttribute("pos-text", val); + if (valid == false) { + this._name.style.opacity = 0.35; + this._link.style.opacity = 0.35; + this._name.style.cursor = "not-allowed"; + this._link.style.cursor = "not-allowed"; } - - /** - * Set the card's main image thumbnail - * @param {image} image - */ - setImage(image) { - this.reader = new FileReader(); - this.reader.readAsDataURL(image); // converts the blob to base64 - this.reader.addEventListener("load", this._setImgSrcReader.bind(this)); + else { + let project = val.project; + if (typeof (val.project) == "undefined") { + project = val.project_id; + } + var uri = `/${project}/annotation/${val.id}?${this._mediaParams.toString()}`; + this._name.setAttribute("href", uri); + this._link.setAttribute("href", uri); + this._name.style.opacity = 1; + this._link.style.opacity = 1; + this._name.style.cursor = "pointer"; + this._link.style.cursor = "pointer"; } - - _setImgSrcReader() { - this._img.setAttribute("src", this.reader.result); - this._img.onload = () => {this.dispatchEvent(new Event("loaded"))}; + + if (this._media.archive_state == "to_archive") { + this._archiveDownEmblem.style.display = "flex"; + this._archiveDownEmblem.setAttribute("tooltip", "Pending Archival"); } - - setImageStatic(image) { - //this.setAttribute("thumb", image); - this._img.setAttribute("src", image); - this.cardObj.image = image; - this._img.onload = () => {this.dispatchEvent(new Event("loaded"))}; + else if (this._media.archive_state == "archived") { + this._archiveEmblem.style.display = "flex"; + this._archiveEmblem.setAttribute("tooltip", "Archived"); } - - togglePanel(e){ + else if (this._media.archive_state == "to_live") { + this._archiveUpEmblem.style.display = "flex"; + this._archiveUpEmblem.setAttribute("tooltip", "Pending Live"); + } + else { + this._archiveDownEmblem.style.display = "none"; + this._archiveUpEmblem.style.display = "none"; + this._archiveEmblem.style.display = "none"; + } + } + + get media() { + return this._media; + } + + set attachments(val) { + this._attachments = val; + if (val.length > 0) { + this._attachmentButton.style.display = "flex"; + } else { + this._attachmentButton.style.display = "none"; + } + } + + /** + * Set the card's main image thumbnail + * @param {image} image + */ + setImage(image) { + this.reader = new FileReader(); + this.reader.readAsDataURL(image); // converts the blob to base64 + this.reader.addEventListener("load", this._setImgSrcReader.bind(this)); + } + + _setImgSrcReader() { + this._img.setAttribute("src", this.reader.result); + this._img.onload = () => { this.dispatchEvent(new Event("loaded")) }; + } + + setImageStatic(image) { + //this.setAttribute("thumb", image); + this._img.setAttribute("src", image); + this.cardObj.image = image; + this._img.onload = () => { this.dispatchEvent(new Event("loaded")) }; + } + + togglePanel(e) { + // console.log("TOGGLE CARD ") + if (this._link.getAttribute("href") !== "#" && !this._multiEnabled) { + // follow the link... + // otherwise do some panel, or multi stuff + // console.log("clicked...."); + } else { e.preventDefault(); - if (this.multiEnabled) { + if (this._multiEnabled) { + this._multiSelectionToggle = true; + /* @ "card-click"*/ - if (e.shiftKey ) { - console.log("Shift click!"); - this._multiSelectionToggle = true; - this.dispatchEvent(new CustomEvent("shift-select", { detail: { element: this, id: this.cardObj.id, isSelected: this._li.classList.contains("is-selected") } })); //user is clicking specific cards - } + // if (e.shiftKey && !this._mediaInit) { + // // console.log("Shift click!"); + // // this._multiSelectionToggle = true; + // this.dispatchEvent(new CustomEvent("shift-select", { detail: { element: this, id: this.cardObj.id, isSelected: this._li.classList.contains("is-selected") } })); //user is clicking specific cards + // } else - if (e.code == "Enter" ) { - console.log("Enter click!... "+this._li.hasFocus()); + if (e.code == "Enter") { + // console.log("Enter click!... " + this._li.hasFocus()); if (this._li.hasFocus()) { // } - this._multiSelectionToggle = true; - this.dispatchEvent(new CustomEvent("shift-select", { detail: { element: this, id: this.cardObj.id, isSelected: this._li.classList.contains("is-selected") } })); //user is clicking specific cards - } - - if (e.ctrlKey || e.code == "Control") { + // this._multiSelectionToggle = true; + this.dispatchEvent(new CustomEvent("shift-select", { detail: { element: this, id: this.cardObj.id, isSelected: this._li.classList.contains("is-selected") } })); //user is clicking specific cards + } else if ((e.ctrlKey && !this._mediaInit) || (e.code == "Control" && !this._mediaInit)) { // usually context menu is hit, and not this keeping in case.... - this._multiSelectionToggle = true; - this.dispatchEvent(new CustomEvent("ctrl-select", { detail: { element : this, id: this.cardObj.id, isSelected: this._li.classList.contains("is-selected") } })); //user is clicking specific cards - } + // this._multiSelectionToggle = true; + this.dispatchEvent(new CustomEvent("ctrl-select", { detail: { element: this, id: this.cardObj.id, isSelected: this._li.classList.contains("is-selected") } })); //user is clicking specific cards + } else if (this._mediaInit) { + // console.log("this._li.classList.contains(is-selected .................................... "+this._li.classList.contains("is-selected")) + this.dispatchEvent(new CustomEvent("ctrl-select", { detail: { element: this, id: this.cardObj.id, isSelected: this._li.classList.contains("is-selected") } })); //user is clicking specific cards + } } - if(this._li.classList.contains("is-selected") && !this._multiSelectionToggle) { - this._deselectedCardAndPanel(); - } else if(!this._multiSelectionToggle){ + if (this._li.classList.contains("is-selected") && !this._multiSelectionToggle && !this._mediaInit && !this._sectionInit) { + this._deselectedCardAndPanel(); + } else if (!this._multiSelectionToggle && !this._mediaInit && !this._sectionInit) { this._selectedCardAndPanel(); } else { this.cardClickEvent(false); } } - - _unselectOpen() { - const cardId = this.panelContainer._panelTop._panel.getAttribute("selected-id"); - - // if it exists, close it! - if (!this._multiSelectionToggle) { - console.log("unselecting this cardId: "+cardId) - if(typeof cardId !== "undefined" && cardId !== null) { - let evt = new CustomEvent("unselected", { detail: { id: cardId } }); - this.panelContainer.dispatchEvent(evt); // this even unselected related card - } + } + + _unselectOpen() { + const cardId = this.panelContainer._panelTop._panel.getAttribute("selected-id"); + + // if it exists, close it! + if (!this._multiSelectionToggle) { + // console.log("unselecting this cardId: " + cardId) + if (typeof cardId !== "undefined" && cardId !== null) { + let evt = new CustomEvent("unselected", { detail: { id: cardId } }); + this.panelContainer.dispatchEvent(evt); // this even unselected related card } } - - _deselectedCardAndPanel(){ - this.cardClickEvent(false); - this._li.classList.remove("is-selected"); - this.annotationEvent("hide-annotation"); - } - + } + + _deselectedCardAndPanel() { + this.cardClickEvent(false); + this._li.classList.remove("is-selected"); + this.annotationEvent("hide-annotation"); + } + _selectedCardAndPanel() { // Hide open panels this._unselectOpen(); - this.cardClickEvent(true); - this.annotationEvent("open-annotation"); - this._li.classList.add("is-selected"); + this.cardClickEvent(true); + this.annotationEvent("open-annotation"); + this._li.classList.add("is-selected"); + } + + cardClickEvent(openFlag = false) { + // Send event to panel to hide the localization canvas & title + let cardClickEvent = new CustomEvent("card-click", { detail: { openFlag, cardObj: this.cardObj } }); + this.dispatchEvent(cardClickEvent); + } + + annotationEvent(evtName) { + // Send event to panel to hide the localization + let annotationEvent = new CustomEvent(evtName, { detail: { cardObj: this.cardObj } }); + this.panelContainer.dispatchEvent(annotationEvent); + } + + contextMenuHandler(e) { + if (e.ctrlKey && !this._mediaInit) { + // console.log("Card was clicked with ctrl"); + this._multiSelectionToggle = true; + e.preventDefault(); // stop contextmenu + // this.togglePanel(e); + this.dispatchEvent(new CustomEvent("ctrl-select", { detail: { element: this, id: this.cardObj.id, isSelected: this._li.classList.contains("is-selected") } })); //user is clicking specific cards + // this._li.classList.add("is-selected"); } - - cardClickEvent(openFlag = false){ - // Send event to panel to hide the localization canvas & title - let cardClickEvent = new CustomEvent("card-click", { detail : { openFlag, cardObj : this.cardObj } }); - this.dispatchEvent( cardClickEvent ); + } + + rename(name) { + this._title.textContent = name; + } + + sectionInit(section, sectionType) { + this._section = section; + this._sectionType = sectionType; + this._img.remove(); + this._styledDiv.remove(); + this._li.classList.add("section"); + this._li.classList.remove("entity-card"); + this._sectionInit = true; + + if (section === null) { + this._title.textContent = "All Media"; + } else { + this._title.textContent = section.name; } - - annotationEvent(evtName){ - // Send event to panel to hide the localization - let annotationEvent = new CustomEvent(evtName, { detail : { cardObj : this.cardObj } }); - this.panelContainer.dispatchEvent( annotationEvent ); + this._title.hidden = false; + + const svg = document.createElementNS(svgNamespace, "svg"); + svg.setAttribute("viewBox", "0 0 24 24"); + svg.setAttribute("height", "1em"); + svg.setAttribute("width", "1em"); + svg.setAttribute("fill", "none"); + svg.style.fill = "none"; + svg.setAttribute("stroke", "currentColor"); + svg.setAttribute("stroke-width", "2"); + svg.setAttribute("stroke-linecap", "round"); + svg.setAttribute("stroke-linejoin", "round"); + this._link.insertBefore(svg, this._title); + + // Null section means display all media. + if (section === null) { + const path = document.createElementNS(svgNamespace, "path"); + path.setAttribute("d", "M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"); + svg.appendChild(path); + + const poly = document.createElementNS(svgNamespace, "polyline"); + poly.setAttribute("points", "9 22 9 12 15 12 15 22"); + svg.appendChild(poly); } - - contextMenuHandler(e) { - if (e.ctrlKey) { - console.log("Card was clicked with ctrl"); - this._multiSelectionToggle = true; - e.preventDefault(); // stop contextmenu - // this.togglePanel(e); - this.dispatchEvent(new CustomEvent("ctrl-select", { detail: { element : this, id: this.cardObj.id, isSelected: this._li.classList.contains("is-selected") } })); //user is clicking specific cards - // this._li.classList.add("is-selected"); + if (sectionType == "folder") { + const path = document.createElementNS(svgNamespace, "path"); + path.setAttribute("d", "M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"); + svg.appendChild(path); + + const context = document.createElement("div"); + context.setAttribute("class", "more d-flex flex-column f2 px-3 py-2 lh-condensed"); + context.style.display = "none"; + this._shadow.appendChild(context); + + const toggle = document.createElement("toggle-button"); + if (this._section.visible) { + toggle.setAttribute("text", "Archive folder"); + } else { + toggle.setAttribute("text", "Restore folder"); } - } - + context.appendChild(toggle); + + toggle.addEventListener("click", evt => { + this._section.visible = !this._section.visible; + const sectionId = Number(); + fetch(`/rest/Section/${this._section.id}`, { + method: "PATCH", + credentials: "same-origin", + headers: { + "X-CSRFToken": getCookie("csrftoken"), + "Accept": "application/json", + "Content-Type": "application/json" + }, + body: JSON.stringify({ + "visible": this._section.visible, + }) + }) + .then(response => { + if (this._section.visible) { + toggle.setAttribute("text", "Archive folder"); + } else { + toggle.setAttribute("text", "Restore folder"); + } + this.dispatchEvent(new CustomEvent("visibilityChange", { + detail: { section: this._section } + })); + }); + }); + + this.addEventListener("contextmenu", evt => { + evt.preventDefault(); + context.style.display = "block"; + }); + + window.addEventListener("click", evt => { + context.style.display = "none"; + }); + } else if (sectionType == "savedSearch") { + const circle = document.createElementNS(svgNamespace, "circle"); + circle.setAttribute("cx", "11"); + circle.setAttribute("cy", "11"); + circle.setAttribute("r", "8"); + svg.appendChild(circle); + + const line = document.createElementNS(svgNamespace, "line"); + line.setAttribute("x1", "21"); + line.setAttribute("y1", "21"); + line.setAttribute("x2", "16.65"); + line.setAttribute("y2", "16.65"); + svg.appendChild(line); + } else if (sectionType == "bookmark") { + const path = document.createElementNS(svgNamespace, "path"); + path.setAttribute("d", "M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"); + svg.appendChild(path); + this._link.setAttribute("href", section.uri); + + // Set up bookmark management controls. + const input = document.createElement("input"); + input.setAttribute("class", "form-control input-sm f1"); + input.style.display = "none"; + this._link.appendChild(input); + + const context = document.createElement("div"); + context.setAttribute("class", "more d-flex flex-column f2 px-3 py-2 lh-condensed"); + context.style.display = "none"; + this._shadow.appendChild(context); + + const rename = document.createElement("rename-button"); + rename.setAttribute("text", "Rename"); + context.appendChild(rename); + + const remove = document.createElement("delete-button"); + remove.init("Delete"); + context.appendChild(remove); + + this._link.addEventListener("contextmenu", evt => { + evt.preventDefault(); + context.style.display = "block"; + }); + + rename.addEventListener("click", () => { + // console.log("Rename event......"); + input.style.display = "block"; + this._link.style.pointerEvents = "none"; + this._title.style.display = "none"; + input.setAttribute("value", this._title.textContent); + input.focus(); + }); + + input.addEventListener("focus", evt => { + evt.target.select(); + }); + + input.addEventListener("keydown", evt => { + if (evt.keyCode == 13) { + evt.preventDefault(); + input.blur(); + } + }); + + input.addEventListener("blur", evt => { + if (evt.target.value !== "") { + this._title.textContent = evt.target.value; + this._link.style.pointerEvents = ""; + this._section.name = evt.target.value; + fetchRetry("/rest/Bookmark/" + this._section.id, { + method: "PATCH", + headers: { + "X-CSRFToken": getCookie("csrftoken"), + "Accept": "application/json", + "Content-Type": "application/json" + }, + body: JSON.stringify({ "name": evt.target.value }), + }); + } + input.style.display = "none"; + this._title.style.display = "block"; + }); + + remove.addEventListener("click", () => { + this.parentNode.removeChild(this); + fetchRetry("/rest/Bookmark/" + this._section.id, { + method: "DELETE", + headers: { + "X-CSRFToken": getCookie("csrftoken"), + "Accept": "application/json", + "Content-Type": "application/json" + }, + }); + }); + + window.addEventListener("click", evt => { + context.style.display = "none"; + }); + } } - - customElements.define("entity-card", EntityCard); + +} + +customElements.define("entity-card", EntityCard); diff --git a/ui/src/js/components/entity-gallery/entity-gallery-labels.js b/ui/src/js/components/entity-gallery/entity-gallery-labels.js index 4b260b3d0..db7fb35ba 100644 --- a/ui/src/js/components/entity-gallery/entity-gallery-labels.js +++ b/ui/src/js/components/entity-gallery/entity-gallery-labels.js @@ -67,8 +67,8 @@ export class EntityGalleryLabels extends TatorElement { async add({ typeData, hideTypeName = false, checkedFirst = null }) { // console.log(typeData); let typeName = typeData.name ? typeData.name : ""; - if(this._shownTypes[typeData.id]) { - // don't re-add this type... + if(this._shownTypes[typeData.id] || typeData.visible == false || typeData.attribute_types.length == 0) { + // don't re-add this type, or don't add if visible=false... return false; } else { this._shownTypes[typeData.id] = true; @@ -138,14 +138,20 @@ export class EntityGalleryLabels extends TatorElement { return labelsMain; } - _getValue(typeId){ - return this._selectionValues[typeId].getValue(); + _getValue(typeId) { + if (this._selectionValues[typeId]) { + return this._selectionValues[typeId].getValue(); + } else { + return []; + } } _setValue({ typeId, values }){ // # assumes values are in the accepted format for checkbox set // let valuesList = this._getValue(typeId); + console.log("valuesList"); + console.log(valuesList); for(let box in valuesList){ if(values.contains(box.name)){ box.checked = true; @@ -187,16 +193,17 @@ export class EntityGalleryLabels extends TatorElement { sorted.push(...hiddenAttrs); // Create an array for checkbox set el - let checked = checkedFirst == null ? false : checkedFirst; + // console.log("checkedFirst "+checkedFirst) + let checkedValue = checkedFirst == null ? false : checkedFirst; for (let attr of sorted) { this.newList.push({ id: encodeURI(attr.name), name: attr.name, - checked + checked: checkedValue }); // reset checked - only check the first one - if(checked) checked = false; + checkedValue = false; } return this.newList; } diff --git a/ui/src/js/components/entity-gallery/entity-gallery-slider.js b/ui/src/js/components/entity-gallery/entity-gallery-slider.js index 9bc21717e..dd7e1ef75 100644 --- a/ui/src/js/components/entity-gallery/entity-gallery-slider.js +++ b/ui/src/js/components/entity-gallery/entity-gallery-slider.js @@ -336,12 +336,12 @@ export class EntityGallerySlider extends TatorElement { */ this.entityType = (this.association == "Localization") ? cardObj.entityType : cardObj.mediaInfo.entityType; this.entityTypeId = this.entityType.id; - // this._cardAtributeLabels.add({ + // this._cardAttributeLabels.add({ // typeData: this.entityType, // checkedFirst: true // }); - this.gallery.cardLabelsChosenByType[this.entityTypeId] = this._cardAtributeLabels._getValue(this.entityTypeId); + this.gallery.cardLabelsChosenByType[this.entityTypeId] = this._cardAttributeLabels._getValue(this.entityTypeId); this._cardLabelOptions = this.entityType.attribute_types; this._cardLabelOptions.sort((a, b) => { @@ -367,7 +367,7 @@ export class EntityGallerySlider extends TatorElement { this._resizeCards._rangeHandler(resizeValue, this._ul); }); - this._cardAtributeLabels.addEventListener("labels-update", (evt) => { + this._cardAttributeLabels.addEventListener("labels-update", (evt) => { card._updateShownAttributes(evt); this.gallery.cardLabelsChosenByType[this.entityTypeId] = evt.detail.value; let msg = `Entry labels updated`; diff --git a/ui/src/js/components/entity-panel/entity-panel-container.js b/ui/src/js/components/entity-panel/entity-panel-container.js index a7ff4f6f7..d4929990b 100644 --- a/ui/src/js/components/entity-panel/entity-panel-container.js +++ b/ui/src/js/components/entity-panel/entity-panel-container.js @@ -15,19 +15,37 @@ export class EntityPanelContainer extends TatorElement { this.el = null; } - init({ main, aside, pageModal, modelData, gallery }) { + // + init({ position = "right", main, isMediaSection = false, aside, pageModal, modelData, gallery, contents }) { this.lside = main; this.rside = aside; this.gallery = gallery; + this.position = position; + this.contents = contents; // listener to close panelContainer - if (this.gallery._customContent) { + if (isMediaSection) { + //then we have the media section.... + // this._panelTop.init({ pageModal, modelData, panelContainer: this }); + this.open = true; + this._panelTop.init({ + panelContainer: this, + customContentHandler: this.gallery.customContentHandler, + isMediaSection: true, + contents: this.contents + }) + } else if (this.gallery._customContent) { this._panelTop.init({ pageModal, modelData, panelContainer: this, customContentHandler: this.gallery.customContentHandler}); } else { this._panelTop.init({ pageModal, modelData, panelContainer: this }); } - this._panelTop._topBarArrow.addEventListener("click", this._toggleRightOnClick.bind(this)); + // if (position == "right") { + // this._panelTop._topBarArrow.addEventListener("click", this._toggleLeftOnClick.bind(this)); + // } else { + this._panelTop._topBarArrow.addEventListener("click", this._toggleRightOnClick.bind(this)); + // } + // Check and set current permission level on annotationPanel if (this.hasAttribute("permissionValue")) { @@ -70,9 +88,17 @@ export class EntityPanelContainer extends TatorElement { this.lside.classList.add("col-9"); this.lside.classList.remove("col-12"); this.lside.style.marginRight = "0"; + + if (this.position == "left") { + this.lside.style.paddingLeft = "390px"; + this.gallery._main.classList.remove("ml-6"); + this.gallery._main.classList.add("ml-3"); + } else { + this.gallery._main.classList.remove("mr-6"); + this.gallery._main.classList.add("mr-3"); + } + - this.gallery._main.classList.remove("mr-6"); - this.gallery._main.classList.add("mr-3"); this._panelTop._topBarArrow.style.transform = "scaleX(1)"; this.open = true; @@ -88,8 +114,17 @@ export class EntityPanelContainer extends TatorElement { this.lside.classList.remove("col-9"); this.lside.style.marginRight = "2%"; - this.gallery._main.classList.add("mr-6"); - this.gallery._main.classList.remove("mr-3"); + if (this.position == "left") { + this.lside.style.paddingLeft = "2%"; + this.lside.style.marginRight = "0"; + this.gallery._main.classList.add("ml-6"); + this.gallery._main.classList.remove("ml-3"); + } else { + this.gallery._main.classList.add("mr-6"); + this.gallery._main.classList.remove("mr-3"); + } + + this.open = false; this._panelTop._topBarArrow.style.transform = "scaleX(-1)"; diff --git a/ui/src/js/components/entity-panel/entity-panel-top.js b/ui/src/js/components/entity-panel/entity-panel-top.js index 03dee22bc..bc21367b2 100644 --- a/ui/src/js/components/entity-panel/entity-panel-top.js +++ b/ui/src/js/components/entity-panel/entity-panel-top.js @@ -80,8 +80,15 @@ export class EntityGalleryPanelTop extends TatorElement { } } - init({ pageModal, modelData, panelContainer, customContentHandler = false }) { - if (!customContentHandler) { + init({ pageModal, modelData, panelContainer, customContentHandler = false, isMediaSection = false, contents }) { + if (isMediaSection) { + // no heading and no data handling within panel top + this._headingText.innerHTML = ""; + this._staticImage.innerHTML = ""; + this._topBarArrow.classList.add("left"); + this.openHandler = customContentHandler; + this._box.appendChild(contents); + } else if (!customContentHandler) { if (this._locImage == undefined) { this._locImage = document.createElement("entity-panel-localization"); this._box.insertBefore(this._locImage, this._staticImage); diff --git a/ui/src/js/project-detail/media-card.js b/ui/src/js/project-detail/__bak_media-card.js similarity index 79% rename from ui/src/js/project-detail/media-card.js rename to ui/src/js/project-detail/__bak_media-card.js index 06f8277db..b3a818035 100644 --- a/ui/src/js/project-detail/media-card.js +++ b/ui/src/js/project-detail/__bak_media-card.js @@ -13,7 +13,7 @@ export class MediaCard extends TatorElement { this._li.setAttribute("class", "project__file rounded-2"); this._shadow.appendChild(this._li); - this._link = document.createElement("a"); + this._link = document.createElement("div"); this._link.setAttribute("class", "file__link d-flex flex-items-center text-white"); this._link.setAttribute("href", "#"); this._li.appendChild(this._link); @@ -246,94 +246,7 @@ export class MediaCard extends TatorElement { } } - set project(val) { - if (!hasPermission(val.permission, "Can Edit")) { - this._more.style.display = "none"; - } - this._more.project = val; - } - - set algorithms(val) { - this._more.algorithms = val; - } - - set mediaParams(val) { - this._mediaParams = val; - } - - set media(val) { - this._media = val; - this._more.media = val; - let valid = false; - if (this._media.media_files) - { - if ('streaming' in this._media.media_files || - 'layout' in this._media.media_files || - 'image' in this._media.media_files) - { - valid = true; - } - if (!('thumbnail' in this._media.media_files) && 'live' in this._media.media_files) - { - // Default to tator thumbnail - // TODO: Have some visual indication if stream is active. - this._img.setAttribute("src", LiveThumb); - - } - } - if (valid == false) - { - this._name.style.opacity = 0.35; - this._link.style.opacity = 0.35; - this._name.style.cursor = "not-allowed"; - this._link.style.cursor = "not-allowed"; - } - else - { - let project = val.project; - if(typeof(val.project) == "undefined") { - project = val.project_id; - } - var uri = `/${project}/annotation/${val.id}?${this._mediaParams.toString()}`; - this._name.setAttribute("href", uri); - this._link.setAttribute("href", uri); - this._name.style.opacity = 1; - this._link.style.opacity = 1; - this._name.style.cursor = "pointer"; - this._link.style.cursor = "pointer"; - } - - if (this._media.archive_state == "to_archive") { - this._archiveDownEmblem.style.display = "flex"; - this._archiveDownEmblem.setAttribute("tooltip", "Pending Archival"); - } - else if (this._media.archive_state == "archived") { - this._archiveEmblem.style.display = "flex"; - this._archiveEmblem.setAttribute("tooltip", "Archived"); - } - else if (this._media.archive_state == "to_live") { - this._archiveUpEmblem.style.display = "flex"; - this._archiveUpEmblem.setAttribute("tooltip", "Pending Live"); - } - else { - this._archiveDownEmblem.style.display = "none"; - this._archiveUpEmblem.style.display = "none"; - this._archiveEmblem.style.display = "none"; - } - } - - get media() { - return this._media; - } - - set attachments(val) { - this._attachments = val; - if (val.length > 0) { - this._attachmentButton.style.display = "flex"; - } else { - this._attachmentButton.style.display = "none"; - } - } + } customElements.define("media-card", MediaCard); diff --git a/ui/src/js/project-detail/__bak_section-card.js b/ui/src/js/project-detail/__bak_section-card.js new file mode 100644 index 000000000..7254583d0 --- /dev/null +++ b/ui/src/js/project-detail/__bak_section-card.js @@ -0,0 +1,40 @@ +import { TatorElement } from "../components/tator-element.js"; +import { getCookie } from "../util/get-cookie.js"; +import { fetchRetry } from "../util/fetch-retry.js"; +import { svgNamespace } from "../components/tator-element.js"; + +export class SectionCard extends TatorElement { + constructor() { + super(); + + this._li = document.createElement("li"); + this._li.style.cursor = "pointer"; + this._li.setAttribute("class", "section d-flex flex-items-center flex-justify-between px-2 rounded-1"); + this._shadow.appendChild(this._li); + + this._link = document.createElement("a"); + this._link.setAttribute("class", "section__link d-flex flex-items-center text-gray"); + this._li.appendChild(this._link); + + this._title = document.createElement("h2"); + this._title.setAttribute("class", "section__name py-1 px-1 css-truncate"); + this._link.appendChild(this._title); + + } + + + + rename(name) { + this._title.textContent = name; + } + + set active(enabled) { + if (enabled) { + this._li.classList.add("is-active"); + } else { + this._li.classList.remove("is-active"); + } + } +} + +customElements.define("section-card", SectionCard); diff --git a/ui/src/js/project-detail/media-section.js b/ui/src/js/project-detail/media-section.js index 55cc4c398..53013754b 100644 --- a/ui/src/js/project-detail/media-section.js +++ b/ui/src/js/project-detail/media-section.js @@ -55,6 +55,10 @@ export class MediaSection extends TatorElement { this._more.setAttribute("class", "px-2"); actions.appendChild(this._more); + + this._hiddenMediaLabel = document.createElement("div"); + section.appendChild(this._hiddenMediaLabel); + this._defaultPageSize = 25; this._maxPageSizeDefault = 100; @@ -71,6 +75,7 @@ export class MediaSection extends TatorElement { this._files = document.createElement("section-files"); this._files.setAttribute("class", "col-12"); this._files.mediaParams = this._sectionParams.bind(this); + div.appendChild(this._files); this._paginator_bottom = document.createElement("entity-gallery-paginator"); @@ -83,6 +88,10 @@ export class MediaSection extends TatorElement { this._numFilesCount = 0; this._searchString = ""; + this._more.addEventListener("bulk-edit", () => { + this.dispatchEvent(new Event("bulk-edit")); + }); + this._setCallbacks(); } @@ -98,6 +107,8 @@ export class MediaSection extends TatorElement { this._section = section; this._sectionName = this._sectionName; this._files.setAttribute("project-id", project); + + this._nameText.nodeValue = this._sectionName; this._upload.setAttribute("project-id", project); this._upload.setAttribute("username", username); @@ -107,10 +118,16 @@ export class MediaSection extends TatorElement { this._start = 0; this._stop = this._paginator_top._pageSize; this._after = new Map(); + return this.reload(); } + set mediaTypesMap(val) { + this._mediaTypesMap = val; + this._files.mediaTypesMap = val; + } + set project(val) { this._files.project = val; if (!hasPermission(val.permission, "Can Edit")) { @@ -169,7 +186,7 @@ export class MediaSection extends TatorElement { } removeMedia(mediaId) { - for (const mediaCard of this._files._main.children) { + for (const mediaCard of this._ul._main.children) { if (mediaCard.getAttribute("media-id") == mediaId) { mediaCard.parentNode.removeChild(mediaCard); const numFiles = Number(this._numFiles.textContent.split(' ')[0]) - 1; @@ -289,7 +306,7 @@ export class MediaSection extends TatorElement { } reload() { - console.log("Reload media..."); + console.log("Reload media section..."); this._reload.busy(); const sectionQuery = this._sectionParams(); @@ -732,6 +749,9 @@ export class MediaSection extends TatorElement { this._stop = evt.detail.stop; this._paginationState = evt.detail; + // clear any selected cards + this._bulkEdit.clearAllCheckboxes(); + otherPaginator.init(otherPaginator._numFiles, this._paginationState); this._updatePageArgs(); diff --git a/ui/src/js/project-detail/project-detail.js b/ui/src/js/project-detail/project-detail.js index 1fcc7e584..30744ce3c 100644 --- a/ui/src/js/project-detail/project-detail.js +++ b/ui/src/js/project-detail/project-detail.js @@ -14,13 +14,58 @@ export class ProjectDetail extends TatorPage { window._uploader = new Worker(new URL("../tasks/upload-worker.js", import.meta.url)); - const main = document.createElement("main"); - main.setAttribute("class", "d-flex"); - this._shadow.appendChild(main); - + // Success and warning Utility hooks + const utilitiesDiv = document.createElement("div"); + this._headerDiv = this._header._shadow.querySelector("header"); + utilitiesDiv.setAttribute("class", "annotation__header d-flex flex-items-center flex-justify-between px-6 f3"); + const user = this._header._shadow.querySelector("header-user"); + user.parentNode.insertBefore(utilitiesDiv, user); + + this._lightSpacer = document.createElement("span"); + this._lightSpacer.style.width = "32px"; + utilitiesDiv.appendChild(this._lightSpacer); + + this._success = document.createElement("success-light"); + this._lightSpacer.appendChild(this._success); + + this._warning = document.createElement("warning-light"); + this._lightSpacer.appendChild(this._warning); + + // Wrapper to allow r.side bar to slide into left + this.mainWrapper = document.createElement("div"); + this.mainWrapper.setAttribute("class", "analysis--main--wrapper col-12 d-flex"); + // this.mainWrapper.setAttribute("style", "padding-left: 25%;"); + this._shadow.appendChild(this.mainWrapper); + + // Original main element + this.main = document.createElement("main"); + this.main.setAttribute("class", "d-flex col-9"); + this.main.setAttribute("style", "padding-left: 390px;"); + this.mainWrapper.appendChild(this.main); + + // // Panel top bar + // const sectionContainer = document.createElement("entity-panel-container"); + // sectionContainer.setAttribute("class", ""); + // this.main.appendChild(sectionContainer); + + + // + /* LEFT*** Navigation Pane - Project Detail Viewer */ + this.aside = document.createElement("aside"); + this.aside.setAttribute("class", "entity-panel--container-left col-3"); //slide-close + this.aside.hidden = true; + this.mainWrapper.appendChild(this.aside); + + // Gallery navigation panel + this._panelContainer = document.createElement("entity-panel-container"); + this.aside.appendChild(this._panelContainer); + + // const section = document.createElement("section"); - section.setAttribute("class", "sections-wrap py-6 px-5 col-3 text-gray"); - main.appendChild(section); + section.setAttribute("class", "sections-wrap py-6 col-3 px-5 text-gray"); // + + // Content for panel is appended in panel code + // this._panelContainer._panelTop._shadow.appendChild(section); const folderHeader = document.createElement("div"); folderHeader.setAttribute("class", "d-flex flex-justify-between flex-items-center py-4"); @@ -101,13 +146,16 @@ export class ProjectDetail extends TatorPage { this._bookmarks.setAttribute("class", "sections"); section.appendChild(this._bookmarks); - const mainSection = document.createElement("section"); - mainSection.setAttribute("class", "project__main py-3 px-6 flex-grow"); - main.appendChild(mainSection); + this._mainSection = document.createElement("section"); + this._mainSection.setAttribute("class", "py-3 px-6 flex-grow"); //project__main + this.main.appendChild(this._mainSection); + + this.gallery = {}; + this.gallery._main = this._mainSection; const div = document.createElement("div"); div.setAttribute("class", "py-6"); - mainSection.appendChild(div); + this.gallery._main.appendChild(div); const header = document.createElement("div"); header.setAttribute("class", "main__header d-flex flex-justify-between"); @@ -155,15 +203,18 @@ export class ProjectDetail extends TatorPage { const subheader = document.createElement("div"); subheader.setAttribute("class", "d-flex flex-justify-right"); - mainSection.appendChild(subheader); + this._mainSection.appendChild(subheader); // Hidden search input this._search = document.createElement("project-search"); // subheader.appendChild(this._search); + + + const filterdiv = document.createElement("div"); filterdiv.setAttribute("class", "mt-3"); - mainSection.appendChild(filterdiv); + this._mainSection.appendChild(filterdiv); this._filterView = document.createElement("filter-interface"); this._filterView._algoButton.hidden = true; @@ -173,12 +224,50 @@ export class ProjectDetail extends TatorPage { subheader.appendChild(this._collaborators); this._projects = document.createElement("div"); - mainSection.appendChild(this._projects); + this._mainSection.appendChild(this._projects); + + // Part of Gallery: Communicates between card + page + this._bulkEdit = document.createElement("entity-gallery-bulk-edit"); + this._bulkEdit._messageBar_top.hidden = false; + this._bulkEdit._selectionPanel.hidden = true; + this._shadow.appendChild(this._bulkEdit); + filterdiv.appendChild(this._bulkEdit._selectionPanel); + // this._bulkEdit.addEventListener("multi-enabled", () => { + // // console.log("multi-enabled heard in project detail"); + // }); + + // Media section this._mediaSection = document.createElement("media-section"); this._projects.appendChild(this._mediaSection); this._mediaSection.addEventListener("runAlgorithm", this._openConfirmRunAlgoModal.bind(this)); + // Card attribute stuff related to mediaSection + /** + * CARD Label display options link for menu, and checkbox div + */ + this._cardAttributeLabels = document.createElement("entity-gallery-labels"); + this._cardAttributeLabels.titleEntityTypeName = "media"; + this._cardAttributeLabels._titleText = document.createTextNode("Select media labels to display."); + this._cardAttributeLabels.menuLinkTextSpan.innerHTML = "Media Labels"; + + this._mediaSection._hiddenMediaLabel.appendChild(this._cardAttributeLabels); + this._mediaSection._more._cardLink.appendChild(this._cardAttributeLabels.menuLink); + this._mediaSection._more.addEventListener("bulk-edit", this._openBulkEdit.bind(this)); + + this._cardAttributeLabels.addEventListener("labels-update", (evt) => { + // updates labels on cards + this._mediaSection._files.dispatchEvent(new CustomEvent("labels-update", evt.detail)); + this._bulkEdit._updateShownAttributes({ typeId: evt.detail.typeId, values: evt.detail.value }); + this._mediaSection._files.cardLabelsChosenByType[evt.detail.typeId] = evt.detail.value; + }); + + // references inner for card setup and pagination checkbox clear + this._mediaSection.bulkEdit = this._bulkEdit; + this._mediaSection._files.bulkEdit = this._bulkEdit; + + + // Confirm algorithm this._confirmRunAlgorithm = document.createElement("confirm-run-algorithm"); this._projects.appendChild(this._confirmRunAlgorithm); this._confirmRunAlgorithm.addEventListener("close", this._closeConfirmRunAlgoModal.bind(this)); @@ -189,8 +278,11 @@ export class ProjectDetail extends TatorPage { const deleteFile = document.createElement("delete-file-form"); this._projects.appendChild(deleteFile); - this._modalNotify = document.createElement("modal-notify"); - this._projects.appendChild(this._modalNotify); + this.modalNotify = document.createElement("modal-notify"); + this._projects.appendChild(this.modalNotify); + + this.modal = document.createElement("modal-dialog"); + this._projects.appendChild(this.modal); const cancelJob = document.createElement("cancel-confirm"); this._shadow.appendChild(cancelJob); @@ -205,7 +297,7 @@ export class ProjectDetail extends TatorPage { this._projects.appendChild(attachmentDialog); this._activityNav = document.createElement("activity-nav"); - main.appendChild(this._activityNav); + this.main.appendChild(this._activityNav); this._leaveConfirmOk = false; @@ -310,14 +402,14 @@ export class ProjectDetail extends TatorPage { }) .then(response => response.json()) .then(section => { - const card = document.createElement("section-card"); + const card = document.createElement("entity-card"); const sectionObj = { id: section.id, project: projectId, ...spec }; if (newSectionDialog._sectionType == "folder") { - card.init(sectionObj, "folder"); + card.sectionInit(sectionObj, "folder"); if (sectionObj.visible) { this._folders.appendChild(card); } else { @@ -335,8 +427,9 @@ export class ProjectDetail extends TatorPage { this._sectionVisibilityEL(evt) }); } else if (newSectionDialog._sectionType == "savedSearch") { - card.init(sectionObj, "savedSearch"); + card.sectionInit(sectionObj, "savedSearch"); this._savedSearches.appendChild(card); + card.addEventListener("click", () => { const clearPage = true; this._selectSection(sectionObj, projectId, clearPage); @@ -456,10 +549,6 @@ export class ProjectDetail extends TatorPage { this.removeAttribute("has-open-modal", ""); }); - this._modalNotify.addEventListener("close", evt => { - this.removeAttribute("has-open-modal", ""); - }); - this._newAlgorithmCallback = evt => { const newAlgorithm = document.createElement("new-algorithm-form"); this._projects.appendChild(newAlgorithm); @@ -483,12 +572,42 @@ export class ProjectDetail extends TatorPage { this._needScroll = true; this._lastQuery = null; + + + /* Init after modal is defined */ + // Init panel side behavior + this._panelContainer.init({ + main: this.main, + aside: this.aside, + pageModal: this.modal, + modelData: null, + gallery: this.gallery, + contents: section, + position: "left", + isMediaSection: true + }); + + + // + this.modalNotify.addEventListener("open", this.showDimmer.bind(this)); + this.modalNotify.addEventListener("close", this.hideDimmer.bind(this)); + this.modal.addEventListener("open", this.showDimmer.bind(this)); + this.modal.addEventListener("close", this.hideDimmer.bind(this)); + /* */ + + // State of chosen labels for gallery + this.cardLabelsChosenByType = {}; + this.mediaTypesMap = new Map(); } static get observedAttributes() { return ["project-id", "token"].concat(TatorPage.observedAttributes); } + _openBulkEdit() { + this._bulkEdit.startEditMode(); + } + _sectionVisibilityEL(evt) { const section = evt.detail.section; const id = section.id; @@ -505,8 +624,8 @@ export class ProjectDetail extends TatorPage { } // Create new section card and add to new list - const card = document.createElement("section-card"); - card.init(section, "folder"); + const card = document.createElement("entity-card"); + card.sectionInit(section, "folder"); if (visible) { this._folders.appendChild(card); } else { @@ -533,8 +652,8 @@ export class ProjectDetail extends TatorPage { } _notify(title, message, error_or_ok) { - this._modalNotify.init(title, message, error_or_ok); - this._modalNotify.setAttribute("is-open", ""); + this.modalNotify.init(title, message, error_or_ok); + this.modalNotify.setAttribute("is-open", ""); this.setAttribute("has-open-modal", ""); } @@ -589,26 +708,63 @@ export class ProjectDetail extends TatorPage { } }); + // Get MediaType data for attributes + const mediaTypePromise = fetch("/rest/MediaTypes/" + projectId, { + method: "GET", + credentials: "same-origin", + headers: { + "X-CSRFToken": getCookie("csrftoken"), + "Accept": "application/json", + "Content-Type": "application/json" + } + }); + // Run all above promises Promise.all([ projectPromise, sectionPromise, bookmarkPromise, algoPromise, + mediaTypePromise ]) - .then(([projectResponse, sectionResponse, bookmarkResponse, algoResponse]) => { + .then(([projectResponse, sectionResponse, bookmarkResponse, algoResponse, mediaTypeResponse]) => { const projectData = projectResponse.json(); const sectionData = sectionResponse.json(); const bookmarkData = bookmarkResponse.json(); const algoData = algoResponse.json(); + const mediaTypeData = mediaTypeResponse.json(); - Promise.all([projectData, sectionData, bookmarkData, algoData]) - .then(([project, sections, bookmarks, algos]) => { + Promise.all([projectData, sectionData, bookmarkData, algoData, mediaTypeData]) + .then(([project, sections, bookmarks, algos, mediaTypes]) => { // First hide algorithms if needed. These are not appropriate to be - // run at the project/section/media level. + // run at the project/this._section/media level. var hiddenAlgos = ['tator_extend_track', 'tator_fill_track_gaps']; const hiddenAlgoCategories = ['annotator-view']; + // + // Set up attributes for bulk edit + for (let mediaTypeData of mediaTypes) { + + //init card labels with localization entity type definitions + this._cardAttributeLabels.add({ + typeData: mediaTypeData, + checkedFirst: false + }); + + //init panel with localization entity type definitions + // console.log("ADDING MEDIA TYPE") + this._bulkEdit._editPanel.addLocType(mediaTypeData); + this.mediaTypesMap.set(mediaTypeData.id, mediaTypeData); + } + + this._mediaSection.mediaTypesMap = this.mediaTypesMap; + + // + this._mediaSection._files._cardAttributeLabels = this._cardAttributeLabels; + this._mediaSection._bulkEdit = this._bulkEdit; + this._bulkEdit.init(this, this._mediaSection._files, "media", projectId); + // this._bulkEdit._showEditPanel(); + const parsedAlgos = algos.filter(function (alg) { if (Array.isArray(alg.categories)) { for (const category of alg.categories) { @@ -632,8 +788,8 @@ export class ProjectDetail extends TatorPage { // this._search.autocomplete = project.filter_autocomplete; let projectParams = null; - const home = document.createElement("section-card"); - home.init(null, false); + const home = document.createElement("entity-card"); + home.sectionInit(null, false); home.addEventListener("click", () => { this._selectSection(null, projectId, true); for (const child of this._allSections()) { @@ -654,8 +810,8 @@ export class ProjectDetail extends TatorPage { } else { sectionType = "savedSearch"; } - const card = document.createElement("section-card"); - card.init(section, sectionType); + const card = document.createElement("entity-card"); + card.sectionInit(section, sectionType); if (sectionType == "folder") { if (section.visible) { this._folders.appendChild(card); @@ -663,7 +819,7 @@ export class ProjectDetail extends TatorPage { this._archivedFolders.appendChild(card); } card.addEventListener("visibilityChange", evt => { - this._sectionVisibilityEL(evt) + sectionVisibilityEL(evt) }); } else { this._savedSearches.appendChild(card); @@ -682,8 +838,8 @@ export class ProjectDetail extends TatorPage { const first = "Last visited"; bookmarks.sort((a, b) => { return a.name == first ? -1 : b.name == first ? 1 : 0; }); for (const bookmark of bookmarks) { - const card = document.createElement("section-card"); - card.init(bookmark, "bookmark"); + const card = document.createElement("entity-card"); + card.sectionInit(bookmark, "bookmark"); this._bookmarks.appendChild(card); } @@ -718,7 +874,7 @@ export class ProjectDetail extends TatorPage { // try { home.active = true; - this._selectSection(null, projectId).then( async () => { + this._selectSection(null, projectId).then(async () => { this.loading.hideSpinner(); this.hideDimmer(); }); @@ -748,15 +904,19 @@ export class ProjectDetail extends TatorPage { this._filterDataView.init(); this._filterView.dataView = this._filterDataView; + + // Set UI and results to any url param conditions that exist (from URL) this._mediaSection._filterConditions = this._mediaSection.getFilterConditionsObject(); + this._bulkEdit.checkForFilters(this._mediaSection._filterConditions); if (this._mediaSection._filterConditions.length > 0) { - this._updateFilterResults({ detail: { conditions: this._mediaSection._filterConditions }}); + this._updateFilterResults({ detail: { conditions: this._mediaSection._filterConditions } }); } // Listen for filter events this._filterView.addEventListener("filterParameters", this._updateFilterResults.bind(this)); + }); } catch (err) { @@ -765,15 +925,14 @@ export class ProjectDetail extends TatorPage { this.hideDimmer(); } - }).catch(err => { - console.log("Error setting up page with all promises", err); + console.error("Error setting up page with all promises", err); this.loading.hideSpinner(); this.hideDimmer(); }); }).catch(err => { - console.log("Error setting up page with all promises", err); + console.error("Error setting up page with all promises", err); this.loading.hideSpinner(); this.hideDimmer(); }); @@ -832,7 +991,7 @@ export class ProjectDetail extends TatorPage { const samePageSize = pageSize == this._mediaSection._defaultPageSize; const samePage = page == 1; - if (!samePageSize) { + if (!samePageSize) { this._mediaSection._paginator_bottom.pageSize = pageSize; this._mediaSection._paginator_top.pageSize = pageSize; } @@ -845,7 +1004,7 @@ export class ProjectDetail extends TatorPage { if (!samePageSize || !samePage) { this._mediaSection._paginator_top._emit(); this._mediaSection._paginator_bottom._emit(); - } + } } return true; @@ -925,6 +1084,7 @@ export class ProjectDetail extends TatorPage { async _updateFilterResults(evt) { this._filterConditions = evt.detail.conditions; this._filterView.setFilterConditions(this._filterConditions); + this._bulkEdit.checkForFilters(this._filterConditions); this.showDimmer(); this.loading.showSpinner(); @@ -949,14 +1109,14 @@ export class ProjectDetail extends TatorPage { this.hideDimmer(); } - // Modal for this page, and handlers - showDimmer() { - return this.setAttribute("has-open-modal", ""); - } + // Modal for this page, and handlers + showDimmer() { + return this.setAttribute("has-open-modal", ""); + } - hideDimmer() { - return this.removeAttribute("has-open-modal"); - } + hideDimmer() { + return this.removeAttribute("has-open-modal"); + } } diff --git a/ui/src/js/project-detail/section-card.js b/ui/src/js/project-detail/section-card.js deleted file mode 100644 index b5c02c165..000000000 --- a/ui/src/js/project-detail/section-card.js +++ /dev/null @@ -1,221 +0,0 @@ -import { TatorElement } from "../components/tator-element.js"; -import { getCookie } from "../util/get-cookie.js"; -import { fetchRetry } from "../util/fetch-retry.js"; -import { svgNamespace } from "../components/tator-element.js"; - -export class SectionCard extends TatorElement { - constructor() { - super(); - - this._li = document.createElement("li"); - this._li.style.cursor = "pointer"; - this._li.setAttribute("class", "section d-flex flex-items-center flex-justify-between px-2 rounded-1"); - this._shadow.appendChild(this._li); - - this._link = document.createElement("a"); - this._link.setAttribute("class", "section__link d-flex flex-items-center text-gray"); - this._li.appendChild(this._link); - - this._title = document.createElement("h2"); - this._title.setAttribute("class", "section__name py-1 px-1 css-truncate"); - this._link.appendChild(this._title); - - } - - init(section, sectionType) { - this._section = section; - this._sectionType = sectionType; - if (section === null) { - this._title.textContent = "All Media"; - } else { - this._title.textContent = section.name; - } - - const svg = document.createElementNS(svgNamespace, "svg"); - svg.setAttribute("viewBox", "0 0 24 24"); - svg.setAttribute("height", "1em"); - svg.setAttribute("width", "1em"); - svg.setAttribute("fill", "none"); - svg.style.fill = "none"; - svg.setAttribute("stroke", "currentColor"); - svg.setAttribute("stroke-width", "2"); - svg.setAttribute("stroke-linecap", "round"); - svg.setAttribute("stroke-linejoin", "round"); - this._link.insertBefore(svg, this._title); - - // Null section means display all media. - if (section === null) { - const path = document.createElementNS(svgNamespace, "path"); - path.setAttribute("d", "M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"); - svg.appendChild(path); - - const poly = document.createElementNS(svgNamespace, "polyline"); - poly.setAttribute("points", "9 22 9 12 15 12 15 22"); - svg.appendChild(poly); - } - if (sectionType == "folder") { - const path = document.createElementNS(svgNamespace, "path"); - path.setAttribute("d", "M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"); - svg.appendChild(path); - - const context = document.createElement("div"); - context.setAttribute("class", "more d-flex flex-column f2 px-3 py-2 lh-condensed"); - context.style.display = "none"; - this._shadow.appendChild(context); - - const toggle = document.createElement("toggle-button"); - if (this._section.visible) { - toggle.setAttribute("text", "Archive folder"); - } else { - toggle.setAttribute("text", "Restore folder"); - } - context.appendChild(toggle); - - toggle.addEventListener("click", evt => { - this._section.visible = !this._section.visible; - const sectionId = Number(); - fetch(`/rest/Section/${this._section.id}`, { - method: "PATCH", - credentials: "same-origin", - headers: { - "X-CSRFToken": getCookie("csrftoken"), - "Accept": "application/json", - "Content-Type": "application/json" - }, - body: JSON.stringify({ - "visible": this._section.visible, - }) - }) - .then(response => { - if (this._section.visible) { - toggle.setAttribute("text", "Archive folder"); - } else { - toggle.setAttribute("text", "Restore folder"); - } - this.dispatchEvent(new CustomEvent("visibilityChange", { - detail: {section: this._section} - })); - }); - }); - - this.addEventListener("contextmenu", evt => { - evt.preventDefault(); - context.style.display = "block"; - }); - - window.addEventListener("click", evt => { - context.style.display = "none"; - }); - } else if (sectionType == "savedSearch") { - const circle = document.createElementNS(svgNamespace, "circle"); - circle.setAttribute("cx", "11"); - circle.setAttribute("cy", "11"); - circle.setAttribute("r", "8"); - svg.appendChild(circle); - - const line = document.createElementNS(svgNamespace, "line"); - line.setAttribute("x1", "21"); - line.setAttribute("y1", "21"); - line.setAttribute("x2", "16.65"); - line.setAttribute("y2", "16.65"); - svg.appendChild(line); - } else if (sectionType == "bookmark") { - const path = document.createElementNS(svgNamespace, "path"); - path.setAttribute("d", "M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"); - svg.appendChild(path); - this._link.setAttribute("href", section.uri); - - // Set up bookmark management controls. - const input = document.createElement("input"); - input.setAttribute("class", "form-control input-sm f1"); - input.style.display = "none"; - this._link.appendChild(input); - - const context = document.createElement("div"); - context.setAttribute("class", "more d-flex flex-column f2 px-3 py-2 lh-condensed"); - context.style.display = "none"; - this._shadow.appendChild(context); - - const rename = document.createElement("rename-button"); - rename.setAttribute("text", "Rename"); - context.appendChild(rename); - - const remove = document.createElement("delete-button"); - remove.init("Delete"); - context.appendChild(remove); - - this._link.addEventListener("contextmenu", evt => { - evt.preventDefault(); - context.style.display = "block"; - }); - - rename.addEventListener("click", () => { - input.style.display = "block"; - this._link.style.pointerEvents = "none"; - this._title.style.display = "none"; - input.setAttribute("value", this._title.textContent); - input.focus(); - }); - - input.addEventListener("focus", evt => { - evt.target.select(); - }); - - input.addEventListener("keydown", evt => { - if (evt.keyCode == 13) { - evt.preventDefault(); - input.blur(); - } - }); - - input.addEventListener("blur", evt => { - if (evt.target.value !== "") { - this._title.textContent = evt.target.value; - this._link.style.pointerEvents = ""; - this._section.name = evt.target.value; - fetchRetry("/rest/Bookmark/" + this._section.id, { - method: "PATCH", - headers: { - "X-CSRFToken": getCookie("csrftoken"), - "Accept": "application/json", - "Content-Type": "application/json" - }, - body: JSON.stringify({"name": evt.target.value}), - }); - } - input.style.display = "none"; - this._title.style.display = "block"; - }); - - remove.addEventListener("click", () => { - this.parentNode.removeChild(this); - fetchRetry("/rest/Bookmark/" + this._section.id, { - method: "DELETE", - headers: { - "X-CSRFToken": getCookie("csrftoken"), - "Accept": "application/json", - "Content-Type": "application/json" - }, - }); - }); - - window.addEventListener("click", evt => { - context.style.display = "none"; - }); - } - } - - rename(name) { - this._title.textContent = name; - } - - set active(enabled) { - if (enabled) { - this._li.classList.add("is-active"); - } else { - this._li.classList.remove("is-active"); - } - } -} - -customElements.define("section-card", SectionCard); diff --git a/ui/src/js/project-detail/section-files.js b/ui/src/js/project-detail/section-files.js index 46b115261..cab2a158a 100644 --- a/ui/src/js/project-detail/section-files.js +++ b/ui/src/js/project-detail/section-files.js @@ -1,19 +1,40 @@ import { TatorElement } from "../components/tator-element.js"; import Spinner from "../../images/spinner-transparent.svg"; +import { Utilities } from "../util/utilities.js"; export class SectionFiles extends TatorElement { constructor() { super(); - this._main = document.createElement("ul"); - this._main.setAttribute("class", "project__files d-flex"); - this._shadow.appendChild(this._main); + // this._main = document.createElement("ul"); + // this._main.setAttribute("class", "project__files d-flex"); + // this._shadow.appendChild(this._main); + + this._ul = document.createElement("ul"); + this._ul.setAttribute("class", "project__files d-flex"); + this._shadow.appendChild(this._ul); + + // State of chosen labels for gallery + this.cardLabelsChosenByType = {}; + this._cardElements = []; + this._currentCardIndexes = {}; + + // + this.multiEnabled = false; } static get observedAttributes() { return ["project-id", "username", "token", "section"]; } + set bulkEdit(val) { + this._bulkEdit = val; + } + + set cardAtributeLabels(val) { + this._cardAttributeLabels = val; + } + set project(val) { this._project = val; } @@ -35,7 +56,7 @@ export class SectionFiles extends TatorElement { } set mediaIds(val) { - this._media = val.map(id => {return {id: id}}); + this._media = val.map(id => { return { id: id } }); this._updateNumCards(); } @@ -43,6 +64,10 @@ export class SectionFiles extends TatorElement { this._algorithms = val; } + set mediaTypesMap(val) { + this._mediaTypesMap = val; + } + _updateCard(card, media, pos_text) { card.setAttribute("media-id", media.id); @@ -51,6 +76,8 @@ export class SectionFiles extends TatorElement { let seconds = Number(media.num_frames) / Number(media.fps); let duration = new Date(seconds * 1000).toISOString().substr(11, 8); card.setAttribute("duration", duration); + } else { + card.setAttribute("duration", null); } card.setAttribute("thumb", Spinner); @@ -72,9 +99,11 @@ export class SectionFiles extends TatorElement { card.attachments = []; } } + card.mediaParams = this._mediaParams(); card.media = media; card.style.display = "block"; + // card.rename(media.name); card.setAttribute("name", media.name); card.setAttribute("project-id", this.getAttribute("project-id")); card.setAttribute("pos-text", pos_text) @@ -84,21 +113,47 @@ export class SectionFiles extends TatorElement { const hasAlgorithms = typeof this._algorithms !== "undefined"; const hasProject = this.hasAttribute("project-id"); if (hasAlgorithms && hasProject) { - const children = this._main.children; + const children = this._ul.children; + // const cardList = []; + this._cardElements = []; + this._currentCardIndexes = {}; for (const [index, media] of cardInfo.entries()) { const newCard = index >= children.length; + const cardObj = { + id: media.id, + entityType: this._mediaTypesMap.get(media.meta), + media: media + } let card; + let entityType = cardObj.entityType; + let entityTypeId = entityType.id; + + /** + * Card labels / attributes of localization or media type + */ + this.cardLabelsChosenByType[entityTypeId] = this._cardAttributeLabels._getValue(entityTypeId); + // this._bulkEdit._updateShownAttributes({ typeId: entityTypeId, values: this.cardLabelsChosenByType[entityTypeId] }); + + if (newCard) { - card = document.createElement("media-card"); + card = document.createElement("entity-card"); + card.titleDiv.hidden = false; + card._more.hidden = false; + card._ext.hidden = false; + card._title.hidden = false; + card._id_text.hidden = true; card.project = this._project; card.algorithms = this._algorithms; + card._li.classList.add("dark-card"); + card.addEventListener("mouseenter", () => { if (card.hasAttribute("media-id")) { this.dispatchEvent(new CustomEvent("cardMouseover", { - detail: {media: card.media} + detail: { media: card.media } })); } }); + card.addEventListener("mouseleave", () => { if (card.hasAttribute("media-id")) { this.dispatchEvent(new Event("cardMouseexit")); @@ -108,18 +163,134 @@ export class SectionFiles extends TatorElement { card = children[index]; } + // make reference lists / object + // cardList.push(card); + let cardInfo = { + card: card + }; + + + this._cardElements.push(cardInfo); + this._currentCardIndexes[cardObj.id] = index; + + + // Non-hidden attributes (ie order >= 0)) + let nonHiddenAttrs = []; + let hiddenAttrs = []; + for (let attr of entityType.attribute_types) { + if (attr.order >= 0) { + nonHiddenAttrs.push(attr); + } + else { + hiddenAttrs.push(attr); + } + } + + // Show array by order, or alpha + var cardLabelOptions = nonHiddenAttrs.sort((a, b) => { + return a.order - b.order || a.name - b.name; + }); + + cardLabelOptions.push(...hiddenAttrs); + + cardObj.attributes = media.attributes; + cardObj.attributeOrder = cardLabelOptions; + // console.log("MEDIA CARD? ................ cardObj="); + // console.log(cardObj) + + // Notifiy bulk edit about multi-select controls + card.addEventListener("ctrl-select", (e) => { + // console.log("Opening edit mode"); + this._bulkEdit._openEditMode(e); + }); + + // card.addEventListener("shift-select", (e) => { + // // console.log("Opening edit mode"); + // this._bulkEdit._openEditMode(e); + // }); + + this._bulkEdit.addEventListener("multi-enabled", () => { + // console.log("multi-enabled heard in section files"); + card.multiEnabled = true; + this.multiEnabled = true; + }); + this._bulkEdit.addEventListener("multi-disabled", () => { + // console.log("multi-disabled heard in section files"); + card.multiEnabled = false; + this.multiEnabled = false; + }); + + // + this._cardAttributeLabels.addEventListener("labels-update", (evt) => { + // console.log(evt); + + if (entityTypeId == evt.detail.typeId) { + card._updateShownAttributes(evt); + // this._bulkEdit._updateShownAttributes({ typeId: evt.detail.typeId, values: evt.detail.value }); + + // this.cardLabelsChosenByType[evt.detail.typeId] = evt.detail.value; + // let msg = `Entry labels updated`; + // Utilities.showSuccessIcon(msg); + } + + }); + const pos_text = `(${this._startMediaIndex + index + 1} of ${this._numMedia})`; - this._updateCard(card, media, pos_text); + this._updateCard(card, media, pos_text); // todo init might do most of this and is required, maybe cut it out + if (newCard) { - this._main.appendChild(card); + this._ul.appendChild(card); } + // console.log('this.cardLabelsChosenByType[entityTypeId]') + // console.log(this.cardLabelsChosenByType[entityTypeId]); + // this is data used later by label chooser, and bulk edit + card.init({ + obj: cardObj, + idx: index, + mediaInit: true, + cardLabelsChosen: this.cardLabelsChosenByType[entityTypeId], + // enableMultiselect: this.multiEnabled + }); + + // If we're still in multiselect.. check if this card should be toggled... + if (this.multiEnabled) { + const selectedArray = this._bulkEdit._currentMultiSelectionToId.get(entityType.id); + if (typeof selectedArray !== "undefined" && Array.isArray(selectedArray) && selectedArray.includes(cardObj.id)) { + this._bulkEdit._addSelected({ element: card, id: cardObj.id, isSelected: true }) + } + } + + // + // console.log("Is this.multiEnabled??? "+this.multiEnabled) + card.multiEnabled = this.multiEnabled; } + + + // Replace card info so that shift select can get cards in between + this._bulkEdit.elementList = this._cardElements; + // this._bulkEdit.elementList = cardList; + this._bulkEdit.elementIndexes = this._currentCardIndexes; + + + if (children.length > cardInfo.length) { const len = children.length; for (let idx = len - 1; idx >= cardInfo.length; idx--) { children[idx].style.display = "none"; } } + + + } + } + updateCardData(newCardData) { + if (newCardData.id in this._currentCardIndexes) { + const index = this._currentCardIndexes[newCardData.id]; + const card = this._cardElements[index].card; + // this.cardData.updateLocalizationAttributes(card.cardObj).then(() => { + // //card.displayAttributes(); + // card._updateAttributeValues(card.cardObj) + // }); } } } diff --git a/ui/src/js/project-detail/section-more.js b/ui/src/js/project-detail/section-more.js index fb348c2ad..4db274d91 100644 --- a/ui/src/js/project-detail/section-more.js +++ b/ui/src/js/project-detail/section-more.js @@ -27,29 +27,52 @@ export class SectionMore extends TatorElement { this._algorithmMenu = document.createElement("algorithm-menu"); this._div.appendChild(this._algorithmMenu); - const otherButtons = document.createElement("div"); - otherButtons.setAttribute("class", "d-flex flex-column px-4 py-3 lh-condensed"); - this._div.appendChild(otherButtons); + this._otherButtons = document.createElement("div"); + this._otherButtons.setAttribute("class", "d-flex flex-column px-4 py-3 lh-condensed"); + this._div.appendChild(this._otherButtons); + + this._cardLink = document.createElement("div"); + this._otherButtons.appendChild(this._cardLink); + + const bulkDiv = document.createElement("div"); + bulkDiv.setAttribute("id", "bulkCorrectButtonDiv"); + this._otherButtons.appendChild(bulkDiv); + + this._bulkEditMedia = document.createElement("bulk-correct-button"); + // this._bulkEditMedia.setAttribute("id", "bulkCorrectButton"); + this._bulkEditMedia.setAttribute("text", "Edit media attributes"); + bulkDiv.appendChild(this._bulkEditMedia); this._download = document.createElement("download-button"); this._download.setAttribute("text", "Download files"); - otherButtons.appendChild(this._download); + this._otherButtons.appendChild(this._download); this._annotations = document.createElement("download-button"); this._annotations.setAttribute("text", "Download metadata"); - otherButtons.appendChild(this._annotations); + this._otherButtons.appendChild(this._annotations); this._rename = document.createElement("rename-button"); this._rename.setAttribute("text", "Rename section"); - otherButtons.appendChild(this._rename); + this._otherButtons.appendChild(this._rename); this._deleteSection = document.createElement("delete-button"); this._deleteSection.init("Delete section"); - otherButtons.appendChild(this._deleteSection); + this._otherButtons.appendChild(this._deleteSection); this._deleteMedia = document.createElement("delete-button"); this._deleteMedia.init("Delete media", "text-red"); - otherButtons.appendChild(this._deleteMedia); + this._otherButtons.appendChild(this._deleteMedia); + + this._bulkEditMedia.addEventListener("click", () => { + details.removeAttribute("open"); + console.log("dispatch bulk edit!") + this.dispatchEvent(new Event("bulk-edit")); + }); + + this._cardLink.addEventListener("click", () => { + details.removeAttribute("open"); + // this.dispatchEvent(new Event("bulk-edit")); + }); this._algorithmMenu.addEventListener("click", () => { details.removeAttribute("open"); @@ -111,6 +134,7 @@ export class SectionMore extends TatorElement { set algorithms(val) { this._algorithmMenu.algorithms = val; } + } customElements.define("section-more", SectionMore); diff --git a/ui/src/js/project-settings/settings-box-helpers.js b/ui/src/js/project-settings/settings-box-helpers.js index e4299dbee..54d7c4464 100644 --- a/ui/src/js/project-settings/settings-box-helpers.js +++ b/ui/src/js/project-settings/settings-box-helpers.js @@ -252,7 +252,8 @@ export class SettingsBox { } _modalCloseCallback(){ - return this.modal._closeCallback(); + this.modal._closeCallback(); + this._modalClear(); }