Преглед изворни кода

Merge pull request #318 from Oldiesmann/release-2.1

Group-based board moderation
Oldiesmann пре 11 година
родитељ
комит
7e1157f083

+ 10 - 0
Sources/Display.php

@@ -482,13 +482,23 @@ function Display()
 
 	// Build a list of this board's moderators.
 	$context['moderators'] = &$board_info['moderators'];
+	$context['moderator_groups'] = &$board_info['moderator_groups'];
 	$context['link_moderators'] = array();
 	if (!empty($board_info['moderators']))
 	{
 		// Add a link for each moderator...
 		foreach ($board_info['moderators'] as $mod)
 			$context['link_moderators'][] = '<a href="' . $scripturl . '?action=profile;u=' . $mod['id'] . '" title="' . $txt['board_moderator'] . '">' . $mod['name'] . '</a>';
+	}
+	if (!empty($board_info['moderator_groups']))
+	{
+		// Add a link for each moderator group as well...
+		foreach ($board_info['moderator_groups'] as $mod_group)
+			$context['link_moderators'][] = '<a href="' . $scripturl . '?action=groups;sa=viewmemberes;group=' . $mod_group['id'] . '" title="' . $txt['board_moderator'] . '">' . $mod_group['name'] . '</a>';
+	}
 
+	if (!empty($context['link_moderators']))
+	{
 		// And show it after the board's name.
 		$context['linktree'][count($context['linktree']) - 2]['extra_after'] = '<span class="board_moderators"> (' . (count($context['link_moderators']) == 1 ? $txt['moderator'] : $txt['moderators']) . ': ' . implode(', ', $context['link_moderators']) . ')</span>';
 	}

+ 60 - 8
Sources/Load.php

@@ -543,7 +543,7 @@ function loadBoard()
 	// Load this board only if it is specified.
 	if (empty($board) && empty($topic))
 	{
-		$board_info = array('moderators' => array());
+		$board_info = array('moderators' => array(), 'moderator_groups' => array());
 		return;
 	}
 
@@ -567,13 +567,16 @@ function loadBoard()
 		$request = $smcFunc['db_query']('', '
 			SELECT
 				c.id_cat, b.name AS bname, b.description, b.num_topics, b.member_groups, b.deny_member_groups,
-				b.id_parent, c.name AS cname, IFNULL(mem.id_member, 0) AS id_moderator,
+				b.id_parent, c.name AS cname, IFNULL(mg.id_group, 0) AS id_moderator_group, mg.group_name,
+				IFNULL(mem.id_member, 0) AS id_moderator,
 				mem.real_name' . (!empty($topic) ? ', b.id_board' : '') . ', b.child_level,
 				b.id_theme, b.override_theme, b.count_posts, b.id_profile, b.redirect,
 				b.unapproved_topics, b.unapproved_posts' . (!empty($topic) ? ', t.approved, t.id_member_started' : '') . '
 			FROM {db_prefix}boards AS b' . (!empty($topic) ? '
 				INNER JOIN {db_prefix}topics AS t ON (t.id_topic = {int:current_topic})' : '') . '
 				LEFT JOIN {db_prefix}categories AS c ON (c.id_cat = b.id_cat)
+				LEFT JOIN {db_prefix}moderator_groups AS modgs ON (modgs.id_board = {raw:board_link})
+				LEFT JOIN {db_prefix}membergroups AS mg ON (mg.id_group = modgs.id_group)
 				LEFT JOIN {db_prefix}moderators AS mods ON (mods.id_board = {raw:board_link})
 				LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = mods.id_member)
 			WHERE b.id_board = {raw:board_link}',
@@ -595,6 +598,7 @@ function loadBoard()
 			$board_info = array(
 				'id' => $board,
 				'moderators' => array(),
+				'moderator_groups' => array(),
 				'cat' => array(
 					'id' => $row['id_cat'],
 					'name' => $row['cname']
@@ -630,6 +634,14 @@ function loadBoard()
 						'href' => $scripturl . '?action=profile;u=' . $row['id_moderator'],
 						'link' => '<a href="' . $scripturl . '?action=profile;u=' . $row['id_moderator'] . '">' . $row['real_name'] . '</a>'
 					);
+
+				if (!empty($row['id_moderator_group']))
+					$board_info['moderator_groups'][$row['id_moderator_group']] = array(
+						'id' => $row['id_moderator_group'],
+						'name' => $row['group_name'],
+						'href' => $scripturl . '?action=groups;sa=members;group=' . $row['id_moderator_group'],
+						'link' => '<a href="' . $scripturl . '?action=groups;sa=members;group=' . $row['id_moderator_group'] . '">' . $row['group_name'] . '</a>'
+					);
 			}
 			while ($row = $smcFunc['db_fetch_assoc']($request));
 
@@ -671,6 +683,7 @@ function loadBoard()
 			// Otherwise the topic is invalid, there are no moderators, etc.
 			$board_info = array(
 				'moderators' => array(),
+				'moderator_groups' => array(),
 				'error' => 'exist'
 			);
 			$topic = null;
@@ -684,8 +697,11 @@ function loadBoard()
 
 	if (!empty($board))
 	{
+		// Get this into an array of keys for array_intersect
+		$moderator_groups = array_keys($board_info['moderator_groups']);
+		
 		// Now check if the user is a moderator.
-		$user_info['is_mod'] = isset($board_info['moderators'][$user_info['id']]);
+		$user_info['is_mod'] = isset($board_info['moderators'][$user_info['id']]) || count(array_intersect($user_info['groups'], $moderator_groups)) != 0;
 
 		if (count(array_intersect($user_info['groups'], $board_info['groups'])) == 0 && !$user_info['is_admin'])
 			$board_info['error'] = 'access';
@@ -925,11 +941,11 @@ function loadMemberData($users, $is_name = false, $set = 'normal')
 	switch ($set)
 	{
 		case 'normal':
-			$select_columns .= ', mem.buddy_list';
+			$select_columns .= ', mem.buddy_list,  mem.additional_groups';
 			break;
 		case 'profile':
 			$select_columns .= ', mem.openid_uri, mem.id_theme, mem.pm_ignore_list, mem.pm_email_notify, mem.pm_receive_from,
-			mem.time_format, mem.secret_question,  mem.additional_groups, mem.smiley_set,
+			mem.time_format, mem.secret_question, mem.smiley_set,
 			mem.total_time_logged_in, mem.notify_announcements, mem.notify_regularity, mem.notify_send_body,
 			mem.notify_types, lo.url, mem.ignore_boards, mem.password_salt, mem.pm_prefs, mem.buddy_list';
 			break;
@@ -984,6 +1000,27 @@ function loadMemberData($users, $is_name = false, $set = 'normal')
 		$smcFunc['db_free_result']($request);
 	}
 
+	$additional_mods = array();
+	
+	// Are any of these users in groups assigned to moderate this board?
+	if (!empty($loaded_ids) && !empty($board_info['moderator_groups']) && $set === 'normal')
+	{
+		foreach ($loaded_ids as $a_member)
+		{
+			if (!empty($user_profile[$a_member]['additional_groups']))
+				$groups = array_merge(array($user_profile[$a_member]['id_group']), explode(',', $user_profile[$a_member]['additional_groups']));
+			else
+				$groups = array($user_profile[$a_member]['id_group']);
+			
+			$temp = array_intersect($groups, array_keys($board_info['moderator_groups']));
+			
+			if (!empty($temp))
+			{
+				$additional_mods[] = $a_member;
+			}
+		}
+	}
+
 	if (!empty($new_loaded_ids) && !empty($modSettings['cache_enable']) && $modSettings['cache_enable'] >= 3)
 	{
 		for ($i = 0, $n = count($new_loaded_ids); $i < $n; $i++)
@@ -991,7 +1028,7 @@ function loadMemberData($users, $is_name = false, $set = 'normal')
 	}
 
 	// Are we loading any moderators?  If so, fix their group data...
-	if (!empty($loaded_ids) && !empty($board_info['moderators']) && $set === 'normal' && count($temp_mods = array_intersect($loaded_ids, array_keys($board_info['moderators']))) !== 0)
+	if (!empty($loaded_ids) && (!empty($board_info['moderators']) || !empty($board_info['moderator_groups'])) && $set === 'normal' && count($temp_mods = array_merge(array_intersect($loaded_ids, array_keys($board_info['moderators'])), $additional_mods)) !== 0)
 	{
 		if (($row = cache_get_data('moderator_group_info', 480)) == null)
 		{
@@ -2233,10 +2270,12 @@ function getBoardParents($id_parent)
 			$result = $smcFunc['db_query']('', '
 				SELECT
 					b.id_parent, b.name, {int:board_parent} AS id_board, IFNULL(mem.id_member, 0) AS id_moderator,
-					mem.real_name, b.child_level
+					mem.real_name, b.child_level, IFNULL(mg.id_group, 0) AS id_moderator_group, mg.group_name
 				FROM {db_prefix}boards AS b
 					LEFT JOIN {db_prefix}moderators AS mods ON (mods.id_board = b.id_board)
 					LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = mods.id_member)
+					LEFT JOIN {db_prefix}moderator_groups AS modgs ON (modgs.id_board = b.id_board)
+					LEFT JOIN {db_prefix}membergroups AS mg ON (mg.id_group = modgs.id_group)
 				WHERE b.id_board = {int:board_parent}',
 				array(
 					'board_parent' => $id_parent,
@@ -2254,7 +2293,8 @@ function getBoardParents($id_parent)
 						'url' => $scripturl . '?board=' . $row['id_board'] . '.0',
 						'name' => $row['name'],
 						'level' => $row['child_level'],
-						'moderators' => array()
+						'moderators' => array(),
+						'moderator_groups' => array()
 					);
 				}
 				// If a moderator exists for this board, add that moderator for all children too.
@@ -2268,6 +2308,18 @@ function getBoardParents($id_parent)
 							'link' => '<a href="' . $scripturl . '?action=profile;u=' . $row['id_moderator'] . '">' . $row['real_name'] . '</a>'
 						);
 					}
+				
+				// If a moderator group exists for this board, add that moderator group for all children too
+				if (!empty($row['id_moderator_group']))
+					foreach ($boards as $id => $dummy)
+					{
+						$boards[$id]['moderator_groups'][$row['id_moderator_group']] = array(
+							'id' => $row['id_moderator_group'],
+							'name' => $row['group_name'],
+							'href' => $scripturl . '?action=groups;sa=members;group=' . $row['id_moderator_group'],
+							'link' => '<a href="' . $scripturl . '?action=groups;sa=members;group=' . $row['id_moderator_group'] . '">' . $row['group_name'] . '</a>'
+						);					
+					}
 			}
 			$smcFunc['db_free_result']($result);
 		}

+ 29 - 0
Sources/ManageBoards.php

@@ -547,6 +547,25 @@ function EditBoard()
 	if (!empty($context['board']['moderators']))
 		list ($context['board']['last_moderator_id']) = array_slice(array_keys($context['board']['moderators']), -1);
 
+	// Get all the groups assigned as moderators
+	$request = $smcFunc['db_query']('', '
+		SELECT id_group
+		FROM {db_prefix}moderator_groups
+		WHERE id_board = {int:current_board}',
+		array(
+			'current_board' => $_REQUEST['boardid'],
+		)
+	);
+	$context['board']['moderator_groups'] = array();
+	while ($row = $smcFunc['db_fetch_assoc']($request))
+		$context['board']['moderator_groups'][$row['id_group']] = $context['groups'][$row['id_group']]['name'];
+	$smcFunc['db_free_result']($request);
+	
+	$context['board']['moderator_groups_list'] = empty($context['board']['moderator_groups']) ? '' : '&quot;' . implode('&quot;, &qout;', $context['board']['moderator_groups']) . '&quot;';
+
+	if (!empty($context['board']['moderator_groups']))
+		list ($context['board']['last_moderator_group_id']) = array_slice(array_keys($context['board']['moderator_groups']), -1);
+
 	// Get all the themes...
 	$request = $smcFunc['db_query']('', '
 		SELECT id_theme AS id, value AS name
@@ -650,6 +669,16 @@ function EditBoard2()
 			$boardOptions['moderators'] = $moderators;
 		}
 
+		$boardOptions['moderator_group_string'] = $_POST['moderator_groups'];
+		
+		if (isset($_POST['moderator_group_list']) && is_array($_POST['moderator_group_list']))
+		{
+			$moderator_groups = array();
+			foreach ($_POST['moderator_group_list'] as $moderator_group)
+				$moderator_groups[(int) $moderator_group] = (int) $moderator_group;
+			$boardOptions['moderator_groups'] = $moderator_groups;
+		}
+
 		// Are they doing redirection?
 		$boardOptions['redirect'] = !empty($_POST['redirect_enable']) && isset($_POST['redirect_address']) && trim($_POST['redirect_address']) != '' ? trim($_POST['redirect_address']) : '';
 

+ 13 - 2
Sources/MessageIndex.php

@@ -111,13 +111,24 @@ function MessageIndex()
 
 	// Build a list of the board's moderators.
 	$context['moderators'] = &$board_info['moderators'];
+	$context['moderator_groups'] = &$board_info['moderator_groups'];
 	$context['link_moderators'] = array();
 	if (!empty($board_info['moderators']))
 	{
 		foreach ($board_info['moderators'] as $mod)
 			$context['link_moderators'][] ='<a href="' . $scripturl . '?action=profile;u=' . $mod['id'] . '" title="' . $txt['board_moderator'] . '">' . $mod['name'] . '</a>';
-
-		$context['linktree'][count($context['linktree']) - 1]['extra_after'] = '<span class="board_moderators"> (' . (count($context['link_moderators']) == 1 ? $txt['moderator'] : $txt['moderators']) . ': ' . implode(', ', $context['link_moderators']) . ')</span>';
+	}
+	if (!empty($board_info['moderator_groups']))
+	{
+		// By default just tack the moderator groups onto the end of the members
+		foreach ($board_info['moderator_groups'] as $mod_group)
+			$context['link_moderators'][] = '<a href="' . $scripturl . '?action=groups;sa=members;group=' . $mod_group['id'] . '" title="' . $txt['board_moderator'] . '">' . $mod_group['name'] . '</a>';
+	}
+	
+	// Now we tack the info onto the end of the linktree
+	if (!empty($context['link_moderators']))
+	{
+	 	$context['linktree'][count($context['linktree']) - 1]['extra_after'] = '<span class="board_moderators"> (' . (count($context['link_moderators']) == 1 ? $txt['moderator'] : $txt['moderators']) . ': ' . implode(', ', $context['link_moderators']) . ')</span>';
 	}
 
 	// Mark current and parent boards as seen.

+ 7 - 4
Sources/Profile-View.php

@@ -2218,12 +2218,14 @@ function showPermissions($memID)
 
 	// Load a list of boards for the jump box - except the defaults.
 	$request = $smcFunc['db_query']('order_by_board_order', '
-		SELECT b.id_board, b.name, b.id_profile, b.member_groups, IFNULL(mods.id_member, 0) AS is_mod
+		SELECT b.id_board, b.name, b.id_profile, b.member_groups, IFNULL(mods.id_member, IFNULL(modgs.id_group, 0)) AS is_mod
 		FROM {db_prefix}boards AS b
 			LEFT JOIN {db_prefix}moderators AS mods ON (mods.id_board = b.id_board AND mods.id_member = {int:current_member})
+			LEFT JOIN {db_prefix}moderator_groups AS modgs ON (modgs.id_board = b.id_board AND modgs.id_group IN ({array_int:current_groups}))
 		WHERE {query_see_board}',
 		array(
 			'current_member' => $memID,
+			'current_groups' => $curGroups,
 		)
 	);
 	$context['boards'] = array();
@@ -2313,14 +2315,15 @@ function showPermissions($memID)
 	$request = $smcFunc['db_query']('', '
 		SELECT
 			bp.add_deny, bp.permission, bp.id_group, mg.group_name' . (empty($board) ? '' : ',
-			b.id_profile, CASE WHEN mods.id_member IS NULL THEN 0 ELSE 1 END AS is_moderator') . '
+			b.id_profile, CASE WHEN (mods.id_member IS NULL AND modgs.id_group IS NULL) THEN 0 ELSE 1 END AS is_moderator') . '
 		FROM {db_prefix}board_permissions AS bp' . (empty($board) ? '' : '
 			INNER JOIN {db_prefix}boards AS b ON (b.id_board = {int:current_board})
-			LEFT JOIN {db_prefix}moderators AS mods ON (mods.id_board = b.id_board AND mods.id_member = {int:current_member})') . '
+			LEFT JOIN {db_prefix}moderators AS mods ON (mods.id_board = b.id_board AND mods.id_member = {int:current_member})
+			LEFT JOIN {db_prefix}moderator_groups AS modgs ON (modgs.id_board = b.id_board AND modgs.id_group IN ({array_int:group_list}))') . '
 			LEFT JOIN {db_prefix}membergroups AS mg ON (mg.id_group = bp.id_group)
 		WHERE bp.id_profile = {raw:current_profile}
 			AND bp.id_group IN ({array_int:group_list}' . (empty($board) ? ')' : ', {int:moderator_group})
-			AND (mods.id_member IS NOT NULL OR bp.id_group != {int:moderator_group})'),
+			AND (mods.id_member IS NOT NULL OR modgs.id_group IS NOT NULL OR bp.id_group != {int:moderator_group})'),
 		array(
 			'current_board' => $board,
 			'group_list' => $curGroups,

+ 57 - 0
Sources/Reports.php

@@ -156,6 +156,19 @@ function BoardReport()
 		$moderators[$row['id_board']][] = $row['real_name'];
 	$smcFunc['db_free_result']($request);
 
+	// Get every moderator gruop.
+	$request = $smcFunc['db_query']('', '
+		SELECT modgs.id_board, modgs.id_group, memg.group_name
+		FROM {db_prefix}moderator_groups AS modgs
+			INNER JOIN {db_prefix}membergroups AS memg ON (memg.id_group = modgs.id_group)',
+		array(
+		)
+	);
+	$moderator_groups = array();
+	while ($row = $smcFunc['db_fetch_assoc']($request))
+		$moderator_groups[$row['id_board']][] = $row['group_name'];
+	$smcFunc['db_free_result']($request);
+
 	// Get all the possible membergroups!
 	$request = $smcFunc['db_query']('', '
 		SELECT id_group, group_name, online_color
@@ -179,6 +192,7 @@ function BoardReport()
 		'override_theme' => $txt['board_override_theme'],
 		'profile' => $txt['board_profile'],
 		'moderators' => $txt['board_moderators'],
+		'moderator_groups' => $txt['board_moderator_groups'],
 		'groups' => $txt['board_groups'],
 	);
 	if (!empty($modSettings['deny_boards_access']))
@@ -223,6 +237,7 @@ function BoardReport()
 			'profile' => $profile_name,
 			'override_theme' => $row['override_theme'] ? $txt['yes'] : $txt['no'],
 			'moderators' => empty($moderators[$row['id_board']]) ? $txt['none'] : implode(', ', $moderators[$row['id_board']]),
+			'moderator_groups' => empty($moderator_groups[$row['id_board']]) ? $txt['none'] : implode(', ', $moderator_groups[$row['id_board']]),
 		);
 
 		// Work out the membergroups who can and cannot access it (but only if enabled).
@@ -309,10 +324,26 @@ function BoardPermissionsReport()
 		$boards[$row['id_board']] = array(
 			'name' => $row['name'],
 			'profile' => $row['id_profile'],
+			'mod_groups' => array(),
 		);
 		$profiles[] = $row['id_profile'];
 	}
 	$smcFunc['db_free_result']($request);
+	
+	// Get the ids of any groups allowed to moderate this board
+	// Limit it to any boards and/or groups we're looking at
+	$request = $smcFunc['db_query']('', '
+		SELECT id_board, id_group
+		FROM {db_prefix}moderator_groups
+		WHERE ' . $board_clause .' AND ' . $group_clause,
+		array(
+		)
+	);
+	while ($row = $smcFunc['db_fetch_assoc']($request))
+	{
+		$boards[$row['id_board']]['mod_groups'][] = $row['id_group'];
+	}
+	$smcFunc['db_free_result']($request);
 
 	// Get all the possible membergroups, except admin!
 	$request = $smcFunc['db_query']('', '
@@ -409,6 +440,11 @@ function BoardPermissionsReport()
 					// Set the data for this group to be the local permission.
 					$curData[$id_group] = $group_permissions[$ID_PERM];
 				}
+				// Is it inherited from Moderator?
+				elseif (in_array($id_group, $boards[$board]['mod_groups']) && !empty($groups[3]) && isset($groups[3][$ID_PERM]))
+				{
+					$curData[$id_group] = $groups[3][$ID_PERM];
+				}
 				// Otherwise means it's set to disallow..
 				else
 				{
@@ -702,6 +738,27 @@ function StaffReport()
 	}
 	$smcFunc['db_free_result']($request);
 
+	// Get any additional boards they can moderate through group-based board moderation
+	$request = $smcFunc['db_query']('', '
+		SELECT mem.id_member, modgs.id_board
+		FROM {db_prefix}members AS mem
+			INNER JOIN {db_prefix}moderator_groups AS modgs ON (modgs.id_group = mem.id_group OR FIND_IN_SET(modgs.id_group, mem.additional_groups) != 0)',
+		array(
+		)
+	);
+	
+	// Add each board/member to the arrays, but only if they aren't already there
+	while ($row = $smcFunc['db_fetch_assoc']($request))
+	{
+		// Either we don't have them as a moderator at all or at least not as a moderator of this board
+		if (!array_key_exists($row['id_member'], $moderators) || !in_array($row['id_board'], $moderators[$row['id_member']]))
+			$moderators[$row['id_member']][] = $row['id_board'];
+		
+		// We don't have them listed as a moderator yet
+		if (!array_key_exists($row['id_member'], $local_mods))
+			$local_mods[$row['id_member']] = $row['id_member'];
+	}
+
 	// Get a list of global moderators (i.e. members with moderation powers).
 	$global_mods = array_intersect(membersAllowedTo('moderate_board', 0), membersAllowedTo('approve_posts', 0), membersAllowedTo('remove_any', 0), membersAllowedTo('modify_any', 0));
 

+ 4 - 2
Sources/Security.php

@@ -951,10 +951,11 @@ function allowedTo($permission, $boards = null)
 		FROM {db_prefix}boards AS b
 			INNER JOIN {db_prefix}board_permissions AS bp ON (bp.id_profile = b.id_profile)
 			LEFT JOIN {db_prefix}moderators AS mods ON (mods.id_board = b.id_board AND mods.id_member = {int:current_member})
+			LEFT JOIN {db_prefix}moderator_groups AS modgs ON (modgs.id_board = b.id_board AND b.id_group IN ({array_int:group_list})
 		WHERE b.id_board IN ({array_int:board_list})
 			AND bp.id_group IN ({array_int:group_list}, {int:moderator_group})
 			AND bp.permission {raw:permission_list}
-			AND (mods.id_member IS NOT NULL OR bp.id_group != {int:moderator_group})
+			AND (mods.id_member IS NOT NULL OR modgs.id_group IS NOT NULL OR bp.id_group != {int:moderator_group})
 		GROUP BY b.id_board',
 		array(
 			'current_member' => $user_info['id'],
@@ -1087,9 +1088,10 @@ function boardsAllowedTo($permissions, $check_access = true, $simple = true)
 		FROM {db_prefix}board_permissions AS bp
 			INNER JOIN {db_prefix}boards AS b ON (b.id_profile = bp.id_profile)
 			LEFT JOIN {db_prefix}moderators AS mods ON (mods.id_board = b.id_board AND mods.id_member = {int:current_member})
+			LEFT JOIN {db_prefix}moderator_groups AS modgs ON (modgs.id_board = b.id_board AND modgs.id_group IN ({array_int:group_list}))
 		WHERE bp.id_group IN ({array_int:group_list}, {int:moderator_group})
 			AND bp.permission IN ({array_string:permissions})
-			AND (mods.id_member IS NOT NULL OR bp.id_group != {int:moderator_group})' .
+			AND (mods.id_member IS NOT NULL OR modgs.id_group IS NOT NULL OR bp.id_group != {int:moderator_group})' .
 			($check_access ? ' AND {query_see_board}' : ''),
 		array(
 			'current_member' => $user_info['id'],

+ 21 - 0
Sources/SendTopic.php

@@ -572,7 +572,28 @@ function ReportToModerator2()
 	while ($row = $smcFunc['db_fetch_assoc']($request2))
 		$real_mods[] = $row['id_member'];
 	$smcFunc['db_free_result']($request2);
+	
+	// Get any additional members who are in groups assigned to moderate this board
+	$request3 = $smcFunc['db_query']('', '
+		SELECT mem.id_member
+		FROM {db_prefix}members AS mem, {db_prefix}moderator_groups AS bm
+		WHERE bm.id_board = {int:current_board}
+			AND(
+				mem.id_group = bm.id_group
+				OR FIND_IN_SET(bm.id_group, mem.additional_groups) != 0
+			)',
+		array(
+			'current_board' => $board,
+		)
+	);
 
+	while ($row = $smcFunc['db_fetch_assoc']($request3))
+		$real_mods[] = $row['id_member'];
+	$smcFunc['db_free_result']($request3);
+	
+	// Make sure we don't have any duplicates
+	$real_mods = array_unique($real_mods);
+	
 	// Send every moderator an email.
 	while ($row = $smcFunc['db_fetch_assoc']($request))
 	{

+ 16 - 0
Sources/Subs-Auth.php

@@ -736,6 +736,22 @@ function rebuildModCache()
 		while ($row = $smcFunc['db_fetch_assoc']($request))
 			$boards_mod[] = $row['id_board'];
 		$smcFunc['db_free_result']($request);
+		
+		// Can any of the groups they're in moderate any of the boards?
+		$request = $smcFunc['db_query']('', '
+			SELECT id_board
+			FROM {db_prefix}moderator_groups
+			WHERE id_group IN({array_int:groups})',
+			array(
+				'groups' => $user_info['groups'],
+			)
+		);
+		while ($row = $smcFunc['db_fetch_assoc']($request))
+			$boards_mod[] = $row['id_board'];
+		$smcFunc['db_free_result']($request);
+		
+		// Just in case we've got duplicates here...
+		$boards_mod = array_unique($boards_mod);
 	}
 
 	$mod_query = empty($boards_mod) ? '0=1' : 'b.id_board IN (' . implode(',', $boards_mod) . ')';

+ 15 - 0
Sources/Subs-BoardIndex.php

@@ -54,6 +54,7 @@ function getBoardIndex($boardIndexOptions)
 			(IFNULL(lb.id_msg, 0) >= b.id_msg_updated) AS is_read, IFNULL(lb.id_msg, -1) + 1 AS new_from,' . ($boardIndexOptions['include_categories'] ? '
 			c.can_collapse, IFNULL(cc.id_member, 0) AS is_collapsed,' : '')) . '
 			IFNULL(mem.id_member, 0) AS id_member, mem.avatar, m.id_msg,
+			IFNULL(mods_grp.id_group, 0) AS id_moderator_group, mods_grp.group_name AS mod_group_name,
 			IFNULL(mods_mem.id_member, 0) AS id_moderator, mods_mem.real_name AS mod_real_name' . (!empty($settings['avatars_on_indexes']) ? ',
 			IFNULL(a.id_attach, 0) AS id_attach, a.filename, a.attachment_type' : '') . '
 		FROM {db_prefix}boards AS b' . ($boardIndexOptions['include_categories'] ? '
@@ -63,6 +64,8 @@ function getBoardIndex($boardIndexOptions)
 			LEFT JOIN {db_prefix}log_boards AS lb ON (lb.id_board = b.id_board AND lb.id_member = {int:current_member})' . ($boardIndexOptions['include_categories'] ? '
 			LEFT JOIN {db_prefix}collapsed_categories AS cc ON (cc.id_cat = c.id_cat AND cc.id_member = {int:current_member})' : '')) . '
 			LEFT JOIN {db_prefix}moderators AS mods ON (mods.id_board = b.id_board)
+			LEFT JOIN {db_prefix}moderator_groups AS mods_g ON (mods_g.id_board = b.id_board)
+			LEFT JOIN {db_prefix}membergroups AS mods_grp ON (mods_grp.id_group = mods_g.id_group)
 			LEFT JOIN {db_prefix}members AS mods_mem ON (mods_mem.id_member = mods.id_member)' . (!empty($settings['avatars_on_indexes']) ? '
 			LEFT JOIN {db_prefix}attachments AS a ON (a.id_member = m.id_member)' : '') . '
 		WHERE {query_see_board}' . (empty($boardIndexOptions['countChildPosts']) ? (empty($boardIndexOptions['base_level']) ? '' : '
@@ -137,7 +140,9 @@ function getBoardIndex($boardIndexOptions)
 					'name' => $row_board['board_name'],
 					'description' => $row_board['description'],
 					'moderators' => array(),
+					'moderator_groups' => array(),
 					'link_moderators' => array(),
+					'link_moderator_groups' => array(),
 					'children' => array(),
 					'link_children' => array(),
 					'children_new' => false,
@@ -161,6 +166,16 @@ function getBoardIndex($boardIndexOptions)
 				);
 				$this_category[$row_board['id_board']]['link_moderators'][] = '<a href="' . $scripturl . '?action=profile;u=' . $row_board['id_moderator'] . '" title="' . $txt['board_moderator'] . '">' . $row_board['mod_real_name'] . '</a>';
 			}
+			if (!empty($row_board['id_moderator_group']))
+			{
+				$this_category[$row_board['id_board']]['moderator_groups'][$row_board['id_moderator_group']] = array(
+					'id' => $row_board['id_moderator_group'],
+					'name' => $row_board['mod_group_name'],
+					'href' => $scripturl . '?action=groups;sa=members;group=' . $row_board['id_moderator_group'],
+					'link' => '<a href="' . $scripturl . '?action=groups;sa=members;group=' . $row_board['id_moderator_group'] . '" title="' . $txt['board_moderator'] . '">' . $row_board['mod_group_name'] . '</a>'
+				);
+				$this_category[$row_board['id_board']]['link_moderator_groups'][] = '<a href="' . $scripturl . '?action=groups;sa=members;group=' . $row_board['id_moderator_group'] . '" title="' . $txt['board_moderator'] . '">' . $row_board['mod_group_name'] . '</a>';
+			}
 		}
 		// Found a child board.... make sure we've found its parent and the child hasn't been set already.
 		elseif (isset($this_category[$row_board['id_parent']]['children']) && !isset($this_category[$row_board['id_parent']]['children'][$row_board['id_board']]))

+ 69 - 1
Sources/Subs-Boards.php

@@ -657,7 +657,7 @@ function modifyBoard($board_id, &$boardOptions)
 		);
 
 	// Set moderators of this board.
-	if (isset($boardOptions['moderators']) || isset($boardOptions['moderator_string']))
+	if (isset($boardOptions['moderators']) || isset($boardOptions['moderator_string']) || isset($boardOptions['moderator_groupss']) || isset($boardOptions['moderator_group_string']))
 	{
 		// Reset current moderators for this board - if there are any!
 		$smcFunc['db_query']('', '
@@ -718,6 +718,65 @@ function modifyBoard($board_id, &$boardOptions)
 			);
 		}
 
+		// Reset current moderator groups for this board - if there are any!
+		$smcFunc['db_query']('', '
+			DELETE FROM {db_prefix}moderator_groups
+			WHERE id_board = {int:board_list}',
+			array(
+				'board_list' => $board_id,
+			)
+		);
+
+		// Validate and get the IDs of the new moderator groups.
+		if (isset($boardOptions['moderator_group_string']) && trim($boardOptions['moderator_group_string']) != '')
+		{
+			// Divvy out the group names, remove extra space.
+			$moderator_group_string = strtr($smcFunc['htmlspecialchars']($boardOptions['moderator_group_string'], ENT_QUOTES), array('&quot;' => '"'));
+			preg_match_all('~"([^"]+)"~', $moderator_group_string, $matches);
+			$moderator_groups = array_merge($matches[1], explode(',', preg_replace('~"[^"]+"~', '', $moderator_group_string)));
+			for ($k = 0, $n = count($moderator_groups); $k < $n; $k++)
+			{
+				$moderator_groups[$k] = trim($moderator_groups[$k]);
+
+				if (strlen($moderator_groups[$k]) == 0)
+					unset($moderator_groups[$k]);
+			}
+
+			// Find all the id_member's for the member_name's in the list.
+			if (empty($boardOptions['moderator_groups']))
+				$boardOptions['moderator_groups'] = array();
+			if (!empty($moderator_groups))
+			{
+				$request = $smcFunc['db_query']('', '
+					SELECT id_group
+					FROM {db_prefix}membergroups
+					WHERE group_name IN ({array_string:moderator_group_list})
+					LIMIT ' . count($moderator_groups),
+					array(
+						'moderator_group_list' => $moderator_groups,
+					)
+				);
+				while ($row = $smcFunc['db_fetch_assoc']($request))
+					$boardOptions['moderator_groups'][] = $row['id_group'];
+				$smcFunc['db_free_result']($request);
+			}
+		}
+
+		// Add the moderator groups to the board.
+		if (!empty($boardOptions['moderator_groups']))
+		{
+			$inserts = array();
+			foreach ($boardOptions['moderator_groups'] as $moderator_group)
+				$inserts[] = array($board_id, $moderator_group);
+
+			$smcFunc['db_insert']('insert',
+				'{db_prefix}moderator_groups',
+				array('id_board' => 'int', 'id_group' => 'int'),
+				$inserts,
+				array('id_board', 'id_group')
+			);
+		}
+
 		// Note that caches can now be wrong!
 		updateSettings(array('settings_updated' => time()));
 	}
@@ -928,6 +987,15 @@ function deleteBoards($boards_to_remove, $moveChildrenTo = null)
 		)
 	);
 
+	// Delete this board's moderator groups.
+	$smcFunc['db_query']('', '
+		DELETE FROM {db_prefix}moderator_groups
+		WHERE id_board IN ({array_int:boards_to_remove})',
+		array(
+			'boards_to_remove' => $boards_to_remove,
+		)
+	);
+
 	// Delete any extra events in the calendar.
 	$smcFunc['db_query']('', '
 		DELETE FROM {db_prefix}calendar

+ 54 - 0
Sources/Subs-Editor.php

@@ -2164,6 +2164,7 @@ function AutoSuggestHandler($checkRegistered = null)
 	// These are all registered types.
 	$searchTypes = array(
 		'member' => 'Member',
+		'membergroups' => 'MemberGroups',
 		'versions' => 'SMFVersions',
 	);
 
@@ -2234,6 +2235,59 @@ function AutoSuggest_Search_Member()
 	return $xml_data;
 }
 
+/**
+ * Search for a membergroup by name
+ *
+ * @return string
+ */
+function AutoSuggest_Search_MemberGroup()
+{
+	global $txt, $smcFunc, $context;
+
+	$_REQUEST['search'] = trim($smcFunc['strtolower']($_REQUEST['search'])) . '*';
+	$_REQUEST['search'] = strtr($_REQUEST['search'], array('%' => '\%', '_' => '\_', '*' => '%', '?' => '_', '&#038;' => '&amp;'));
+
+	// Find the group.
+	// Only return groups which are not post-based, not "Hidden", not the "Moderators" group and not "Protected"
+	$request = $smcFunc['db_query']('', '
+		SELECT id_group, group_name
+		FROM {db_prefix}membergroups
+		WHERE group_name LIKE {string:search}
+			AND min_posts = {int:min_posts}
+			AND group_type != {int:is_protected}
+			AND id_group != {int:mod_group}
+			AND hidden != {int:hidden}
+		',
+		array(
+			'min_posts' => -1,
+			'is_protected' => 1,
+			'mod_group' => 3,
+			'hidden' => 2,
+			'search' => $_REQUEST['search'],
+		)
+	);
+	$xml_data = array(
+		'items' => array(
+			'identifier' => 'item',
+			'children' => array(),
+		),
+	);
+	while ($row = $smcFunc['db_fetch_assoc']($request))
+	{
+		$row['group_name'] = strtr($row['group_name'], array('&amp;' => '&#038;', '&lt;' => '&#060;', '&gt;' => '&#062;', '&quot;' => '&#034;'));
+
+		$xml_data['items']['children'][] = array(
+			'attributes' => array(
+				'id' => $row['id_group'],
+			),
+			'value' => $row['group_name'],
+		);
+	}
+	$smcFunc['db_free_result']($request);
+
+	return $xml_data;
+}
+
 /**
  * Provides a list of possible SMF versions to use in emulation
  *

+ 50 - 3
Sources/Subs-Members.php

@@ -1030,6 +1030,49 @@ function groupsAllowedTo($permission, $board_id = null)
 		while ($row = $smcFunc['db_fetch_assoc']($request))
 			$member_groups[$row['add_deny'] === '1' ? 'allowed' : 'denied'][] = $row['id_group'];
 		$smcFunc['db_free_result']($request);
+		
+		$moderator_groups = array();
+
+		// "Inherit" any moderator permissions as needed
+		if (isset($board_info['moderator_groups']))
+		{
+			$moderator_groups = array_keys($board_info['moderator_groups']);
+		}
+		elseif ($board_id !== 0)
+		{
+			// Get the groups that can moderate this board
+			$request = $smcFunc['db_query']('', '
+				SELECT id_group
+				FROM {db_prefix}moderator_groups
+				WHERE id_board = {int:board_id}',
+				array(
+					'board_id' => $board_id,
+				)
+			);
+			
+			while ($row = $smcFunc['db_fetch_assoc']($request))
+			{
+				$moderator_groups[] = $row['id_group'];
+			}
+		}
+		
+		$smcFunc['db_free_result']($request);
+		
+		// "Inherit" any additional permissions from the "Moderators" group		
+		foreach ($moderator_groups as $mod_group)
+		{
+			// If they're not specifically allowed, but the moderator group is, then allow it
+			if (in_array(3, $member_groups['allowed']) && !in_array($mod_group, $member_groups['allowed']))
+			{
+				$member_groups['allowed'][] = $mod_group;
+			}
+			
+			// They're not denied, but the moderator group is, so deny it
+			if (in_array(3, $member_groups['denied']) && !in_array($mod_group, $member_groups['denied']))
+			{
+				$member_groups['denied'][] = $mod_group;
+			}
+		}
 	}
 
 	// Denied is never allowed.
@@ -1054,6 +1097,8 @@ function membersAllowedTo($permission, $board_id = null)
 	global $smcFunc;
 
 	$member_groups = groupsAllowedTo($permission, $board_id);
+	
+	$all_groups = $member_groups;
 
 	$include_moderators = in_array(3, $member_groups['allowed']) && $board_id !== null;
 	$member_groups['allowed'] = array_diff($member_groups['allowed'], array(3));
@@ -1064,12 +1109,14 @@ function membersAllowedTo($permission, $board_id = null)
 	$request = $smcFunc['db_query']('', '
 		SELECT mem.id_member
 		FROM {db_prefix}members AS mem' . ($include_moderators || $exclude_moderators ? '
-			LEFT JOIN {db_prefix}moderators AS mods ON (mods.id_member = mem.id_member AND mods.id_board = {int:board_id})' : '') . '
-		WHERE (' . ($include_moderators ? 'mods.id_member IS NOT NULL OR ' : '') . 'mem.id_group IN ({array_int:member_groups_allowed}) OR FIND_IN_SET({raw:member_group_allowed_implode}, mem.additional_groups) != 0 OR mem.id_post_group IN ({array_int:member_groups_allowed}))' . (empty($member_groups['denied']) ? '' : '
-			AND NOT (' . ($exclude_moderators ? 'mods.id_member IS NOT NULL OR ' : '') . 'mem.id_group IN ({array_int:member_groups_denied}) OR FIND_IN_SET({raw:member_group_denied_implode}, mem.additional_groups) != 0 OR mem.id_post_group IN ({array_int:member_groups_denied}))'),
+			LEFT JOIN {db_prefix}moderators AS mods ON (mods.id_member = mem.id_member AND mods.id_board = {int:board_id})
+			LEFT JOIN {db_prefix}moderator_groups AS modgs ON (modgs.id_group IN ({array_int:all_member_groups}))' : '') . '
+		WHERE (' . ($include_moderators ? 'mods.id_member IS NOT NULL OR modgs.id_group IS NOT NULL OR ' : '') . 'mem.id_group IN ({array_int:member_groups_allowed}) OR FIND_IN_SET({raw:member_group_allowed_implode}, mem.additional_groups) != 0 OR mem.id_post_group IN ({array_int:member_groups_allowed}))' . (empty($member_groups['denied']) ? '' : '
+			AND NOT (' . ($exclude_moderators ? 'mods.id_member IS NOT NULL OR modgs.id_group IS NOT NULL OR ' : '') . 'mem.id_group IN ({array_int:member_groups_denied}) OR FIND_IN_SET({raw:member_group_denied_implode}, mem.additional_groups) != 0 OR mem.id_post_group IN ({array_int:member_groups_denied}))'),
 		array(
 			'member_groups_allowed' => $member_groups['allowed'],
 			'member_groups_denied' => $member_groups['denied'],
+			'all_member_groups' => $all_groups,
 			'board_id' => $board_id,
 			'member_group_allowed_implode' => implode(', mem.additional_groups) != 0 OR FIND_IN_SET(', $member_groups['allowed']),
 			'member_group_denied_implode' => implode(', mem.additional_groups) != 0 OR FIND_IN_SET(', $member_groups['denied']),

+ 33 - 0
Themes/default/ManageBoards.template.php

@@ -428,6 +428,14 @@ function template_modify_board()
 							<input type="text" name="moderators" id="moderators" value="', $context['board']['moderator_list'], '" size="30" class="input_text" />
 							<div id="moderator_container"></div>
 						</dd>
+						<dt>
+							<strong>', $txt['mboards_moderator_groups'], ':</strong><br />
+							<span class="smalltext">', $txt['mboards_moderator_groups_desc'], '</span><br />
+						</dt>
+						<dd>
+							<input type="text" name="moderator_groups" id="moderator_groups" value="', $context['board']['moderator_group_list'], '" size="30" class="input_text" />
+							<div id="moderator_group_container"></div>
+						</dd>
 					</dl>
 					<script type="text/javascript"><!-- // --><![CDATA[
 						$(document).ready(function () {
@@ -594,6 +602,31 @@ function template_modify_board()
 	echo '
 		]
 	});
+	
+	var oModeratorGroupSuggest = new smc_AutoSuggest({
+		sSelf: \'oModeratorGroupSuggest\',
+		sSessionId: smf_session_id,
+		sSessionVar: smf_session_var,
+		sSuggestId: \'moderator_groups\',
+		sControlId: \'moderator_groups\',
+		sSearchType: \'membergroups\',
+		bItemList: true,
+		sPostName: \'moderator_group_list\',
+		sURLMask: \'action=groups;sa=members;group=%item_id%\',
+		sTextDeleteItem: \'', $txt['autosuggest_delete_item'], '\',
+		sItemListContainerId: \'moderator_group_container\',
+		aListItems: [';
+		
+	foreach ($context['board']['moderator_groups'] as $id_group => $group_name)
+		echo '
+					{
+						sItemId: ', JavaScriptEscape($id_group), ',
+						sItemName: ', JavaScriptEscape($group_name), '
+					}', $id_group == $context['board']['last_moderator_group_id'] ? '' : ',';
+
+		echo '
+			]
+		});
 // ]]></script>';
 
 	// Javascript for deciding what to show.

+ 1 - 1
Themes/default/languages/Help.english.php

@@ -507,7 +507,7 @@ $helptxt['permissions_postgroups'] = 'Enabling permissions for post count based
 $helptxt['membergroup_guests'] = 'The Guests membergroup are all users that are not logged in.';
 $helptxt['membergroup_regular_members'] = 'The Regular Members are all members that are logged in, but that have no primary membergroup assigned.';
 $helptxt['membergroup_administrator'] = 'The administrator can, per definition, do anything and see any board. There are no permission settings for the administrator.';
-$helptxt['membergroup_moderator'] = 'The Moderator membergroup is a special membergroup. Permissions and settings assigned to this group apply to moderators but only <em>on the boards they moderate</em>. Outside these boards they\'re just like any other member.';
+$helptxt['membergroup_moderator'] = 'The Moderator membergroup is a special membergroup. Permissions and settings assigned to this group apply to moderators but only <em>on the boards they moderate</em>. Outside these boards they\'re just like any other member. Note that permissions for this group also apply to any group assigned to moderate a board.';
 $helptxt['membergroups'] = 'In SMF there are two types of groups that your members can be part of. These are:
 	<ul class="normallist">
 		<li><strong>Regular Groups:</strong> A regular group is a group to which members are not automatically put into. To assign a member to be in a group simply go to their profile and click &quot;Account Settings&quot;. From here you can assign them any number of regular groups to which they will be part.</li>

+ 2 - 0
Themes/default/languages/ManageBoards.english.php

@@ -53,6 +53,8 @@ $txt['mboards_groups_regular_members'] = 'This group contains all members that h
 $txt['mboards_groups_post_group'] = 'This group is a post count based group.';
 $txt['mboards_moderators'] = 'Moderators';
 $txt['mboards_moderators_desc'] = 'Additional members to have moderation privileges on this board.  Note that administrators don\'t have to be listed here.';
+$txt['mboards_moderator_groups'] = 'Moderator Groups';
+$txt['mboards_moderator_groups_desc'] = 'Groups whose members have moderation priveleges on this board. Note that this is limited to groups which are not post-based, not "hidden" and not "protected".';
 $txt['mboards_count_posts'] = 'Count Posts';
 $txt['mboards_count_posts_desc'] = 'Makes new replies and topics raise members\' post counts.';
 $txt['mboards_unchanged'] = 'Unchanged';

+ 1 - 0
Themes/default/languages/Reports.english.php

@@ -89,6 +89,7 @@ $txt['board_theme'] = 'Board Theme';
 $txt['board_override_theme'] = 'Force Board Theme';
 $txt['board_profile'] = 'Permissions Profile';
 $txt['board_moderators'] = 'Moderators';
+$txt['board_moderatr_groups'] = 'Moderator Groups';
 $txt['board_groups'] = 'Groups with Access';
 $txt['board_disallowed_groups'] = 'Groups with Access Denied';
 

+ 10 - 0
other/install_2-1_mysql.sql

@@ -1403,6 +1403,16 @@ CREATE TABLE {$db_prefix}moderators (
   PRIMARY KEY (id_board, id_member)
 ) ENGINE=MyISAM;
 
+#
+# Table structure for table `moderator_groups`
+#
+
+CREATE TABLE {$db_prefix}moderator_groups (
+  id_board smallint(5) unsigned NOT NULL default '0',
+  id_group smallint(5) unsigned NOT NULL default '0',
+  PRIMARY KEY (id_board, id_group) 
+) ENGINE=MyISAM;
+
 #
 # Table structure for table `openid_assoc`
 #

+ 10 - 0
other/install_2-1_postgresql.sql

@@ -1837,6 +1837,16 @@ CREATE TABLE {$db_prefix}moderators (
   PRIMARY KEY (id_board, id_member)
 );
 
+#
+# Table structure for table `moderator_groups`
+#
+
+CREATE TABLE {$db_prefix}moderator_groups (
+  id_board smallint NOT NULL default '0',
+  id_group smallint NOT NULL default '0',
+  PRIMARY KEY (id_board, id_group) 
+);
+
 #
 # Table structure for table `openid_assoc`
 #

+ 10 - 0
other/install_2-1_sqlite.sql

@@ -1523,6 +1523,16 @@ CREATE TABLE {$db_prefix}moderators (
   PRIMARY KEY (id_board, id_member)
 );
 
+#
+# Table structure for table `moderator_groups`
+#
+
+CREATE TABLE {$db_prefix}moderator_groups (
+  id_board smallint NOT NULL default '0',
+  id_group smallint NOT NULL default '0',
+  PRIMARY KEY (id_board, id_group) 
+);
+
 #
 # Table structure for table `openid_assoc`
 #

+ 10 - 0
other/upgrade_2-1_mysql.sql

@@ -288,3 +288,13 @@ if (@$modSettings['smfVersion'] < '2.1')
 }
 ---}
 ---#
+
+/******************************************************************************/
+--- Adding support for group-based board moderation
+/******************************************************************************/
+---# Creating moderator_groups table
+CREATE TABLE IF NOT EXISTS {$db_prefix}moderator_groups (
+  id_board smallint(5) unsigned NOT NULL default '0',
+  id_group smallint(5) unsigned NOT NULL default '0',
+  PRIMARY KEY (id_board, id_group)
+) ENGINE=MyISAM;

+ 10 - 0
other/upgrade_2-1_postgresql.sql

@@ -342,3 +342,13 @@ if (@$modSettings['smfVersion'] < '2.1')
 }
 ---}
 ---#
+
+/******************************************************************************/
+--- Adding support for group-based board moderation
+/******************************************************************************/
+---# Creating moderator_groups table
+CREATE TABLE IF NOT EXISTS {$db_prefix}moderator_groups (
+  id_board smallint NOT NULL default '0',
+  id_group smallint NOT NULL default '0',
+  PRIMARY KEY (id_board, id_group)
+);

+ 10 - 0
other/upgrade_2-1_sqlite.sql

@@ -325,3 +325,13 @@ if (@$modSettings['smfVersion'] < '2.1')
 }
 ---}
 ---#
+
+/******************************************************************************/
+--- Adding support for group-based board moderation
+/******************************************************************************/
+---# Creating moderator_groups table
+CREATE TABLE IF NOT EXISTS {$db_prefix}moderator_groups (
+  id_board smallint NOT NULL default '0',
+  id_group smallint NOT NULL default '0',
+  PRIMARY KEY (id_board, id_group)
+);