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 automatic propagation to child elements. The class integrates logging for audit trails, provides a Panel-based UI with dropdowns and search functionality, and manages the visibility state of slides that can be shown to customers.
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
log: A logging object for general application logging (though the class creates its own specialized logger)
user: A user object containing user information, must have a 'user' attribute for logging purposes
Return Value
Instantiation returns an options object with a 'body' attribute containing a Panel Column layout that can be displayed in a Panel application. Methods return various types: properties return data from database queries or logging objects, _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: Retrieve 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 to slide_release.log
Returns: Configured logging.FileHandler object writing to ./logs/slide_release.log
@property formatter(self) -> logging.Formatter
property
Purpose: Create a logging formatter with timestamp, log level, and message
Returns: logging.Formatter object with format '[timestamp: level] :: message'
@property tooltip_message(self) -> str
property
Purpose: Provide HTML-formatted help text explaining how the slide release interface works
Returns: HTML string with detailed usage instructions
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, False otherwise
create_options_log(self) -> None
Purpose: Create or retrieve the 'SlideRelease' logger for audit logging
Returns: None - sets self.releaselog attribute
_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: Neo4j node label (Study, Group, ExpItem, Parblock, Slide)uid: Unique identifier of the node to check
Returns: True if all child slides are released, False otherwise
build_body(self) -> None
Purpose: Construct the main UI layout with dropdowns, search field, and results panel with reactive dependencies
Returns: None - sets self.body attribute with Panel Column layout
_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: Neo4j node labelN: Node name for loggingUID: Unique identifier of the nodeis_visible: Boolean indicating whether to release (True) or lock (False) slides
Returns: None - updates database and writes to log
_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: Neo4j node label for database operationsvisual_label: Display label for the UIN: Node name to displayUID: Unique identifier for database operationsvisible: Current visibility status
Returns: Panel WidgetBox containing a row with StaticText and Toggle button
Attributes
| Name | Type | Description | Scope |
|---|---|---|---|
graph |
py2neo.Graph | Neo4j database connection for querying and updating slide data | instance |
log |
logging.Logger | General application logger passed during initialization | instance |
user |
object | User object containing username for audit logging | instance |
search_results |
pn.Column | Scrollable Panel Column container for displaying search results and toggle buttons | instance |
releaselog |
logging.Logger | Dedicated logger for slide release audit trail, writes to slide_release.log | instance |
body |
pn.Column | Main UI layout containing dropdowns, search field, and results panel | 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
from py2neo import Graph
import panel as pn
# Setup
graph = Graph('bolt://localhost:7687', auth=('neo4j', 'password'))
log = logging.getLogger('app')
user = type('User', (), {'user': 'john_doe'})()
# Instantiate the options class
options_panel = options(graph, log, user)
# Display the UI in a Panel application
pn.extension()
app = pn.template.FastListTemplate(
title='Slide Release Manager',
main=[options_panel.body]
)
app.servable()
# The UI will automatically handle:
# - Study selection from dropdown
# - View level selection (Study/Group/Animal/Block/Slide)
# - Free text search
# - Toggle buttons for releasing/locking slides
# - Logging all changes to ./logs/slide_release.log
Best Practices
- Ensure Neo4j database connection is active before instantiation
- Create logs directory before instantiation to avoid file handler errors
- The class automatically creates a singleton logger 'SlideRelease' - multiple instances will share the same logger
- The search results are limited to 100 items to prevent UI performance issues
- Replaced slides (containing 'replaced' in name) are automatically excluded from visibility operations
- The class uses reactive Panel dependencies - UI updates are automatic when dropdowns or search fields change
- All visibility changes are logged with username and timestamp for audit purposes
- The body attribute must be accessed after instantiation to get the UI component
- State management is handled through Panel's reactive framework - no manual state updates needed
- The class expects specific Neo4j node labels and relationships - ensure database schema matches
Tags
Similar Components
AI-powered semantic similarity - components with related functionality:
-
class User 55.3% similar
-
class ControlledDocApp 51.6% similar
-
function controlled_docs_navigation 46.8% similar
-
class CDocsApp 46.5% similar
-
class ObjectSharingInformation 41.6% similar