Skip to content

Commit

Permalink
(feat): Tags
Browse files Browse the repository at this point in the history
fix(tag): Create tags on mobile

chore(tags): Change TagName to Name

chore(tags): eslint

chore(tags): dbFetchAll to dbQuery for removetag

chore(events): eslint (attempt 2)

feat(tags): Better handling of keyboard

fix(tags): Enter key for creating new tag

fix(tags): Don't allow space as a tag name

feat(tags): Delete tag if last assignment removed

fix(tags): Increase height of dropdown

in progress

fix(Tags): Use T.Id on the events page dropdown

fix(Tags): Remove $availableTags from events.php

chore(sql): Formatting sql statements

feat(Tags): Working OR on filters and events pages

fix(filter): Populate availableTags

chore(Tags): code formatting

fix(tag): Add tag on create tag

Fix(tags): Remove tag from available if last

feat(tags): Add zm_update.sql

fix(chosen): Undo css width

fix(chosen): tags dropdown width

fix(tags): dropdown over timeline

fix(tags): Full width input

fix(events): Refresh table on page show

chore(filter): Clean up availableTags

chore(event): Clean up available & selected Tags

fix(event): Update available tags on remove

fix(event): Remove hack for selected tags

feat(tags): Blur input after adding tag

doc(tags): Initial tags documentation

fix(tags): Dark theme dropdown

fix(tags): Dark theme for tags on input

fix(tags): Dark theme for highlight in dropdown

fix(tags): Populate filter tags droplist

chore(): Bump zm_update to 1.37.42

chore(tags): Move mobile check to skin.js

chore(tags): Comment debug statements

fix(tags): Enter key to create tag on mobile Chome

chore(tags): Space in 'All Tags' for translation

Temporary commit to handle cookie expiration times

chore(tags): Remove unnecessary Tag(s) from en_gb

chore(): Cleanup unnecessary Error and Debug

chore(): Resolve merge conflicts

chore(): Address merge conflicts with master
  • Loading branch information
Simpler1 committed Aug 25, 2023
1 parent 79b7550 commit ee925f1
Show file tree
Hide file tree
Showing 27 changed files with 900 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
77 changes: 69 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,13 +205,46 @@ 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 ($filter->limit() and !count($filter->post_sql_conditions())) {
$sql .= ' LIMIT '.$filter->limit();
}
Expand Down Expand Up @@ -243,6 +284,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 +313,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 +363,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 +381,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 ee925f1

Please sign in to comment.