From 6e0661560cb927f049bf06fad05f1cd6775485e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Dornier?= Date: Mon, 2 Sep 2024 09:28:06 +0200 Subject: [PATCH 1/6] First proof of concept --- .gitignore | 47 +++++++- omero_tagsearch/forms.py | 5 +- .../tagsearch/css/webtagging_search.css | 7 +- .../templates/omero_tagsearch/tagnav.html | 27 +++++ omero_tagsearch/views.py | 113 ++++++++++++------ 5 files changed, 154 insertions(+), 45 deletions(-) diff --git a/.gitignore b/.gitignore index f7ae72a..373c1a2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,46 @@ -*.log *.pot *.pyc -__pycache__/ local_settings.py -.DS_Store *.egg-info -dist -build \ No newline at end of file +*.log +*.env +*.history +__pycache__/ + +### IntelliJ IDEA ### +/target/ +*.iws +*.iml +*.ipr +*.releaseBackup +.idea + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +**/bin/ + +#Gradle +**/.gradle/ +gradle.properties +/build/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ +.history/ +*.vsix + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/omero_tagsearch/forms.py b/omero_tagsearch/forms.py index 1784a15..422320f 100644 --- a/omero_tagsearch/forms.py +++ b/omero_tagsearch/forms.py @@ -5,6 +5,8 @@ class TagSearchForm(Form): selectedTags = MultipleChoiceField() excludedTags = MultipleChoiceField() + selectedKeys = MultipleChoiceField() + operation = ChoiceField( widget=RadioSelect, choices=(("AND", "AND"), ("OR", "OR")), @@ -18,10 +20,11 @@ class TagSearchForm(Form): view_plate = BooleanField(initial=True) view_screen = BooleanField(initial=True) - def __init__(self, tags, conn=None, *args, **kwargs): + def __init__(self, tags, keys, conn=None, *args, **kwargs): super(TagSearchForm, self).__init__(*args, **kwargs) # Process Tags into choices (lists of tuples) self.fields["selectedTags"].choices = tags self.fields["excludedTags"].choices = tags + self.fields["selectedKeys"].choices = keys self.conn = conn diff --git a/omero_tagsearch/static/tagsearch/css/webtagging_search.css b/omero_tagsearch/static/tagsearch/css/webtagging_search.css index e39b68b..d99e6a6 100644 --- a/omero_tagsearch/static/tagsearch/css/webtagging_search.css +++ b/omero_tagsearch/static/tagsearch/css/webtagging_search.css @@ -10,11 +10,7 @@ margin-bottom:25px; } -#id_selectedTags { - width:200px; -} - -#id_excludedTags { +#id_selectedTags, #id_excludedTags, #id_selectedKeys { width:200px; } @@ -23,6 +19,7 @@ flex-direction: row; align-items: center; margin-bottom: 1px; /* Adjust as needed */ + width:200px; } .tagSearchDivider label { diff --git a/omero_tagsearch/templates/omero_tagsearch/tagnav.html b/omero_tagsearch/templates/omero_tagsearch/tagnav.html index 939dbf3..128b888 100644 --- a/omero_tagsearch/templates/omero_tagsearch/tagnav.html +++ b/omero_tagsearch/templates/omero_tagsearch/tagnav.html @@ -53,6 +53,7 @@ var objTypes = ['project', 'dataset', 'image', 'screen', 'plate', 'acquisition', 'well'] $("#id_selectedTags").chosen({placeholder_text:'Choose tags'}); $("#id_excludedTags").chosen({placeholder_text:'Choose tags to exclude'}); + $("#id_selectedKeys").chosen({placeholder_text:'Choose keys'}); $(".searching_info").tooltip({ track: true, @@ -109,16 +110,27 @@ } }); + $("#id_selectedKeys option").each(function() { + if (($.inArray(parseInt(this.value), data.navdata) == -1) && (data.navdata.length != 0)) { + $(this).hide(); + } else { + $(this).css('display', ''); + } + }); + // Preserve the order of the select because otherwise Chosen will show the same order as // the underlying select. i.e. the original option ordering var selection_incl = $('#id_selectedTags').getSelectionOrder(); var selection_excl = $('#id_excludedTags').getSelectionOrder(); + var selection_keys = $('#id_selectedKeys').getSelectionOrder(); // Update the chosen selector $("#id_selectedTags").trigger("chosen:updated"); $("#id_excludedTags").trigger("chosen:updated"); + $("#id_selectedKeys").trigger("chosen:updated"); // Restore the ordering $('#id_selectedTags').setSelectionOrder(selection_incl); $("#id_excludedTags").setSelectionOrder(selection_excl); + $('#id_selectedKeys').setSelectionOrder(selection_keys); $('#id_search').quicksearch('table#dataTable tbody tr', { 'delay': 300, @@ -161,6 +173,12 @@ $("#tagSearchForm").submit(); } }); + $( "#id_selectedKeys" ).chosen().change(function(event, params) { + // Tag selection or deselection made + if ((params.selected || params.deselected) && $('#enableCheckbox').is(':checked')) { + $("#tagSearchForm").submit(); + } + }); $('#enableCheckbox').change(function() { if ($(this).is(':checked')) { @@ -252,6 +270,15 @@

{% trans "Tag Search" %}

data-content="Remove from the results the objects annotated with excluded tags.
Only the tags of the selected group & user are listed."> +
+
+ {{ tagnav_form.selectedKeys.errors }} + + {{ tagnav_form.selectedKeys }} + + +


diff --git a/omero_tagsearch/views.py b/omero_tagsearch/views.py index d1762c8..4fc4021 100644 --- a/omero_tagsearch/views.py +++ b/omero_tagsearch/views.py @@ -188,26 +188,6 @@ def index(request, conn=None, **kwargs): service_opts = conn.SERVICE_OPTS.copy() service_opts.setOmeroGroup(active_group) - def get_tagsets(): - # Get tagsets for tag_ids - # Do not filter tagsets on user, as it's meant to be - # information added to tags - - params = Parameters() - hql = ( - """ - SELECT DISTINCT link.child.id, tagset.textValue - FROM Annotation tagset - JOIN tagset.annotationLinks link - WHERE tagset.class IS TagAnnotation - """ - ) - - return { - result[0].val: f" [{result[1].val}]" - for result in qs.projection(hql, params, service_opts) - } - def get_tags(obj, tagset_d): # Get tags # It is not sufficient to simply get the objects as there may be tags @@ -234,6 +214,49 @@ def get_tags(obj, tagset_d): for result in qs.projection(hql, params, service_opts) ] + def get_key_values(obj): + # Get keys + + params = Parameters() + hql = ( + """ + SELECT DISTINCT ann.id, map.name, map.value + FROM %sAnnotationLink link + JOIN link.child ann + JOIN ann.mapValue map + WHERE ann.class IS MapAnnotation + """ + % obj + ) + + if user_id != -1: + hql += " AND ann.details.owner.id = (:uid)" + params.map = {"uid": rlong(user_id)} + + return [ + (result[0].val, result[1].val, result[2].val) for result in qs.projection(hql, params, service_opts) + ] + + def get_tagsets(): + # Get tagsets for tag_ids + # Do not filter tagsets on user, as it's meant to be + # information added to tags + + params = Parameters() + hql = ( + """ + SELECT DISTINCT link.child.id, tagset.textValue + FROM Annotation tagset + JOIN tagset.annotationLinks link + WHERE tagset.class IS TagAnnotation + """ + ) + + return { + result[0].val: f" [{result[1].val}]" + for result in qs.projection(hql, params, service_opts) + } + tagset_d = defaultdict(str) tagset_d.update(get_tagsets()) @@ -246,12 +269,26 @@ def get_tags(obj, tagset_d): tags.update(get_tags("Screen", tagset_d)) tags.update(get_tags("Well", tagset_d)) + # List of tuples (name, value) + kvps = set(get_key_values("Image")) + kvps.update(get_key_values("Dataset")) + kvps.update(get_key_values("Project")) + kvps.update(get_key_values("Plate")) + kvps.update(get_key_values("PlateAcquisition")) + kvps.update(get_key_values("Screen")) + kvps.update(get_key_values("Well")) + # Convert back to an ordered list and sort tags = list(tags) tags.sort(key=lambda x: (x[2].lower(), x[1].lower())) tags = list(map(lambda t: (t[0], t[1] + t[2]), tags)) - form = TagSearchForm(tags, conn, use_required_attribute=False) + # Convert back to an ordered list and sort + kvps = list(kvps) + kvps.sort(key=lambda x: x[1].lower()) + keys = list(map(lambda t: (t[0], t[1]), kvps)) #[(x[0], x[1]) for x in kvps] + print(keys) + tag_form = TagSearchForm(tags, keys, conn, use_required_attribute=False) context = { "init": init, @@ -269,7 +306,7 @@ def get_tags(obj, tagset_d): context["isLeader"] = conn.isLeader() context["current_url"] = url context["template"] = template - context["tagnav_form"] = form + context["tagnav_form"] = tag_form context["user_name"] = user_name return context @@ -286,6 +323,8 @@ def tag_image_search(request, conn=None, **kwargs): selected_tags = [int(x) for x in request.GET.getlist("selectedTags")] excluded_tags = [int(x) for x in request.GET.getlist("excludedTags")] + selected_keys = [int(x) for x in request.GET.getlist("selectedKeys")] + operation = request.GET.get("operation") # validate experimenter is in the active group @@ -296,14 +335,20 @@ def tag_image_search(request, conn=None, **kwargs): service_opts = conn.SERVICE_OPTS.copy() service_opts.setOmeroGroup(active_group) - def get_annotated_obj(obj_type, in_ids, excl_ids): + def get_annotated_obj(obj_type, in_ids, excl_ids, key_ids): # Get the images that match params = Parameters() params.map = {} - params.map["in_ids"] = rlist([rlong(o) for o in set(in_ids)]) + ann_ids = in_ids + ann_ids.extend(key_ids) + params.map["in_ids"] = rlist([rlong(o) for o in set(ann_ids)]) hql = ("select link.parent.id from %sAnnotationLink link " - "where link.child.id in (:in_ids) " % (obj_type)) + "where link.child.id in (%s)" % (obj_type, ",".join([str(x) for x in ann_ids]))) + #if len(key_ids) > 0: + # params.map["key_ids"] = rlist([rlong(o) for o in set(key_ids)]) + # hql += " and link.child.id in (%s)" % ",".join([str(x) for x in key_ids]) + if len(excl_ids) > 0: params.map["ex_ids"] = rlist([rlong(o) for o in set(excl_ids)]) hql += (" and link.parent.id not in " @@ -312,7 +357,7 @@ def get_annotated_obj(obj_type, in_ids, excl_ids): hql += "group by link.parent.id" if operation == "AND": - hql += f" having count (distinct link.child) = {len(in_ids)}" + hql += f" having count (distinct link.child) = {len(ann_ids)}" qs = conn.getQueryService() return [x[0].getValue() for x in qs.projection(hql, @@ -326,33 +371,33 @@ def get_annotated_obj(obj_type, in_ids, excl_ids): manager = {"containers": {}} preview = False count_d = {} - if selected_tags: + if selected_tags or selected_keys: image_ids = get_annotated_obj("Image", selected_tags, - excluded_tags) + excluded_tags, selected_keys) count_d["image"] = len(image_ids) dataset_ids = get_annotated_obj("Dataset", selected_tags, - excluded_tags) + excluded_tags, selected_keys) count_d["dataset"] = len(dataset_ids) project_ids = get_annotated_obj("Project", selected_tags, - excluded_tags) + excluded_tags, selected_keys) count_d["project"] = len(project_ids) screen_ids = get_annotated_obj("Screen", selected_tags, - excluded_tags) + excluded_tags, selected_keys) count_d["screen"] = len(screen_ids) plate_ids = get_annotated_obj("Plate", selected_tags, - excluded_tags) + excluded_tags, selected_keys) count_d["plate"] = len(plate_ids) well_ids = get_annotated_obj("Well", selected_tags, - excluded_tags) + excluded_tags, selected_keys) count_d["well"] = len(well_ids) acquisition_ids = get_annotated_obj("PlateAcquisition", - selected_tags, excluded_tags) + selected_tags, excluded_tags, selected_keys) count_d["acquisition"] = len(acquisition_ids) if image_ids: From a16418b113288ad4af6ff45df0638279d471eab4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Dornier?= Date: Mon, 2 Sep 2024 11:03:32 +0200 Subject: [PATCH 2/6] Adding namespace filtering --- omero_tagsearch/forms.py | 4 +- .../tagsearch/css/webtagging_search.css | 29 +++++++++- .../templates/omero_tagsearch/tagnav.html | 40 +++++++++++--- omero_tagsearch/views.py | 55 +++++++++++++------ 4 files changed, 99 insertions(+), 29 deletions(-) diff --git a/omero_tagsearch/forms.py b/omero_tagsearch/forms.py index 422320f..bb92916 100644 --- a/omero_tagsearch/forms.py +++ b/omero_tagsearch/forms.py @@ -6,6 +6,7 @@ class TagSearchForm(Form): selectedTags = MultipleChoiceField() excludedTags = MultipleChoiceField() selectedKeys = MultipleChoiceField() + selectedNamespaces = MultipleChoiceField() operation = ChoiceField( widget=RadioSelect, @@ -20,11 +21,12 @@ class TagSearchForm(Form): view_plate = BooleanField(initial=True) view_screen = BooleanField(initial=True) - def __init__(self, tags, keys, conn=None, *args, **kwargs): + def __init__(self, tags, keys, namespaces, conn=None, *args, **kwargs): super(TagSearchForm, self).__init__(*args, **kwargs) # Process Tags into choices (lists of tuples) self.fields["selectedTags"].choices = tags self.fields["excludedTags"].choices = tags self.fields["selectedKeys"].choices = keys + self.fields["selectedNamespaces"].choices = namespaces self.conn = conn diff --git a/omero_tagsearch/static/tagsearch/css/webtagging_search.css b/omero_tagsearch/static/tagsearch/css/webtagging_search.css index d99e6a6..d0c4857 100644 --- a/omero_tagsearch/static/tagsearch/css/webtagging_search.css +++ b/omero_tagsearch/static/tagsearch/css/webtagging_search.css @@ -1,4 +1,4 @@ -.tagSearchDivider { +.tagSearchDivider, .keyValueSearchDivider { border-bottom:solid 1px hsl(210,20%,90%); -webkit-box-shadow:0 1px 0 rgba(255,255,255,.3); @@ -10,16 +10,27 @@ margin-bottom:25px; } -#id_selectedTags, #id_excludedTags, #id_selectedKeys { +#id_selectedTags{ width:200px; } +#id_excludedTags { + width:200px; +} + +#id_selectedKeys { + width:200px; +} + +#id_selectedNamespaces { + width:300px; +} + .tagSearchDivider .field { display: flex; flex-direction: row; align-items: center; margin-bottom: 1px; /* Adjust as needed */ - width:200px; } .tagSearchDivider label { @@ -27,6 +38,18 @@ margin-right: 10px; /* Adjust as needed */ } +.keyValueSearchDivider .field { + display: flex; + flex-direction: row; + align-items: center; + margin-bottom: 1px; /* Adjust as needed */ +} + +.keyValueSearchDivider label { + width:150px; + margin-right: 10px; /* Adjust as needed */ +} + .hidden { display: none !important; diff --git a/omero_tagsearch/templates/omero_tagsearch/tagnav.html b/omero_tagsearch/templates/omero_tagsearch/tagnav.html index 128b888..23dd208 100644 --- a/omero_tagsearch/templates/omero_tagsearch/tagnav.html +++ b/omero_tagsearch/templates/omero_tagsearch/tagnav.html @@ -54,6 +54,7 @@ $("#id_selectedTags").chosen({placeholder_text:'Choose tags'}); $("#id_excludedTags").chosen({placeholder_text:'Choose tags to exclude'}); $("#id_selectedKeys").chosen({placeholder_text:'Choose keys'}); + $("#id_selectedNamespaces").chosen({placeholder_text:'Choose ns'}); $(".searching_info").tooltip({ track: true, @@ -123,14 +124,17 @@ var selection_incl = $('#id_selectedTags').getSelectionOrder(); var selection_excl = $('#id_excludedTags').getSelectionOrder(); var selection_keys = $('#id_selectedKeys').getSelectionOrder(); + var selection_namespaces = $('#id_selectedNamespaces').getSelectionOrder(); // Update the chosen selector $("#id_selectedTags").trigger("chosen:updated"); $("#id_excludedTags").trigger("chosen:updated"); $("#id_selectedKeys").trigger("chosen:updated"); + $("#id_selectedNamespaces").trigger("chosen:updated"); // Restore the ordering $('#id_selectedTags').setSelectionOrder(selection_incl); $("#id_excludedTags").setSelectionOrder(selection_excl); $('#id_selectedKeys').setSelectionOrder(selection_keys); + $('#id_selectedNamespaces').setSelectionOrder(selection_namespaces); $('#id_search').quicksearch('table#dataTable tbody tr', { 'delay': 300, @@ -173,6 +177,12 @@ $("#tagSearchForm").submit(); } }); + $( "#id_selectedNamespaces" ).chosen().change(function(event, params) { + // Tag selection or deselection made + if ((params.selected || params.deselected) && $('#enableCheckbox').is(':checked')) { + $("#tagSearchForm").submit(); + } + }); $( "#id_selectedKeys" ).chosen().change(function(event, params) { // Tag selection or deselection made if ((params.selected || params.deselected) && $('#enableCheckbox').is(':checked')) { @@ -271,15 +281,6 @@

{% trans "Tag Search" %}


-
- {{ tagnav_form.selectedKeys.errors }} - - {{ tagnav_form.selectedKeys }} - - - -


{% trans "Tag Search" %}

+
+
+ {{ tagnav_form.selectedNamespaces.errors }} + + {{ tagnav_form.selectedNamespaces }} + + + +


+
+ {{ tagnav_form.selectedKeys.errors }} + + {{ tagnav_form.selectedKeys }} + + + +


+
+

Results