View Issue Details

IDProjectCategoryView StatusLast Update
0009716mantisbttaggingpublic2019-06-26 22:46
Reporterthosjo Assigned To 
PrioritynormalSeverityfeatureReproducibilityalways
Status acknowledgedResolutionopen 
Product Version1.1.2 
Summary0009716: Seperation of tags between projects
Description

In 1.1.2 the created tags are shared among all projects, even those the developer isn't a member of.

Would like to have the option to keep tags locked to a project so a developer working on X isn't allowed to view the tags assigned to project Y.

TagsNo tags attached.
Attached Files
projectSpecificTagsForMaster-1.2.x.patch (22,999 bytes)   
From 2e2335841909ef0f25c2616f8b34ed152d726a8e Mon Sep 17 00:00:00 2001
From: Dominik Blunk <dominik@blunk.ch>
Date: Mon, 12 Sep 2011 15:49:06 +0200
Subject: [PATCH] Implemented project specific tags

@see http://www.mantisbt.org/bugs/view.php?id=9716

To enable this feature run a Mantis update (new column "project_id" in tag table is required)
and set the config value $g_tag_project_specific_enabled to ON (defaults to OFF)

If project specific tags are enabled the following happens:

a) All existing tags will be considered as "global" (tags are saved with project_id = 0).
   Every "global" tag may be used in all projects.

b) Newly created tags are related to the current project (project of the bug precisely).
   These tags are only available within the same project scope.

c) No special handling of "parent projects" - a parent project is equal to a child project.
   Either tags for the parent project exists or not - no usage of "collected child project tags"

d) The tag - project relation may be edited in the tag management section. The tag overview
   shows only "global" tags and tags for the current project.

e) The filter shows only "global" tags and tags for the current project. Therefore filtering
   issues for specific tag(s) over multiple projects works only with "global" tags.

f) When switching $g_tag_project_specific_enabled between ON and OFF the existing tags will keep
   its project relation. New tags will always be "global" for "all projects". This makes sure that
   existing "project specific" tags are not unintentionally "globally" available

Other improvements:

- Added auth_reauthenticate() to the manage_tags_page.php
- Added the print_manage_menu() to tag_view_page.php and tag_update_page.php to improve
  usability (navigation back to tags overview after viewing/editing a tag)
---
 admin/schema.php                    |    4 ++
 bug_actiongroup_attach_tags_inc.php |    3 +-
 config_defaults_inc.php             |    7 +++
 core/tag_api.php                    |   97 ++++++++++++++++++++++++-----------
 manage_tags_page.php                |   27 ++++++++--
 tag_attach.php                      |    6 ++-
 tag_create.php                      |    9 +++-
 tag_update.php                      |    8 +++-
 tag_update_page.php                 |   24 +++++++--
 tag_view_page.php                   |   22 +++++---
 10 files changed, 150 insertions(+), 57 deletions(-)

diff --git a/admin/schema.php b/admin/schema.php
index d2b6940..505dd9b 100644
--- a/admin/schema.php
+++ b/admin/schema.php
@@ -608,3 +608,7 @@ $upgrade[] = Array( 'CreateIndexSQL', Array( 'idx_tag_name', db_get_table( 'mant
 $upgrade[] = Array( 'CreateIndexSQL', Array( 'idx_bug_tag_tag_id', db_get_table( 'mantis_bug_tag_table' ), 'tag_id' ) );
 $upgrade[] = Array( 'CreateIndexSQL', Array( 'idx_email_id', db_get_table( 'mantis_email_table' ), 'email_id', array( 'DROP' ) ), Array( 'db_index_exists', Array( db_get_table( 'mantis_email_table' ), 'idx_email_id') ) );
 $upgrade[] = Array( 'UpdateFunction', 'correct_multiselect_custom_fields_db_format' );
+
+/* 190 */
+$upgrade[] = Array( 'AddColumnSQL', Array( db_get_table( 'mantis_tag_table' ), "project_id I UNSIGNED NOTNULL DEFAULT '0'" ) );
+$upgrade[] = Array( 'CreateIndexSQL', Array( 'idx_project_id', db_get_table( 'mantis_tag_table' ), 'project_id' ) );
diff --git a/bug_actiongroup_attach_tags_inc.php b/bug_actiongroup_attach_tags_inc.php
index 3105acf..fca39b6 100644
--- a/bug_actiongroup_attach_tags_inc.php
+++ b/bug_actiongroup_attach_tags_inc.php
@@ -108,9 +108,10 @@
 		global $g_action_attach_tags_attach, $g_action_attach_tags_create;
 
 		$t_user_id = auth_get_current_user_id();
+		$t_bug = bug_get( $p_bug_id );
 
 		foreach( $g_action_attach_tags_create as $t_tag_row ) {
-			$t_tag_row['id'] = tag_create( $t_tag_row['name'], $t_user_id );
+			$t_tag_row['id'] = tag_create( $t_tag_row['name'], $t_user_id, '', $t_bug->project_id );
 			$g_action_attach_tags_attach[] = $t_tag_row;
 		}
 		$g_action_attach_tags_create = array();
diff --git a/config_defaults_inc.php b/config_defaults_inc.php
index 59b4764..2261a05 100644
--- a/config_defaults_inc.php
+++ b/config_defaults_inc.php
@@ -3506,6 +3506,13 @@
 	/***************
 	 * Bug Tagging *
 	 ***************/
+	
+	/**
+	 * When enabled tags will be project specific unless explicitely set to "global"
+	 * in the manage tags section
+	 * @global int $g_tag_project_specific_enabled
+	 */
+	$g_tag_project_specific_enabled = OFF;
 
 	/**
 	 * String that will separate tags as entered for input
diff --git a/core/tag_api.php b/core/tag_api.php
index fd31424..6a1b224 100644
--- a/core/tag_api.php
+++ b/core/tag_api.php
@@ -65,14 +65,15 @@ function tag_ensure_exists( $p_tag_id ) {
  * Determine if a given name is unique (not already used).
  * Uses a case-insensitive search of the database for existing tags with the same name.
  * @param string Tag name
+ * @param integer Project ID
  * @return boolean True if name is unique
  */
-function tag_is_unique( $p_name ) {
+function tag_is_unique( $p_name, $p_project_id = 0 ) {
 	$c_name = trim( $p_name );
 	$t_tag_table = db_get_table( 'mantis_tag_table' );
 
-	$query = 'SELECT id FROM ' . $t_tag_table . ' WHERE ' . db_helper_like( 'name' );
-	$result = db_query_bound( $query, Array( $c_name ) );
+	$query = 'SELECT id FROM ' . $t_tag_table . ' WHERE ' . db_helper_like( 'name' ) . " AND project_id = " . db_param();
+	$result = db_query_bound( $query, Array( $c_name, $p_project_id ) );
 
 	return db_num_rows( $result ) == 0;
 }
@@ -80,9 +81,10 @@ function tag_is_unique( $p_name ) {
 /**
  * Ensure that a name is unique.
  * @param string Tag name
+ * @param integer Project ID
  */
-function tag_ensure_unique( $p_name ) {
-	if( !tag_is_unique( $p_name ) ) {
+function tag_ensure_unique( $p_name, $p_project_id = 0 ) {
+	if( !tag_is_unique( $p_name, $p_project_id ) ) {
 		trigger_error( ERROR_TAG_DUPLICATE, ERROR );
 	}
 }
@@ -136,9 +138,10 @@ function tag_cmp_name( $p_tag1, $p_tag2 ) {
  * id = -1 and the tag name, and if the name is invalid, a row is returned with
  * id = -2 and the tag name.  The resulting array is then sorted by tag name.
  * @param string Input string to parse
+ * @param int Project ID
  * @return array Rows of tags parsed from input string
  */
-function tag_parse_string( $p_string ) {
+function tag_parse_string( $p_string, $p_project_id = 0 ) {
 	$t_tags = array();
 
 	$t_strings = explode( config_get( 'tag_separator' ), $p_string );
@@ -149,19 +152,27 @@ function tag_parse_string( $p_string ) {
 		}
 
 		$t_matches = array();
-		$t_tag_row = tag_get_by_name( $t_name );
+		$t_tag_row = tag_get_by_name( $t_name, $p_project_id );
 		if( $t_tag_row !== false ) {
 			$t_tags[] = $t_tag_row;
 		} else {
-			if( tag_name_is_valid( $t_name, $t_matches ) ) {
-				$t_id = -1;
+			if ( config_get( 'tag_project_specific_enabled' ) && $p_project_id > 0 ) {
+				// check if "global" tag exists
+				$t_tag_row = tag_get_by_name( $t_name, 0 );
+			}
+			if( $t_tag_row !== false ) {
+				$t_tags[] = $t_tag_row;
 			} else {
-				$t_id = -2;
+				if( tag_name_is_valid( $t_name, $t_matches ) ) {
+					$t_id = -1;
+				} else {
+					$t_id = -2;
+				}
+				$t_tags[] = array(
+					'id' => $t_id,
+					'name' => $t_name,
+				);
 			}
-			$t_tags[] = array(
-				'id' => $t_id,
-				'name' => $t_name,
-			);
 		}
 	}
 	usort( $t_tags, 'tag_cmp_name' );
@@ -189,6 +200,10 @@ function tag_parse_filters( $p_string ) {
 
 		if( !is_blank( $t_name ) && tag_name_is_valid( $t_name, $t_matches, $t_prefix ) ) {
 			$t_tag_row = tag_get_by_name( $t_matches[1] );
+			if( $t_tag_row === false && config_get( 'tag_project_specific_enabled' ) ) {
+				// no "global" tag found -> check project specific tag
+				$t_tag_row = tag_get_by_name( $t_matches[1], helper_get_current_project() );
+			}
 			if( $t_tag_row !== false ) {
 				$t_filter = utf8_substr( $t_name, 0, 1 );
 
@@ -238,14 +253,17 @@ function tag_get( $p_tag_id ) {
 /**
  * Return a tag row for the given name.
  * @param string Tag name
+ * @param int Project ID
  * @return Tag row
  */
-function tag_get_by_name( $p_name ) {
+function tag_get_by_name( $p_name, $p_project_id = 0 ) {
 	$t_tag_table = db_get_table( 'mantis_tag_table' );
+	$t_params = array( $p_name, $p_project_id );
 
 	$query = "SELECT * FROM $t_tag_table
-					WHERE " . db_helper_like( 'name' );
-	$result = db_query_bound( $query, Array( $p_name ) );
+					WHERE " . db_helper_like( 'name' ) ."
+					and project_id = " . db_param();
+	$result = db_query_bound( $query, $t_params );
 
 	if( 0 == db_num_rows( $result ) ) {
 		return false;
@@ -279,13 +297,14 @@ function tag_get_field( $p_tag_id, $p_field_name ) {
  * @param string Tag name
  * @param integer User ID
  * @param string Description
+ * @param integer Project ID
  * @return integer Tag ID
  */
-function tag_create( $p_name, $p_user_id = null, $p_description = '' ) {
+function tag_create( $p_name, $p_user_id = null, $p_description = '', $p_project_id = 0 ) {
 	access_ensure_global_level( config_get( 'tag_create_threshold' ) );
 
 	tag_ensure_name_is_valid( $p_name );
-	tag_ensure_unique( $p_name );
+	tag_ensure_unique( $p_name, $p_project_id );
 
 	if( null == $p_user_id ) {
 		$p_used_id = auth_get_current_user_id();
@@ -294,12 +313,17 @@ function tag_create( $p_name, $p_user_id = null, $p_description = '' ) {
 	}
 
 	$c_user_id = db_prepare_int( $p_user_id );
+	$c_project_id = db_prepare_int( $p_project_id );
+	if ( !config_get( 'tag_project_specific_enabled' ) ) {
+		$c_project_id = 0;
+	}
 	$c_date_created = db_now();
 
 	$t_tag_table = db_get_table( 'mantis_tag_table' );
 
 	$query = "INSERT INTO $t_tag_table
 				( user_id,
+				  project_id,
 				  name,
 				  description,
 				  date_created,
@@ -310,10 +334,11 @@ function tag_create( $p_name, $p_user_id = null, $p_description = '' ) {
 				  " . db_param() . ",
 				  " . db_param() . ",
 				  " . db_param() . ",
+				  " . db_param() . ",
 				  " . db_param() . "
 				)";
 
-	db_query_bound( $query, Array( $c_user_id, trim( $p_name ), trim( $p_description ), $c_date_created, $c_date_created ) );
+	db_query_bound( $query, Array( $c_user_id, $c_project_id, trim( $p_name ), trim( $p_description ), $c_date_created, $c_date_created ) );
 	return db_insert_id( $t_tag_table );
 }
 
@@ -322,9 +347,10 @@ function tag_create( $p_name, $p_user_id = null, $p_description = '' ) {
  * @param integer Tag ID
  * @param string Tag name
  * @param integer User ID
+ * @param integer Project ID
  * @param string Description
  */
-function tag_update( $p_tag_id, $p_name, $p_user_id, $p_description ) {
+function tag_update( $p_tag_id, $p_name, $p_user_id, $p_description, $p_project_id = 0 ) {
 	user_ensure_exists( $p_user_id );
 
 	if( auth_get_current_user_id() == tag_get_field( $p_tag_id, 'user_id' ) ) {
@@ -352,11 +378,19 @@ function tag_update( $p_tag_id, $p_name, $p_user_id, $p_description ) {
 
 	$query = "UPDATE $t_tag_table
 					SET user_id=" . db_param() . ",
-						name=" . db_param() . ",
-						description=" . db_param() . ",
+						name=" . db_param() . ",";
+	if ( $p_project_id >= 0 ) {
+		$query .= " 	project_id=" . db_param() . ",";
+	}
+	$query .= "			description=" . db_param() . ",
 						date_updated=" . db_param() . "
 					WHERE id=" . db_param();
-	db_query_bound( $query, Array( (int)$p_user_id, $p_name, $p_description, $c_date_updated, $c_tag_id ) );
+	if ( $p_project_id >= 0 ) {
+		db_query_bound( $query, Array( (int)$p_user_id, $p_name, $p_project_id, $p_description, $c_date_updated, $c_tag_id ) );
+	}
+	else {
+		db_query_bound( $query, Array( (int)$p_user_id, $p_name, $p_description, $c_date_updated, $c_tag_id ) );
+	}
 
 	if( $t_rename ) {
 		$t_bugs = tag_get_bugs_attached( $p_tag_id );
@@ -408,12 +442,13 @@ function tag_get_candidates_for_bug( $p_bug_id ) {
 	$t_params = array();
 	if ( 0 != $p_bug_id ) {
 		$t_bug_tag_table = db_get_table( 'mantis_bug_tag_table' );
-
+		$t_bug = bug_get( $p_bug_id );
+		$t_params[] = $p_bug_id;
 		if ( db_is_mssql() ) {
-			$t_params[] = $p_bug_id;
 			$query = "SELECT t.id FROM $t_tag_table t
 					LEFT JOIN $t_bug_tag_table b ON t.id=b.tag_id
-					WHERE b.bug_id IS NULL OR b.bug_id != " . db_param();
+					WHERE (b.bug_id IS NULL OR b.bug_id != " . db_param() .")
+					AND project_id IN (0, " . $t_bug->project_id . ")";
 			$result = db_query_bound( $query, $t_params );
 
 			$t_subquery_results = array();
@@ -426,12 +461,12 @@ function tag_get_candidates_for_bug( $p_bug_id ) {
 			$query = "SELECT id, name, description FROM $t_tag_table WHERE id IN (
 					SELECT t.id FROM $t_tag_table t
 					LEFT JOIN $t_bug_tag_table b ON t.id=b.tag_id
-					WHERE b.bug_id IS NULL OR b.bug_id != " . db_param() .
-				')';
+					WHERE (b.bug_id IS NULL OR b.bug_id != " . db_param() . ")
+					AND project_id IN (0, " . $t_bug->project_id . ")
+				)";
 		}
-		$t_params[] = $p_bug_id;
 	} else {
-		$query = 'SELECT id, name, description FROM ' . $t_tag_table;
+		$query = 'SELECT id, name, description FROM ' . $t_tag_table . " WHERE project_id IN (0, " . helper_get_current_project() . ")";
 	}
 
 	$query .= ' ORDER BY name ASC ';
diff --git a/manage_tags_page.php b/manage_tags_page.php
index 0718160..f0908ad 100644
--- a/manage_tags_page.php
+++ b/manage_tags_page.php
@@ -32,6 +32,8 @@ require_once( 'tag_api.php' );
 require_once( 'user_pref_api.php' );
 require_once( 'form_api.php' );
 
+auth_reauthenticate();
+
 access_ensure_global_level( config_get( 'tag_edit_threshold' ) );
 
 compress_enable();
@@ -78,11 +80,10 @@ echo '</tr></table>';
 
 $t_where_params = array();
 
-if ( $f_filter === 'ALL' ) {
-	$t_where = '';
-} else {
+$t_where = "WHERE project_id in (0, " . helper_get_current_project() . ")";
+if ( $f_filter !== 'ALL' ) {
 	$t_where_params[] = $f_filter . '%';
-	$t_where = 'WHERE ' . db_helper_like( 'name' );
+	$t_where .= ' AND ' . db_helper_like( 'name' );
 }
 
 # Set the number of Tags per page.
@@ -141,7 +142,8 @@ $t_result = db_query_bound( $t_query, $t_where_params, $t_per_page, $t_offset );
 		</td>
 	</tr>
 	<tr class="row-category">
-		<td width="25%"><?php echo lang_get( 'tag_name' ) ?></td>
+		<td width="20%"><?php echo lang_get( 'tag_name' ) ?></td>
+		<td width="20%"><?php echo lang_get( 'project_name' ) ?></td>
 		<td width="20%"><?php echo lang_get( 'tag_creator' ) ?></td>
 		<td width="20%"><?php echo lang_get( 'tag_created' ) ?></td>
 		<td width="20%"><?php echo lang_get( 'tag_updated' ) ?></td>
@@ -157,6 +159,7 @@ foreach ( $t_result as $t_tag_row ) {
 		<?php } else { ?>
 		<td><?php echo $t_tag_name ?></td>
 		<?php } ?>
+		<td><?php echo string_display_line( project_get_name( $t_tag_row['project_id'] ) ) ?></td>
 		<td><?php echo string_display_line( user_get_name( $t_tag_row['user_id'] ) ) ?></td>
 		<td><?php echo date( config_get( 'normal_date_format' ), $t_tag_row['date_created'] ) ?></td>
 		<td><?php echo date( config_get( 'normal_date_format' ), $t_tag_row['date_updated'] ) ?></td>
@@ -204,7 +207,19 @@ foreach ( $t_result as $t_tag_row ) {
 			<?php echo sprintf( lang_get( 'tag_separate_by' ), config_get( 'tag_separator' ) ); ?>
 		</td>
 	</tr>
-	<tr class="row-2">
+	<?php if ( config_get( 'tag_project_specific_enabled' ) ) { ?>
+	<tr class="row-2" valign="top">
+		<td class="category">
+			<?php echo lang_get( 'project_name' ) ?>
+		</td>
+		<td>
+			<select name="project_id">
+				<?php print_project_option_list( helper_get_current_project(), true ) ?>
+			</select>
+		</td>
+	</tr>
+	<?php } ?>
+	<tr class="row-1">
 		<td class="category">
 			<?php echo lang_get( 'tag_description' ) ?>
 		</td>
diff --git a/tag_attach.php b/tag_attach.php
index 14086f7..09b908b 100644
--- a/tag_attach.php
+++ b/tag_attach.php
@@ -32,6 +32,8 @@
 	form_security_validate( 'tag_attach' );
 
 	$f_bug_id = gpc_get_int( 'bug_id' );
+	$t_bug = bug_get( $f_bug_id );
+	$t_project_id = $t_bug->project_id;
 	$f_tag_select = gpc_get_int( 'tag_select' );
 	$f_tag_string = gpc_get_string( 'tag_string' );
 
@@ -43,7 +45,7 @@
 	 *     to the APIs.  This is to allow other clients of the API to support such
 	 *     functionality.  The access level checks should also be moved to the API.
 	 */
-	$t_tags = tag_parse_string( $f_tag_string );
+	$t_tags = tag_parse_string( $f_tag_string, $t_project_id );
 	$t_can_create = access_has_global_level( config_get( 'tag_create_threshold' ) );
 
 	$t_tags_create = array();
@@ -117,7 +119,7 @@
 		// end failed to attach tag
 	} else {
 		foreach( $t_tags_create as $t_tag_row ) {
-			$t_tag_row['id'] = tag_create( $t_tag_row['name'], $t_user_id );
+			$t_tag_row['id'] = tag_create( $t_tag_row['name'], $t_user_id, '', $t_project_id );
 			$t_tags_attach[] = $t_tag_row;
 		}
 
diff --git a/tag_create.php b/tag_create.php
index fa9662f..83c62fe 100644
--- a/tag_create.php
+++ b/tag_create.php
@@ -32,6 +32,13 @@ form_security_validate( 'tag_create' );
 
 $f_tag_name = gpc_get_string( 'name' );
 $f_tag_description = gpc_get_string( 'description' );
+if ( config_get( 'tag_project_specific_enabled' ) ) {
+	$f_project_id = gpc_get_int( 'project_id' );
+}
+else {
+	$f_project_id = 0;
+}
+
 
 $t_tag_user = auth_get_current_user_id();
 
@@ -39,7 +46,7 @@ if ( !is_null( $f_tag_name )) {
 	$t_tags = tag_parse_string( $f_tag_name );
 	foreach ( $t_tags as $t_tag_row ) {
 		if ( -1 == $t_tag_row['id'] ) {
-			tag_create( $t_tag_row['name'], $t_tag_user, $f_tag_description );
+			tag_create( $t_tag_row['name'], $t_tag_user, $f_tag_description, $f_project_id );
 		}
 	}
 }
diff --git a/tag_update.php b/tag_update.php
index 1252f89..310b40c 100644
--- a/tag_update.php
+++ b/tag_update.php
@@ -35,6 +35,12 @@
 
 	$f_tag_id = gpc_get_int( 'tag_id' );
 	$t_tag_row = tag_get( $f_tag_id );
+	if ( config_get( 'tag_project_specific_enabled' ) ) {
+		$f_project_id = gpc_get_int( 'project_id' );
+	}
+	else {
+		$f_project_id = -1;
+	}
 
 	if ( !( access_has_global_level( config_get( 'tag_edit_threshold' ) )
 		|| ( auth_get_current_user_id() == $t_tag_row['user_id'] )
@@ -65,7 +71,7 @@
 		$t_update = true;
 	}
 
-	tag_update( $f_tag_id, $f_new_name, $f_new_user_id, $f_new_description );
+	tag_update( $f_tag_id, $f_new_name, $f_new_user_id, $f_new_description, $f_project_id );
 
 	form_security_purge( 'tag_update' );
 
diff --git a/tag_update_page.php b/tag_update_page.php
index 69de115..323aac6 100644
--- a/tag_update_page.php
+++ b/tag_update_page.php
@@ -49,6 +49,8 @@
 	}
 
 	html_page_top( sprintf( lang_get( 'tag_update' ), $t_name ) );
+	
+	print_manage_menu( '' );
 ?>
 
 <br />
@@ -69,16 +71,26 @@
 
 <!-- Info -->
 <tr class="row-category">
-	<td width="15%"><?php echo lang_get( 'tag_id' ) ?></td>
+	<td width="5%"><?php echo lang_get( 'tag_id' ) ?></td>
 	<td width="25%"><?php echo lang_get( 'tag_name' ) ?></td>
-	<td width="20%"><?php echo lang_get( 'tag_creator' ) ?></td>
-	<td width="20%"><?php echo lang_get( 'tag_created' ) ?></td>
-	<td width="20%"><?php echo lang_get( 'tag_updated' ) ?></td>
+	<td width="25%"><?php echo lang_get( 'project_name' ) ?></td>
+	<td width="15%"><?php echo lang_get( 'tag_creator' ) ?></td>
+	<td width="15%"><?php echo lang_get( 'tag_created' ) ?></td>
+	<td width="15%"><?php echo lang_get( 'tag_updated' ) ?></td>
 </tr>
 
 <tr <?php echo helper_alternate_class() ?>>
 	<td><?php echo $t_tag_row['id'] ?></td>
 	<td><input type="text" <?php echo helper_get_tab_index() ?> name="name" value="<?php echo $t_name ?>"/></td>
+	<td>
+		<?php if ( config_get( 'tag_project_specific_enabled' ) ) { ?>
+		<select name="project_id">
+			<?php print_project_option_list( $t_tag_row['project_id'], true ) ?>
+		</select>
+		<?php } else {
+			print( project_get_name( $t_tag_row['project_id'] ) );
+		} ?>
+	</td>
 	<td><?php
 			if ( access_has_global_level( config_get( 'tag_edit_threshold' ) ) ) {
 				if ( ON == config_get( 'use_javascript' ) ) {
@@ -99,13 +111,13 @@
 
 <!-- spacer -->
 <tr class="spacer">
-	<td colspan="5"></td>
+	<td colspan="6"></td>
 </tr>
 
 <!-- Description -->
 <tr <?php echo helper_alternate_class() ?>>
 	<td class="category"><?php echo lang_get( 'tag_description' ) ?></td>
-	<td colspan="4">
+	<td colspan="5">
 		<textarea name="description" <?php echo helper_get_tab_index() ?> cols="80" rows="6"><?php echo string_textarea( $t_description ) ?></textarea>
 	</td>
 </tr>
diff --git a/tag_view_page.php b/tag_view_page.php
index 1eb3e7f..7a811a6 100644
--- a/tag_view_page.php
+++ b/tag_view_page.php
@@ -39,6 +39,8 @@
 	$t_description = string_display( $t_tag_row['description'] );
 
 	html_page_top( sprintf( lang_get( 'tag_details' ), $t_name ) );
+	
+	print_manage_menu( '' );
 ?>
 
 <br />
@@ -57,16 +59,18 @@
 
 <!-- Info -->
 <tr class="row-category">
-	<td width="15%"><?php echo lang_get( 'tag_id' ) ?></td>
-	<td width="25%"><?php echo lang_get( 'tag_name' ) ?></td>
-	<td width="20%"><?php echo lang_get( 'tag_creator' ) ?></td>
-	<td width="20%"><?php echo lang_get( 'tag_created' ) ?></td>
-	<td width="20%"><?php echo lang_get( 'tag_updated' ) ?></td>
+	<td width="10%"><?php echo lang_get( 'tag_id' ) ?></td>
+	<td width="30%"><?php echo lang_get( 'tag_name' ) ?></td>
+	<td width="30%"><?php echo lang_get( 'project_name' ) ?></td>
+	<td width="10%"><?php echo lang_get( 'tag_creator' ) ?></td>
+	<td width="10%"><?php echo lang_get( 'tag_created' ) ?></td>
+	<td width="10%"><?php echo lang_get( 'tag_updated' ) ?></td>
 </tr>
 
 <tr <?php echo helper_alternate_class() ?>>
 	<td><?php echo $t_tag_row['id'] ?></td>
 	<td><?php echo $t_name ?></td>
+	<td><?php echo string_display_line( project_get_name( $t_tag_row['project_id'] ) ) ?></td>
 	<td><?php echo string_display_line( user_get_name($t_tag_row['user_id']) ) ?></td>
 	<td><?php echo date( config_get( 'normal_date_format' ), $t_tag_row['date_created'] ) ?> </td>
 	<td><?php echo date( config_get( 'normal_date_format' ), $t_tag_row['date_updated'] ) ?> </td>
@@ -74,13 +78,13 @@
 
 <!-- spacer -->
 <tr class="spacer">
-	<td colspan="5"></td>
+	<td colspan="6"></td>
 </tr>
 
 <!-- Description -->
 <tr <?php echo helper_alternate_class() ?>>
 	<td class="category"><?php echo lang_get( 'tag_description' ) ?></td>
-	<td colspan="4"><?php echo $t_description ?></td>
+	<td colspan="5"><?php echo $t_description ?></td>
 </tr>
 
 <!-- Statistics -->
@@ -98,7 +102,7 @@
 
 			echo ( $i > 0 ? '<tr '.helper_alternate_class().'>' : '' );
 			echo "<td><a href='tag_view_page.php?tag_id=$t_tag[id]' title='$t_description'>$t_name</a></td>\n";
-			echo '<td colspan="3">';
+			echo '<td colspan="4">';
 			print_bracket_link( 'search.php?tag_string='.urlencode("+$t_tag_row[name]".config_get('tag_separator')."+$t_name"), sprintf( lang_get( 'tag_related_issues' ), $t_tag['count'] ) );
 			echo '</td></tr>';
 
@@ -109,7 +113,7 @@
 
 <!-- Buttons -->
 <tr>
-	<td colspan="5">
+	<td colspan="6">
 <?php
 	$t_can_edit = access_has_global_level( config_get( 'tag_edit_threshold' ) );
 	$t_can_edit_own = $t_can_edit || auth_get_current_user_id() == tag_get_field( $f_tag_id, 'user_id' )
-- 
1.7.4.msysgit.0

Relationships

has duplicate 0010161 closedjreese Suggestion - Unable to set tags as project specfic 
has duplicate 0015299 closedatrol Tags exposed between private projects 

Activities

ghohm

ghohm

2008-10-20 11:03

reporter   ~0019599

Is it true? It's not in the standard?!

jreese

jreese

2008-10-21 08:25

reporter   ~0019618

No, tags are not separated by project because that would create duplication, and lots of extra complexity and complications, which I don't think is really feasible.

olegos

olegos

2008-10-28 17:26

reporter   ~0019718

I think tags should be similar to categories -- global tags, per-project tags, copying tags from one project to another.

olegos

olegos

2008-11-10 16:59

reporter   ~0019836

I really would like to see this implemented. I don't see why it's necessarily complicated. You could just add a property to tags for a project id or All Projects; and when placing a tag, if it's a new one, have a confirmation screen for "You're adding a new tag - are you sure?" and a checkbox for "Make it global" versus "Apply to the current project only" (I think the confirmation is necessary anyway, to differentiate typos of existing tags from legitimate new ones). Then in the drop-down list of tags only select tags for All Projects + the current project (but still allow anything to by typed).

I have several projects on my system which are completely different (different types, e.g. hardware vs software vs support; some are for internal use only, others are accessible to external customers). I keep them completely separated: all projects are private, and all users are globally viewers. Tagging is a very nice feature and I'd like to use it, but without being able to separate tags between projects it's useless to me. By the way, I'm planning on allowing only project managers to create new tags, but reporters & above will be able to use existing tags.

jreese

jreese

2008-11-10 21:00

reporter   ~0019838

Olegos, it's not as simple as you make it out to be. The entire tagging system for Mantis works under the assumption that tags are global, including the API, all views and forms, and the database (the database schema being the least of my worries).

The problems and complications arise when you try to move this system into a project-specific manner. Not only would most of the API, pages, and forms need to be reworked (some completely rewritten), but you also introduce many added layers of complexity:

  • How do you deal with permissions for creating tags for global and per-project usage? I could see multiple different sets of needs.

  • How do you handle two projects needing the same tag, but not globally? You then enter problems with filtering on a single tag name but multiple tag ids.

  • What if you create a tag in two projects, and then decide you want it global?

  • What happens when you have nested projects? Do your tags inherit from parent projects?

It's not as easy as it seems to you, and I don't see why tags are automatically "useless" just because they're global. Is it really a show-stopper just because a software guy might see a hardware-related tag? If you really need per-project organization, categories are always available and already include the proper API and interfaces elements for handling them per-project or globally.

At this point in development, tags are global, and no offense, but that's most likely how it will stay. Categories have been overhauled in 1.2 to offer the option of project inheritence and global categorization, and it basically required a complete rewrite to get those features; I'd have to believe that (as the person who wrote the tagging system), that it would be the same thing for tags, and quite frankly, it's not worth it to me and most people who use them.

I hate to be the bearer of bad news, but what you ask will require a lot of effort to implement; if you really want the features, then I urge you to implement it yourself and submit a patch (or series of patches) for us to review and consider.

olegos

olegos

2008-11-12 16:07

reporter   ~0019858

@jreese: That's a bummer. Yes, it's a show-stopper for us to for example have customers see developers' tags on another project (and in fact I was going to propose that each tag should have access level associated with it, so that some tags could be used only by developers, and be invisible to reporters -- but that's a separate feature). We don't even want reporters on one project knowing what technologies are being used on other projects.

Categories are good, but a big advantage of tags is that more than one can be applied. But as far as project/subproject/global behavior, I think categories have similar problems to tags and do reasonable things, and tags should be modeled after them.

Specifically, about your questions:

  • Permissions: I'm fine with one global set of permissions.

  • Multiple projects, same tag: same as categories.

  • Making a project tag global: same as categories.

  • Inheriting from project to sub-project: same as categories.

I think you're wrong about most people not caring about tags being always global.

By the way, I would be happy with something simple like tags always being per-project, no inheritance, but allowing them to be copied in bulk between projects (hey, this sounds a lot like categories before the overhaul). Maybe with a config option for "tags are global", for those who prefer the current behavior.

djcarr

djcarr

2009-05-26 20:43

reporter   ~0021930

Slightly off-ball idea... what about a Custom Field that is of a Tags type? So for this custom field you could enter in a series of tags, delete them, etc. And Custom Fields already have project-level control embedded in Mantis, so you can expose this field on whichever projects you choose.

Sergiodf

Sergiodf

2009-08-06 11:11

reporter   ~0022674

Olegos:
Maybe you can patch Mantis to automatically add project name at end of the tag and filter by tag ending when showing. You get unspecified behavior when no project is selected, but may work for you. Quick and dirty.

Georges_

Georges_

2011-06-21 05:54

reporter   ~0029046

I agree with olegos. It will be a nice nice thing to have project separated tags.

dominik

dominik

2011-09-12 10:41

reporter   ~0029692

Attached a possible solution:

To enable this feature run a Mantis update (new column "project_id" in tag table is required)
and set the config value $g_tag_project_specific_enabled to ON (defaults to OFF)

If project specific tags are enabled the following happens:

a) All existing tags will be considered as "global" (tags are saved with project_id = 0).
Every "global" tag may be used in all projects.

b) Newly created tags are related to the current project (project of the bug precisely).
These tags are only available within the same project scope.

c) No special handling of "parent projects" - a parent project is equal to a child project.
Either tags for the parent project exists or not - no usage of "collected child project tags"

d) The tag - project relation may be edited in the tag management section. The tag overview
shows only "global" tags and tags for the current project.

e) The filter shows only "global" tags and tags for the current project. Therefore filtering
issues for specific tag(s) over multiple projects works only with "global" tags.

f) When switching $g_tag_project_specific_enabled between ON and OFF the existing tags will keep
its project relation. New tags will always be "global" for "all projects". This makes sure that
existing "project specific" tags are not unintentionally "globally" available

Other improvements:

  • Added auth_reauthenticate() to the manage_tags_page.php
  • Added the print_manage_menu() to tag_view_page.php and tag_update_page.php to improve
    usability (navigation back to tags overview after viewing/editing a tag)

Feedback appreciated - Would be nice if this "feature" can be integrated into the official version...

atrol

atrol

2011-09-17 09:56

developer   ~0029765

dominik, thank you for the suggested patch.
Database schema changes are not allowed in master-1.2.x. This would break the updates in master branch.

Would you consider submitting pull requests for this functionality? This would increase the speed of including these changes in MantisBT.
Ideally you would submit pull requests for the master branch at https://github.com/mantisbt/mantisbt

Before going on you should write to the developer mailing list to get some more feedback of other developers.

ilsaul

ilsaul

2016-07-08 16:07

reporter   ~0053544

Hi,
I make the pull request because i need it.
I make on master-1.3.x

atrol

atrol

2016-07-08 18:46

developer   ~0053547

@ilsaul I commented your PR https://github.com/mantisbt/mantisbt/pull/814

You should also consider what I wrote to dominik at 0009716:0029765

Before going on you should write to the developer mailing list to get some more feedback of other developers.

grisch111

grisch111

2018-02-02 05:37

reporter   ~0058713

I really would like to see this implemented. It would be helpful to have Tags based on the project and not global. Is there a new update for this ticket?

c2pil

c2pil

2019-06-07 12:39

reporter   ~0062226

Hi,

We would like to work on this feature as well.
We will try to work on the previous PR, taking into account the comments the community already made.

Are database schema changes authorized in the current Mantis version ?

Thanks in advance

atrol

atrol

2019-06-10 10:04

developer   ~0062231

Database schema changes are not allowed in version 2.x.
I will discuss with other devs if we will change this rule, or if there would be a need to start Mantis 3.x development.

c2pil

c2pil

2019-06-24 10:52

reporter   ~0062311

Hi,

In case there are no updates on the subject, here is another idea :

  • Add an option to disable (entirely) the tagging system as it exists right now
  • Add a default plugin which would be included in MantisBT and would handle tags by project

Would it be feasible ?

dregad

dregad

2019-06-25 10:09

developer   ~0062316

Everything is feasible, the question is the effort required to achieve the goal ;-)

I am not opposed to the idea, but moving tags management to a plugin would would not be a small undertaking, first to decouple the code from Mantis core, then creating the necessary events, write the plugin and handle the data migration...