class options
A Panel-based UI class for managing slide release visibility in a study management system, allowing users to view and toggle the release status of slides at various hierarchical levels (Study, Group, Animal, Block, Slide).
/tf/active/vicechatdev/options.py
7 - 152
complex
Purpose
This class provides a comprehensive interface for managing slide visibility/release status in a Neo4j graph database. It allows users to search for and toggle the release status of slides at different organizational levels, with logging of all changes. The class integrates with Panel widgets to create an interactive UI for viewing hierarchical study data and controlling which slides are visible to customers. It handles complex visibility logic where parent nodes (Study, Group, etc.) show as locked if any child slides are locked.
Source Code
class options(param.Parameterized):
def __init__(self, graph, log, user):
self.graph = graph
self.log = log
self.user = user
self.search_results = pn.Column(scroll=True)
self.create_options_log()
self.build_body()
@property
def studies(self):
return self.graph.run(f"MATCH (s:Study) WHERE s.N =~ '\\\d{{2}}\\\w{{3}}' WITH s.N AS studies ORDER BY s.N RETURN COLLECT(studies)").evaluate()
@property
def file_handler(self):
"""
The logging file handler for system logs
"""
file_handler = logging.FileHandler(f"./logs/slide_release.log")
file_handler.setLevel(logging.INFO)
file_handler.setFormatter(self.formatter)
return file_handler
@property
def formatter(self):
"""
The logging formatter
"""
return logging.Formatter('[%(asctime)s: %(levelname)-8s] :: %(message)s')
@property
def tooltip_message(self):
return """
Release slides for customers. The 'View' option searches within the given study as follows:
<ul>
<li><b>Study:</b> Release all slides for a study</li>
<li><b>Group:</b> Release all slides for a specific group</li>
<li><b>Animal:</b> Release all slides for a specific animal</li>
<li><b>Block:</b> Release all slides for a specific block</li>
<li><b>Slide:</b> Release a specific slide.</li>
</ul>
If for example within a group 1 slide is visible and 2 are not, the entire group will be shown as 'locked'.<br>
Releasing the group will release all 3 slides.<br>
Use the Free search to for example find a specific slide<br><br>
Replaced slides are always hidden from the customer.
"""
def get_existing_logger(self, loggername):
"""
Checks if a logger already exists for the current user
Returns
-------
Either the matched logger, or Bool False if no matching logger was found
"""
for name in logging.root.manager.loggerDict:
if name == loggername:
return logging.getLogger(loggername)
return False
def create_options_log(self):
"""Create syslog logger"""
logger = self.get_existing_logger("SlideRelease")
if logger:
self.releaselog = logger
else:
self.releaselog = logging.getLogger("SlideRelease")
self.releaselog.setLevel(logging.INFO)
self.releaselog.addHandler(self.file_handler)
def _check_child_visibility(self, label, uid):
return all(self.graph.run(f"""
MATCH (:{label} {{UID:'{uid}'}})-[*]->(s:Slide) WHERE NOT toLower(s.N) CONTAINS 'replaced'
WITH s, CASE WHEN s IS NULL THEN [false] ELSE all(x in COLLECT(s) WHERE x.released IS NOT NULL AND x.released) END AS result
RETURN COLLECT(result)
""").evaluate())
def build_body(self):
mapper = {
'Animal':'ExpItem',
'Block':'Parblock',
}
study_select = pn.widgets.Select(name="Study", options=self.studies, stylesheets=['./assets/stylesheets/dropdown.css'], align='end', width=100)
view_select = pn.widgets.Select(name="View", options=['Study','Group','Animal','Block','Slide'], value='Study', stylesheets=['./assets/stylesheets/dropdown.css'], align='end', width=100)
search = pn.widgets.TextInput(name="Free search", stylesheets=['./assets/stylesheets/textinput.css'], width=200)
tooltip = pn.widgets.TooltipIcon(value=Tooltip(content=HTML(self.tooltip_message), position='bottom'))
@pn.depends(study_select.param.value, view_select.param.value, watch=True)
def _update_view(study, view):
self.search_results.clear()
label = mapper.get(view, view)
if label == 'Study':
label_collection = self.graph.run(f"MATCH (s:Study {{N:'{study}'}}) RETURN COLLECT(s)").evaluate()
else:
label_collection = self.graph.run(f"MATCH (:Study {{N:'{study}'}})-[*]->(o:{label}) WITH o AS result ORDER BY o.N RETURN COLLECT(result)").evaluate()
if len(label_collection) > 100:
pn.state.notifications.error(f"Too many items to display. Either use a different view, or contact your IT administrator with which slides to release/withhold")
return
for i in label_collection: #this loop can quickly get out of hand, put a limit on number of items
if label == 'Slide':
is_all_visible = i.get('released',False)
else:
is_all_visible = self._check_child_visibility(label, i['UID'])
row = self._create_tag_and_button(label, view, i['N'], i['UID'], is_all_visible)
self.search_results.append(row)
@pn.depends(search.param.value, watch=True)
def _update_view_from_search(search):
result = self.graph.run(f"MATCH (o) WHERE o.N = toUpper('{search}') RETURN o").evaluate()
if not result:
pn.state.notifications.error(f"No match found for {search}")
return
label = list(result.labels)[0]
if not label in ['Study','Group','ExpItem','Parblock','Slide']:
pn.state.notifications.error(f"Invalid object {label}. Please search for a Group, Animal, Block or Slide")
return
self.search_results.clear()
row = self._create_tag_and_button(label, mapper.get(label,label), result['N'], result['UID'], result.get('released',False))
self.search_results.append(row)
return
self.body = pn.Column(
pn.Row(study_select, view_select, search,tooltip),
pn.layout.Divider(margin=(5,5,20,5)),
self.search_results,
)
def _release_child_images(self, label, N, UID, is_visible):
self.releaselog.info(f"User {self.user.user} set {label} {N} visibility to {is_visible}")
if label == 'Slide':
self.graph.run(f"MATCH (s:Slide {{UID:'{UID}'}}) SET s.released = {is_visible}")
self.releaselog.info(f"{self.user.user} | {N} set to {is_visible}")
return
all_images = self.graph.run(f"MATCH (:{label} {{UID:'{UID}'}})-[*]->(s:Slide) WHERE NOT toLower(s.N) CONTAINS 'replaced' SET s.released = {is_visible} RETURN COLLECT(s.N)").evaluate()
for i in all_images:
self.releaselog.info(f"{self.user.user} | {i} set to {is_visible}")
def _create_tag_and_button(self, label, visual_label, N, UID, visible):
tag = pn.widgets.StaticText(name=visual_label, value=N, align='center')
button = pn.widgets.Toggle(name='Released' if visible else 'locked', value=visible, button_type = 'success' if visible else 'danger', min_width=130)
@pn.depends(button.param.value, watch=True)
def on_click(value):
pn.state.notifications.info("Updating slide release status, please wait.")
button.button_type = 'success' if value else 'danger'
button.name = 'Released' if value else 'Locked'
self._release_child_images(label, N, UID, value)
pn.state.notifications.success("Slide release status updated!")
return pn.WidgetBox(pn.Row(tag, button), stylesheets=['./assets/stylesheets/widgetbox.css'])
Parameters
| Name | Type | Default | Kind |
|---|---|---|---|
bases |
param.Parameterized | - |
Parameter Details
graph: A Neo4j graph database connection object (py2neo Graph instance) used to query and update slide and study data. Must support the .run() method for Cypher queries.
log: A logging object or logger instance for general application logging (though the class creates its own specialized logger for release operations).
user: A user object that must have a 'user' attribute containing the username string. Used for audit logging of who made visibility changes.
Return Value
Instantiation returns an options object with a fully configured Panel UI in the 'body' attribute. The body contains dropdown selectors, search input, and a scrollable results column. Methods return various types: properties return computed values (lists, loggers, strings), _check_child_visibility returns boolean, other methods perform side effects and return None.
Class Interface
Methods
__init__(self, graph, log, user)
Purpose: Initialize the options class with database connection, logging, and user context, then build the UI
Parameters:
graph: Neo4j graph database connection objectlog: General logging objectuser: User object with 'user' attribute containing username
Returns: None - initializes instance attributes and builds UI
@property studies(self) -> list
property
Purpose: Query and return a list of all study names from the database that match the pattern of 2 digits followed by 3 letters
Returns: List of study names (strings) sorted alphabetically
@property file_handler(self) -> logging.FileHandler
property
Purpose: Create and configure a file handler for logging slide release operations to './logs/slide_release.log'
Returns: Configured logging.FileHandler instance with INFO level and custom formatter
@property formatter(self) -> logging.Formatter
property
Purpose: Create a logging formatter with timestamp, log level, and message
Returns: logging.Formatter instance with format '[%(asctime)s: %(levelname)-8s] :: %(message)s'
@property tooltip_message(self) -> str
property
Purpose: Provide HTML-formatted help text explaining how the slide release interface works
Returns: Multi-line HTML string with usage instructions and feature descriptions
get_existing_logger(self, loggername: str) -> logging.Logger | bool
Purpose: Check if a logger with the given name already exists in the logging system
Parameters:
loggername: Name of the logger to search for
Returns: The existing Logger object if found, or False if not found
create_options_log(self) -> None
Purpose: Create or retrieve the 'SlideRelease' logger for audit logging of visibility changes
Returns: None - sets self.releaselog attribute to the logger instance
_check_child_visibility(self, label: str, uid: str) -> bool
Purpose: Check if all child slides of a given node are released (visible), excluding replaced slides
Parameters:
label: Node label type (Study, Group, ExpItem, Parblock, Slide)uid: Unique identifier of the node to check
Returns: True if all non-replaced child slides are released, False otherwise
build_body(self) -> None
Purpose: Construct the complete Panel UI with dropdowns, search input, and results display with reactive dependencies
Returns: None - sets self.body attribute to a Panel Column containing the full UI
_release_child_images(self, label: str, N: str, UID: str, is_visible: bool) -> None
Purpose: Update the release status of a node and all its child slides in the database, with audit logging
Parameters:
label: Node label type (Study, Group, ExpItem, Parblock, Slide)N: Name/identifier of the nodeUID: Unique identifier of the nodeis_visible: Boolean indicating whether to release (True) or lock (False) the slides
Returns: None - updates database and writes to log file
_create_tag_and_button(self, label: str, visual_label: str, N: str, UID: str, visible: bool) -> pn.WidgetBox
Purpose: Create a UI row with a label and toggle button for controlling slide visibility
Parameters:
label: Internal node label type for database operationsvisual_label: Display label shown in the UIN: Name/identifier of the nodeUID: Unique identifier of the nodevisible: Current visibility status (True=released, False=locked)
Returns: Panel WidgetBox containing a Row with StaticText label and Toggle button with reactive click handler
Attributes
| Name | Type | Description | Scope |
|---|---|---|---|
graph |
py2neo.Graph | Neo4j database connection for querying and updating slide data | instance |
log |
logging.Logger | General application logger (not actively used, releaselog is used instead) | instance |
user |
object | User object with 'user' attribute containing username for audit logging | instance |
search_results |
pn.Column | Scrollable Panel Column container that holds the search/view results with toggle buttons | instance |
releaselog |
logging.Logger | Dedicated logger for slide release operations, writes to './logs/slide_release.log' | instance |
body |
pn.Column | Main Panel UI component containing all widgets (dropdowns, search, results), created by build_body() | instance |
Dependencies
parampanelloggingbokehpy2neo
Required Imports
import param
import panel as pn
import logging
from bokeh.models import Tooltip
from bokeh.models.dom import HTML
Usage Example
# Assuming you have a Neo4j connection and user object
from py2neo import Graph
import panel as pn
# Setup
graph = Graph('bolt://localhost:7687', auth=('neo4j', 'password'))
class User:
def __init__(self, username):
self.user = username
user = User('john_doe')
log = logging.getLogger('app')
# Create options instance
options_panel = options(graph, log, user)
# Display the UI (in a Panel app)
pn.extension()
app = pn.template.FastListTemplate(
title='Slide Release Manager',
main=[options_panel.body]
)
app.servable()
# The UI will show:
# - Study dropdown populated from database
# - View selector (Study/Group/Animal/Block/Slide)
# - Free search text input
# - Results with toggle buttons to release/lock slides
Best Practices
- Ensure the Neo4j database connection is active before instantiating the class
- Create the './logs/' directory before instantiation to avoid file handler errors
- The class automatically creates a shared logger 'SlideRelease' - multiple instances will use the same logger
- Be cautious with large datasets - the class limits display to 100 items and shows an error notification if exceeded
- The visibility logic is hierarchical: releasing a parent (e.g., Group) releases all child slides that aren't marked as 'replaced'
- Slides with 'replaced' in their name (case-insensitive) are always excluded from visibility operations
- All visibility changes are logged with username and timestamp for audit purposes
- The UI uses reactive dependencies (@pn.depends) so changes to dropdowns automatically update the display
- Use the 'body' attribute to access the complete Panel UI component for embedding in applications
- The class maintains state through instance attributes (graph, log, user, search_results) - avoid modifying these directly
Tags
Similar Components
AI-powered semantic similarity - components with related functionality:
-
class DocumentShareManager 54.5% similar
-
class User 53.5% similar
-
class ControlledDocApp 50.5% similar
-
class DocumentAccessControls 49.8% similar
-
class SharePermissionIndicator 48.0% similar