Skip to content

Commit

Permalink
Merge pull request #3758 from Simpler1/tags
Browse files Browse the repository at this point in the history
(feat): Tags
  • Loading branch information
connortechnology authored Sep 14, 2023
2 parents d9601b1 + f8c89a0 commit f16df49
Show file tree
Hide file tree
Showing 27 changed files with 897 additions and 58 deletions.
47 changes: 47 additions & 0 deletions db/zm_update-1.37.44.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
--
-- This adds Tags
--

SELECT 'Checking For Tags Table';
SET @s = (SELECT IF(
(SELECT COUNT(*)
FROM INFORMATION_SCHEMA.TABLES
WHERE table_name = 'Tags'
AND table_schema = DATABASE()
) > 0,
"SELECT 'Tags table exists'",
"CREATE TABLE `Tags` (
`Id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`Name` varchar(64) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL DEFAULT '',
`CreateDate` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`CreatedBy` int(10) unsigned,
`LastAssignedDate` dateTime,
PRIMARY KEY (`Id`),
UNIQUE(`Name`)
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci"
));

PREPARE stmt FROM @s;
EXECUTE stmt;

SELECT 'Checking For Events_Tags Table';
SET @s = (SELECT IF(
(SELECT COUNT(*)
FROM INFORMATION_SCHEMA.TABLES
WHERE table_name = 'Events_Tags'
AND table_schema = DATABASE()
) > 0,
"SELECT 'Events_Tags table exists'",
"CREATE TABLE `Events_Tags` (
`TagId` bigint(20) unsigned NOT NULL,
`EventId` bigint(20) unsigned NOT NULL,
`AssignedDate` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`AssignedBy` int(10) unsigned,
PRIMARY KEY (`TagId`, `EventId`),
CONSTRAINT `Events_Tags_ibfk_1` FOREIGN KEY (`TagId`) REFERENCES `Tags` (`Id`) ON DELETE CASCADE,
CONSTRAINT `Events_Tags_ibfk_2` FOREIGN KEY (`EventId`) REFERENCES `Events` (`Id`) ON DELETE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci"
));

PREPARE stmt FROM @s;
EXECUTE stmt;
1 change: 1 addition & 0 deletions docs/userguide/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ User Guide
viewmonitors
filterevents
viewevents
tags
options
cameracontrol
mobile
Expand Down
38 changes: 38 additions & 0 deletions docs/userguide/tags.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
Tags
====

Tags are a simple quick way to categorize events so that you can identify them easier.


Creating New Tags
-----------------
Creating new tags is as easy as typing a word in the tags field (located just above the video). Pressing the space bar, comma, or Enter will create the new tag and add it to the event.


Adding Existing Tags to an Event
--------------------------------
Clicking in the tags field will show a dropdown list of all of the available tags in descending order of when they were last added to an event.

An existing tag can be added to the event by clicking it from the dropdown or by using the down/up arrow keys to highlight the desired tag and pressing Enter.

<Ctrl-Down Arrow> will add the tag most recently added to any event to the current event.

Typing in the tags field will filter the available tags to the ones that contain the text typed.

.. note::
Since you can use the right/left arrows to move between events when the tags field doesn't have focus, you can quickly add the most recent tag with <Ctrl-Down Arrow> and then move to the next event with Right Arrow. You can also use the Down Arrow to bring up the available tags to add a different tag before pressing the Right Arrow to move to the next event.


Removing Tags from an Event
---------------------------
Pressing the "x" to the right of a tag will remove it from the event. When the tag is removed from the last event, the tag will be deleted from the available tags.


Filtering with Tags
===================
Current Limitations
-------------------
1. Filtering for multiple tags is an OR search (Goal is to make this an AND search)
2. Resulting events only display the tags that were searched (Goal is to display all of the tags on the resulting events)
3. There is no way to search for events that don't have any tag (Goal is to provide search criteria for events with no tag)
4. There is no way to search for events with ONLY the specified tag or tags (Goal is to provide search criteria to search for events with ONLY the specified tag or tags)
21 changes: 19 additions & 2 deletions scripts/ZoneMinder/lib/ZoneMinder/Filter.pm
Original file line number Diff line number Diff line change
Expand Up @@ -143,8 +143,21 @@ sub Sql {
}

my $filter_expr = ZoneMinder::General::jsonDecode($self->{Query_json});
my $sql = 'SELECT E.*, unix_timestamp(E.StartDateTime) as Time
FROM Events as E';
my $sql = '
SELECT
E.*,
unix_timestamp(E.StartDateTime)
AS Time,
GROUP_CONCAT(T.Name SEPARATOR ", ")
FROM Events
AS E
LEFT JOIN Events_Tags
AS ET
ON E.Id = ET.EventId
LEFT JOIN Tags
AS T
ON T.Id = ET.TagId
';

if ( $filter_expr->{terms} ) {
foreach my $term ( @{$filter_expr->{terms}} ) {
Expand All @@ -164,6 +177,8 @@ sub Sql {

if ( $term->{attr} eq 'AlarmedZoneId' ) {
$term->{op} = 'EXISTS';
} elsif ( $term->{attr} eq 'Tags' ) {
$self->{Sql} .= 'T.Name';
} elsif ( $term->{attr} =~ /^Monitor/ ) {
$sql = 'SELECT E.*, unix_timestamp(E.StartDateTime) as Time, M.Name as MonitorName
FROM Events as E INNER JOIN Monitors as M on M.Id = E.MonitorId';
Expand Down Expand Up @@ -368,6 +383,8 @@ sub Sql {
my $sort_column = '';
if ( $filter_expr->{sort_field} eq 'Id' ) {
$sort_column = 'E.Id';
} elsif ( $filter_expr->{sort_field} eq 'Tag' ) {
$sort_column = 'T.Name';
} elsif ( $filter_expr->{sort_field} eq 'MonitorName' ) {
$sql = 'SELECT E.*, unix_timestamp(E.StartDateTime) as Time, M.Name as MonitorName
FROM Events as E INNER JOIN Monitors as M on M.Id = E.MonitorId';
Expand Down
60 changes: 59 additions & 1 deletion web/ajax/event.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,26 @@
} elseif ( empty($_REQUEST['scale']) ) {
ajaxError('Video Generation Failure, no scale given');
} else {
$sql = 'SELECT E.*,M.Name AS MonitorName,M.DefaultRate,M.DefaultScale FROM Events AS E INNER JOIN Monitors AS M ON E.MonitorId = M.Id WHERE E.Id = ?'.monitorLimitSql();
$sql = '
SELECT
E.*,
M.Name
AS MonitorName,M.DefaultRate,M.DefaultScale,
GROUP_CONCAT(T.Name SEPARATOR ", ")
AS Tags
FROM Events
AS E
INNER JOIN Monitors
AS M
ON E.MonitorId = M.Id
LEFT JOIN Events_Tags
AS ET
ON E.Id = ET.EventId
LEFT JOIN Tags
AS T
ON T.Id = ET.TagId
WHERE
E.Id = ?'.monitorLimitSql();
if ( !($event = dbFetchOne($sql, NULL, array( $_REQUEST['id']))) ) {
ajaxError('Video Generation Failure, Unable to load event');
} else {
Expand Down Expand Up @@ -167,6 +186,45 @@
ajaxResponse(array('refreshEvent'=>false, 'refreshParent'=>true));
}
break;
case 'getselectedtags' :
$sql = '
SELECT
T.*
FROM Tags
AS T
INNER JOIN Events_Tags
AS ET
ON ET.TagId = T.Id
WHERE ET.EventId = ?
';
$values = array($_REQUEST['id']);
$response = dbFetchAll($sql, NULL, $values);
ajaxResponse(array('response'=>$response));
break;
case 'addtag' :
$sql = 'INSERT INTO Events_Tags (TagId, EventId, AssignedBy) VALUES (?, ?, ?)';
$values = array($_REQUEST['tid'], $_REQUEST['id'], $user->Id());
$response = dbFetchAll($sql, NULL, $values);

$sql = 'UPDATE Tags SET LastAssignedDate = NOW() WHERE Id = ?';
$values = array($_REQUEST['tid']);
dbFetchAll($sql, NULL, $values);

ajaxResponse(array('response'=>$response));
break;
case 'removetag' :
$tagId = $_REQUEST['tid'];
dbQuery('DELETE FROM Events_Tags WHERE TagId = ? AND EventId = ?', array($tagId, $_REQUEST['id']));
$sql = "SELECT * FROM Events_Tags WHERE TagId = $tagId";
$rowCount = dbNumRows($sql);
if ($rowCount < 1) {
$sql = 'DELETE FROM Tags WHERE Id = ?';
$values = array($_REQUEST['tid']);
$response = dbNumRows($sql, $values);
ajaxResponse(array('response'=>$response));
}
ajaxResponse();
break;
} // end switch action
} // end if canEdit('Events')

Expand Down
79 changes: 71 additions & 8 deletions web/ajax/events.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@
$filter = isset($_REQUEST['filter']) ? ZM\Filter::parse($_REQUEST['filter']) : new ZM\Filter();
if (count( $user->unviewableMonitorIds())) {
$filter = $filter->addTerm(array('cnj'=>'and', 'attr'=>'MonitorId', 'op'=>'IN', 'val'=>$user->viewableMonitorIds()));
// $filter = $filter->addTerm(array('cnj'=>'and', 'attr'=>'MonitorId', 'op'=>'IN', 'val'=>'5'));
}
// TODO: Why is $user->viewableMonitorIds() returning $user->unviewableMonitorIds()
// Error('$user->viewableMonitorIds(): '.print_r($user->viewableMonitorIds()));
if (!empty($_REQUEST['StartDateTime'])) {
$filter->addTerm(array('cnj'=>'and', 'attr'=>'StartDateTime', 'op'=> '>=', 'val'=>$_REQUEST['StartDateTime']));
}
Expand All @@ -42,6 +45,9 @@
if (!empty($_REQUEST['MonitorId'])) {
$filter->addTerm(array('cnj'=>'and', 'attr'=>'MonitorId', 'op'=> '=', 'val'=>$_REQUEST['MonitorId']));
}
if (!empty($_REQUEST['Tag'])) {
$filter->addTerm(array('cnj'=>'and', 'attr'=>'Tag', 'op'=>'=', 'val'=>''));
}

// Search contains a user entered string to search on
$search = isset($_REQUEST['search']) ? $_REQUEST['search'] : '';
Expand Down Expand Up @@ -176,12 +182,14 @@ function queryRequest($filter, $search, $advsearch, $sort, $offset, $order, $lim
$columns = array('Id', 'MonitorId', 'StorageId', 'Name', 'Cause', 'StartDateTime', 'EndDateTime', 'Length', 'Frames', 'AlarmFrames', 'TotScore', 'AvgScore', 'MaxScore', 'Archived', 'Emailed', 'Notes', 'DiskSpace');

// The names of columns shown in the event view that are NOT dB columns in the database
$col_alt = array('Monitor', 'Storage');
$col_alt = array('Monitor', 'Tags', 'Storage');

if ( $sort != '' ) {
if (!in_array($sort, array_merge($columns, $col_alt))) {
ZM\Error('Invalid sort field: ' . $sort);
$sort = '';
} else if ( $sort == 'Tags' ) {
$sort = 'T.Name';
} else if ( $sort == 'Monitor' ) {
$sort = 'M.Name';
} else if ($sort == 'EndDateTime') {
Expand All @@ -197,14 +205,49 @@ function queryRequest($filter, $search, $advsearch, $sort, $offset, $order, $lim

$values = array();
$likes = array();
// Error($filter->sql());
$where = $filter->sql()?' WHERE ('.$filter->sql().')' : '';

$col_str = 'E.*, UNIX_TIMESTAMP(E.StartDateTime) AS StartTimeSecs,
CASE WHEN E.EndDateTime IS NULL THEN (SELECT NOW()) ELSE E.EndDateTime END AS EndDateTime,
CASE WHEN E.EndDateTime IS NULL THEN (SELECT UNIX_TIMESTAMP(NOW())) ELSE UNIX_TIMESTAMP(EndDateTime) END AS EndTimeSecs,
M.Name AS Monitor';
$sql = 'SELECT ' .$col_str. ' FROM `Events` AS E INNER JOIN Monitors AS M ON E.MonitorId = M.Id'.$where.($sort?' ORDER BY '.$sort.' '.$order:'');

$col_str = '
E.*,
UNIX_TIMESTAMP(E.StartDateTime)
AS StartTimeSecs,
CASE WHEN E.EndDateTime
IS NULL
THEN (SELECT NOW())
ELSE E.EndDateTime END
AS EndDateTime,
CASE WHEN E.EndDateTime
IS NULL
THEN (SELECT UNIX_TIMESTAMP(NOW()))
ELSE UNIX_TIMESTAMP(EndDateTime) END
AS EndTimeSecs,
M.Name
AS Monitor,
GROUP_CONCAT(T.Name SEPARATOR ", ")
AS Tags';

$sql = '
SELECT
' .$col_str. '
FROM `Events`
AS E
INNER JOIN Monitors
AS M
ON E.MonitorId = M.Id
LEFT JOIN Events_Tags
AS ET
ON E.Id = ET.EventId
LEFT JOIN Tags
AS T
ON T.Id = ET.TagId
'.$where.'
GROUP BY E.Id
'.($sort?' ORDER BY '.$sort.' '.$order:'');

if ((int)$filter->limit() and !count($filter->post_sql_conditions())) {

$sql .= ' LIMIT '.$filter->limit();
}

Expand Down Expand Up @@ -243,6 +286,8 @@ function queryRequest($filter, $search, $advsearch, $sort, $offset, $order, $lim

$filtered_rows = null;

ZM\Debug('$advsearch: ' . $advsearch );
ZM\Debug('$search: ' . $search);
if (count($advsearch) or $search != '') {
$search_filter = new ZM\Filter();
$search_filter = $search_filter->addTerm(array('cnj'=>'and', 'attr'=>'Id', 'op'=>'IN', 'val'=>$event_ids));
Expand Down Expand Up @@ -270,7 +315,24 @@ function queryRequest($filter, $search, $advsearch, $sort, $offset, $order, $lim
$search_filter = $search_filter->addTerms($terms, array('obr'=>1, 'cbr'=>1, 'op'=>'OR'));
} # end if search

$sql = 'SELECT ' .$col_str. ' FROM `Events` AS E INNER JOIN Monitors AS M ON E.MonitorId = M.Id WHERE '.$search_filter->sql().' ORDER BY ' .$sort. ' ' .$order;
$sql = 'SELECT ' .$col_str. '
FROM `Events`
AS E
INNER JOIN Monitors
AS M
ON E.MonitorId = M.Id
LEFT JOIN Events_Tags
AS ET
ON E.Id = ET.EventId
LEFT JOIN Tags
AS T
ON T.Id = ET.TagId
WHERE
'.$search_filter->sql().'
ORDER BY
' .$sort. '
' .$order;

$filtered_rows = dbFetchAll($sql);
ZM\Debug('Have ' . count($filtered_rows) . ' events matching search filter: '.$sql);
} else {
Expand Down Expand Up @@ -303,6 +365,7 @@ function queryRequest($filter, $search, $advsearch, $sort, $offset, $order, $lim
$row['Archived'] = $row['Archived'] ? translate('Yes') : translate('No');
$row['Emailed'] = $row['Emailed'] ? translate('Yes') : translate('No');
$row['Cause'] = validHtmlStr($row['Cause']);
$row['Tags'] = validHtmlStr($row['Tags']);
$row['StartDateTime'] = $dateTimeFormatter->format(strtotime($row['StartDateTime']));
$row['EndDateTime'] = $row['EndDateTime'] ? $dateTimeFormatter->format(strtotime($row['EndDateTime'])) : null;
$row['Storage'] = ( $row['StorageId'] and isset($StorageById[$row['StorageId']]) ) ? $StorageById[$row['StorageId']]->Name() : 'Default';
Expand All @@ -320,7 +383,7 @@ function queryRequest($filter, $search, $advsearch, $sort, $offset, $order, $lim
} else {
$data['total'] = $data['totalNotFiltered'];
}
ZM\Debug("Done");
ZM\Debug("Done");
return $data;
}
?>
Loading

0 comments on commit f16df49

Please sign in to comment.