'. t('The project module makes special use of the taxonomy (category) system. A special vocabulary, %vocabulary_name, has been created automatically.', array('%vocabulary_name' => $vocabulary->name)) .'

'; $text .= '

'. t('To take full advantage of project categorization, add at least two levels of terms to this vocabulary. The first level will be the basic project types, e.g., "Modules", "Themes", "Translations".') .'

'; $text .= '

'. t('Subterms of each of these types will be the categories that users can select to classify the projects. For example, "Modules" might have sub-terms including "Mail" and "XML".') .'

'; if ($vocab_link) { $text .= '

'. t('Use the vocabulary administration page to view and add terms.', array('@taxonomy-admin' => url('admin/content/taxonomy/'. $vid))) .'

'; } return $text; } /** * Implementation of hook_block(). */ function project_block($op = 'list', $delta = 0, $edit = array()) { if ($op == 'list') { // Note: We can get by with using BLOCK_CACHE_PER_ROLE below because // block caching is disabled when node access control modules are in use. $blocks['project_navigation_select'] = array( 'info' => t('Project navigation (drop-down select)'), 'cache' => BLOCK_CACHE_PER_ROLE, ); $blocks['project_navigation_text'] = array( 'info' => t('Project navigation (text field)'), 'cache' => BLOCK_CACHE_PER_ROLE, ); if (module_exists('search')) { $blocks['project_search'] = array( 'info' => t('Project search'), 'cache' => BLOCK_CACHE_PER_ROLE, ); } module_load_include('inc', 'project', 'project'); foreach (project_get_project_link_info() as $key => $links) { if (isset($links['type']) && $links['type'] == 'block') { $blocks[$key] = array( 'info' => t('Project: @section', array('@section' => $links['name'])), 'cache' => BLOCK_CACHE_PER_ROLE | BLOCK_CACHE_PER_PAGE, ); } } return $blocks; } elseif ($op == 'view') { switch ($delta) { case 'project_navigation_select': return array( 'subject' => t('Project navigation'), 'content' => drupal_get_form('project_quick_navigate_form'), ); case 'project_navigation_text': return array( 'subject' => t('Project navigation'), 'content' => drupal_get_form('project_quick_navigate_title_form'), ); case 'project_search': if (user_access('search content')) { return array( 'subject' => t('Search projects'), 'content' => drupal_get_form('project_search_block_form', 'project_project'), ); } break; } if (($node = project_get_project_from_menu()) && node_access('view', $node)) { module_load_include('inc', 'project', 'project'); foreach (project_get_project_link_info($node) as $key => $links) { if ($delta == $key && isset($links['type']) && $links['type'] == 'block' && !empty($links['links'])) { return array( 'subject' => $links['name'], 'content' => theme('item_list', $links['links']), ); } } } } elseif ($op == 'configure' && $delta == 1) { $form = array(); $form['help_text'] = array( '#type' => 'textfield', '#title' => t('Help text'), '#description' => t('Enter optional help text to display in the block.'), '#default_value' => variable_get('project_search_block_help_text', ''), ); return $form; } elseif ($op == 'save' && $delta == 1) { variable_set('project_search_block_help_text', $edit['help_text']); } } function project_search_block_form($form_state, $node_type) { $form = search_box($form_state, 'project_search_block_form'); $form['node_type'] = array( '#type' => 'value', '#value' => $node_type, ); $form['#base'] = 'project_search_block_form'; $help_text = variable_get('project_search_block_help_text', ''); if (!empty($help_text)) { $element = 'project_search_block_form'; $form[$element]['#description'] = check_plain($help_text); } $form['#submit'][] = 'project_search_block_form_submit'; return $form; } function project_search_block_form_submit($form, &$form_state) { $form_state['redirect'] = 'search/node/type:'. $form_state['values']['node_type'] .' '. trim($form_state['values'][$form_state['values']['form_id']]); } function project_node_info() { return array( 'project_project' => array( 'name' => t('Project'), 'module' => 'project_project', 'description' => t('A project is something a group is working on. It can optionally have issue tracking, integration with revision control systems, releases, and so on.'), ), ); } function project_perm() { $perms = array( 'administer projects', 'create sandbox projects', 'create full projects', 'access projects', 'access own projects', 'delete any projects', 'delete own projects', 'browse project listings', ); return $perms; } /** * Implementation of hook_db_rewrite_sql(). * * Both project (and, if enabled, project_issue) provide some permissions that * restrict access to viewing projects (and issues). For these permissions to * be globally honored by the system, db_rewrite_sql() has to check what * permissions the current user has and restrict query results accordingly. * * If a user has 'access projects' and 'access project issues', they have full * access, so there's nothing to re-write. If they have 'access own projects' * and/or 'access own project issues', the resulting query should JOIN on * {node} and ensure that the node type is not project_project or * project_issue, or that the owner of the node is the current user. If they * do not have any access to either, then we can just restrict the query based * on the {node}.type column. * * @see project_perm() * @see project_issue_perm() * @see project_find_alias() */ function project_db_rewrite_sql($query, $primary_table, $primary_field) { if ($primary_field == 'nid') { $return = array(); $access_projects = user_access('access projects'); $admin_projects = user_access('administer projects'); if (module_exists('project_issue')) { $access_issues = user_access('access project issues'); $access_own_issues = user_access('access own project issues'); } else { $access_issues = TRUE; $access_own_issues = TRUE; } if ($admin_projects || ($access_projects && $access_issues)) { // User has full access, nothing to re-write. return; } else { // We have to make sure {node} is in the query and know the alias. if ($primary_table == 'n' || $primary_table == 'node') { // Great, it's the primary table and we already know the alias. $alias = $primary_table; } else { // Look for {node} in the query. if (!($alias = project_find_alias($query, 'node'))) { // Not already in the query, JOIN it. $return['join'] = "INNER JOIN {node} pn ON pn.nid = ". $primary_table .'.nid'; $alias = 'pn'; } } } // At this point, we know we have to restrict something, and we know what // the {node} table's alias is in the query. $where = array(); global $user; $uid = $user->uid; // Some node types will be restriced by our query, but we want to allow // every other node type. We keep track of the types we're handling, and // at the end we'll add a clause to ignore/allow all other types. $restricted_types = array(); if (!$access_projects) { $restricted_types[] = "'project_project'"; if (user_access('access own projects')) { $where[] = "($alias.type = 'project_project' AND $alias.uid = $uid)"; } } if (module_exists('project_issue') && !$access_issues) { $restricted_types[] = "'project_issue'"; if ($access_own_issues) { $where[] = "($alias.type = 'project_issue' AND $alias.uid = $uid)"; } } // If the type is not one of the restricted project* ones, allow access. $where[] = "($alias.type NOT IN (". implode(', ', $restricted_types) ."))"; // Now that we have all our WHERE clauses, we just OR them all together. $return['where'] = implode(' OR ', $where); return $return; } } /** * Find the table alias for a table in a query. * * @param $query * The query to examine for the alias. * @param * $table The table to examine for the alias. * @return * The alias if it exists, or the name of the table if it's present but not * aliased, or FALSE if the table is not present. * * @see project_db_rewrite_sql() */ function project_find_alias($query, $table) { // See if {$table} is already in the query. $match = array(); // This regexp handles many cases: // - {$table} can be either in FROM or in a JOIN clause // - Query might end immediately after {$table} // - Might not have a table alias for {$table} $pattern = "@.*(FROM|JOIN)\s+\{$table\}\s*(\S+)?@"; if (preg_match($pattern, $query, $match)) { $keywords = '@(ON|INNER|LEFT|RIGHT|WHERE|ORDER|GROUP|HAVING|LIMIT)@'; if (!isset($match[2]) || preg_match($keywords, $match[2])) { // No alias found, just use $table. $alias = $table; } else { // Alias found. $alias = $match[2]; } } else { // Table not in query. $alias = FALSE; } return $alias; } /** * Callback for the main settings page. */ function project_settings_form() { $form = array(); if (module_exists('path')) { $form['project_enable_alias'] = array( '#type' => 'checkbox', '#title' => t('Enable auto-generated project aliases'), '#default_value' => variable_get('project_enable_alias', TRUE), '#description' => t('If checked, project module will automatically generate path aliases (eg. /project/<shortname>/) for each project. Uncheck if you want to use another module, such as pathauto, to generate aliases.') ); } else { $form['project_enable_alias'] = array( '#type' => 'markup', '#value' => t('If you enable the Path module from Drupal core, you can control if the Project module will automatically generate path aliases (eg. /project/<shortname>/) for each project.') ); variable_set('project_enable_alias', FALSE); } $vocabs = project_get_related_tids_map(); $project_types = project_get_project_types(); $project_type_options[0] = t('- None -'); foreach ($project_types as $tid => $term) { $project_type_options[$tid] = $term->name; } foreach ($vocabs as $vid => $vocab) { $form['project_type_associated_tid_' . $vid] = array( '#title' => t('Associated Project type taxonomy term for @vocab', array('@vocab' => $vocab->name)), '#type' => 'select', '#options' => $project_type_options, '#default_value' => variable_get('project_type_associated_tid_' . $vid, NULL), '#description' => t('If this vocabulary is specific to a project type, set the term here to allow Project to automatically show and hide the form as necessary.'), ); } if (module_exists('project_solr') && module_exists('project_release')) { $form['project_solr_project_release_api_tids_alias'] = array( '#title' => t('Alias for the Project Release API tid for use with Solr'), '#type' => 'textfield', '#default_value' => variable_get('project_solr_project_release_api_tids_alias', 'drupal_core'), ); } $form['sandbox'] = array( '#type' => 'fieldset', '#title' => t('Sandbox settings'), '#collapsible' => TRUE, ); $form['sandbox']['project_sandbox_numeric_shortname'] = array( '#title' => t('Auto generate short name for sandboxes'), '#type' => 'checkbox', '#default_value' => variable_get('project_sandbox_numeric_shortname', FALSE), '#description' => t('If checked, projects marked as sandboxes will be have their shortname automatically generated using a unique numeric identifier.'), ); return system_settings_form($form); } /** * Helper function for getting vocabularies related to top-level project terms. * * @param $active * Boolean. TRUE to return only vocabularies actively associated with a term. * Defaults to FALSE. * @return * An array of vocabulary objects, keyed on vocabulary ID. */ function project_get_related_tids_map($active = FALSE) { $related_vocabs = array(); $project_vid = _project_get_vid(); $vocabs = taxonomy_get_vocabularies('project_project'); foreach ($vocabs as $vid => $vocab) { // Skip freetagging vocabularies, and the hard-coded project type // vocabulary, since things will go crazy if you set this for itself. if (!$vocab->tags && $vid != $project_vid && (!$active || variable_get('project_type_associated_tid_' . $vid, NULL))) { $related_vocabs[$vid] = $vocab; } } return $related_vocabs; } /** * Returns the vocabulary id for projects. */ function _project_get_vid() { $vid = variable_get('project_vocabulary', ''); if (empty($vid)) { // Check to see if a project module vocabulary exists. $vid = db_result(db_query("SELECT vid FROM {vocabulary} WHERE module='%s'", 'project')); if (!$vid) { $edit = array( 'name' => t('Project types'), 'multiple' => 1, 'hierarchy' => 1, 'relations' => 0, 'module' => 'project', 'nodes' => array('project_project' => 1), ); // If there is already a vocabulary assigned to 'project_project' nodes, use it. $vocabularies = taxonomy_get_vocabularies('project_project'); if (count($vocabularies)) { $vocabulary = reset($vocabularies); $edit['vid'] = $vocabulary->vid; } taxonomy_save_vocabulary($edit); $vid = $edit['vid']; } variable_set('project_vocabulary', $vid); } return $vid; } /** * Find the top-level terms in the project vocabulary (the project types). * * @return array * An associative array of top-level terms in the project vocabulary keyed * by term ID (tid), the values are the fully-loaded term objects. * * @see taxonomy_get_tree() */ function project_get_project_types() { static $project_types = array(); if (empty($project_types)) { $project_vid = _project_get_vid(); $tree = taxonomy_get_tree($project_vid, 0, -1, 1); foreach ($tree as $term) { $project_types[$term->tid] = $term; } } return $project_types; } /** * Implementation of hook_term_path(). * * @TODO: Modify this function to use the new project settings where the * user selects the views to use. */ function project_term_path($term) { // Make sure we have the entire term object. $term = taxonomy_get_term($term->tid); // The path must include the first-level term name for this term. $tree = taxonomy_get_tree(_project_get_vid()); $parents = taxonomy_get_parents_all($term->tid); foreach($parents as $parent) { $ancestors[] = $parent->tid; } foreach ($tree as $t) { if (in_array($t->tid, $ancestors) && $t->depth == 0) { $termname = $t->name; if ($term->name == $termname) { return 'project/' . drupal_strtolower($termname); } break; } } // TODO: when the project browsing views work, if there's a direct URL to // link to a project category view, we should add that here. // Also, note that in the project_solr case, we'd like to link to, // e.g. "project/modules?filter=tid:12" but it's apparently impossible to // return url() for this since things get double encoded or something. // So, for now, we punt and always link the subterms to the taxonomy page. return 'taxonomy/term/'. $term->tid; } /** * Implementation of hook_taxonomy(). */ function project_taxonomy($op, $type, $array = NULL) { if ($op == 'delete' && $type == 'vocabulary') { if ($array['vid'] == _project_get_vid()) { variable_del('project_vocabulary'); } variable_del('project_type_associated_tid_' . $array['vid']); } } /** * Determine if the currently logged in user could have access to any project_project nodes. */ function project_project_access_any() { return user_access('access projects') || user_access('access own projects') || user_access('administer projects'); } /** * Implement hook_menu(). */ function project_menu() { $items = array(); // @TODO: The project type and browsing views are currently set to require // 'access projects' permission. However, we really want them to require at // least one of 'access projects', 'access own projects', or 'administer // projects'. At this point I'm not sure what the best way to do this is, // but we need to figure this out to keep the same functionality. $items['project/autocomplete/project'] = array( 'title' => 'Autocomplete project', 'page callback' => 'project_autocomplete_project', 'access callback' => 'project_project_access_any', 'type' => MENU_CALLBACK, ); $items['project/autocomplete/project-user/%'] = array( 'title' => 'Autocomplete project', 'page callback' => 'project_autocomplete_project_user', 'page arguments' => array(3, 4), 'access callback' => 'project_project_access_any', 'type' => MENU_CALLBACK, ); // CVS messages: $items['project/cvs'] = array( 'title' => 'CVS', 'page callback' => 'project_cvs', 'access callback' => 'project_project_access_any', 'type' => MENU_CALLBACK, ); // Administration pages $items['admin/project'] = array( 'title' => 'Project administration', 'description' => 'Administrative interface for project management and related modules.', 'page callback' => 'system_admin_menu_block_page', 'access arguments' => array('administer projects'), 'file' => 'system.admin.inc', 'file path' => drupal_get_path('module', 'system'), 'type' => MENU_NORMAL_ITEM, ); $items['admin/project/project-settings'] = array( 'title' => 'Project settings', 'description' => 'Configure settings for the Project module.', 'page callback' => 'drupal_get_form', 'page arguments' => array('project_settings_form'), 'access arguments' => array('administer projects'), 'type' => MENU_NORMAL_ITEM, ); $items['node/%project_node/maintainers'] = array( 'title' => 'Maintainers', 'page callback' => 'drupal_get_form', 'page arguments' => array('project_maintainers_form', 1), 'access callback' => 'project_user_access', 'access arguments' => array(1, 'administer maintainers'), 'file' => 'includes/project_maintainers.inc', 'type' => MENU_LOCAL_TASK, 'weight' => 4, ); $items['node/%project_node/maintainers/delete/%user'] = array( 'page callback' => 'drupal_get_form', 'page arguments' => array('project_maintainer_delete_confirm', 1, 4), 'access callback' => 'project_user_access', 'access arguments' => array(1, 'administer maintainers'), 'file' => 'includes/project_maintainers.inc', 'type' => MENU_CALLBACK, ); $items['node/%project_edit_project/edit/project'] = array( 'title' => 'Project', 'page callback' => 'node_page', 'page arguments' => array(1), 'access callback' => 'node_access', 'access arguments' => array('update', 1), 'weight' => -5, 'type' => MENU_DEFAULT_LOCAL_TASK, ); $items['node/%project_edit_project/edit/promote'] = array( 'title' => 'Promote', 'page callback' => 'project_promote_project_page', 'page arguments' => array(1), 'access callback' => 'project_promote_project_access', 'access arguments' => array(1), 'file' => 'project.inc', 'type' => MENU_LOCAL_TASK, ); $items['node/%project_edit_project/edit/promote/confirm'] = array( 'title' => 'Promote', 'page callback' => 'drupal_get_form', 'page arguments' => array('project_promote_project_confirm_form', 1), 'access callback' => 'project_promote_project_access', 'access arguments' => array(1), 'file' => 'project.inc', 'type' => MENU_CALLBACK, ); return $items; } /** * Menu loader callback to load a project node. * * @param $arg * The menu argument to attempt to load a project from. Can be either a * numeric node ID (nid), or a string project short name (uri). * * @return * The loaded node object if the argument was a valid project, FALSE if not. */ function project_node_load($arg) { if (is_numeric($arg)) { $nid = $arg; } else { $nid = db_result(db_query(db_rewrite_sql("SELECT p.nid FROM {project_projects} p WHERE p.uri = '%s'", 'p'), $arg)); } $node = node_load($nid); if (!isset($node->type) || $node->type != 'project_project') { return FALSE; } return $node; } /** * Menu loader callback to load a project type. * * @param $arg * The menu argument to attempt to load a project type from. * * @return * The taxonomy term representing the project type, or FALSE. */ function project_type_load($arg) { // Get all of the top-level terms in the project type vocabulary. $project_types = project_get_project_types(); foreach ($project_types as $term) { if (strcasecmp($term->name, $arg) == 0) { return $term; } } return FALSE; } /** * Menu loader callback to load a project node if the edit tab needs subtabs. * * Load a project_project node if the given nid is a project_project node and * if the user has permission to edit the project and if either the * project_issue or project_release module exists. */ function project_edit_project_load($nid) { if (!module_exists('project_issue') && !module_exists('project_release')) { return FALSE; } return project_node_load($nid); } /** * Menu access callback for the promote tab at node/%project_node/edit/promote. * * @param $node * The project object to check access against. */ function project_promote_project_access($node) { // This callback is only valid for projects that are sandboxes. Further, // restrict to users with either 'administer projects' or users that have // access to edit the project and 'create full projects' permission. return ($node->project['sandbox'] && (user_access('administer projects') || (project_user_access($node, 'edit project') && user_access('create full projects')))); } /** * See if the current user has the given permission on a given project. * * @param $project * The project to check access against. Can be either a numeric node ID * (nid) or a fully-loaded $node object. * @param $permission * The string representing the permission to check access for. */ function project_user_access($project, $permission) { global $user; if (empty($user->uid)) { return FALSE; } $project_obj = is_numeric($project) ? node_load($project) : $project; if (!isset($project_obj) || (isset($project_obj->type) && $project_obj->type != 'project_project')) { return FALSE; } // If the user has the site-wide admin permission, always grant access. if (user_access('administer projects')) { return TRUE; } if (user_access('create sandbox projects') || user_access('create full projects')) { // Project owners are treated as super users and can always access. if ($user->uid == $project_obj->uid) { return TRUE; } // Otherwise, see if the user has the right permission for this project. return !empty($project_obj->project['maintainers'][$user->uid]['permissions'][$permission]); } // If we haven't granted access yet, deny it. return FALSE; } /** * Implementation of form_alter. This removes the work of * taxonomy.module's form_alter() so we can do our own taxonomy * selection. */ function project_form_alter(&$form, &$form_state, $form_id) { switch ($form_id) { case 'project_project_node_form': // Totally redo the form for the project vocabulary. // Use the top level terms as the project types (which get their own radio) // Use the subterms as a separate select/multiselect that appears when the // radio button is selected. $project_vid = _project_get_vid(); if (isset($form['taxonomy'][$project_vid])) { unset($form['taxonomy'][$project_vid]); } if (!empty($form['taxonomy'])) { $unhandled_vocabs = 0; foreach (element_children($form['taxonomy']) as $vid) { $associated_tid = variable_get('project_type_associated_tid_' . $vid, FALSE); if (is_numeric($vid) && $associated_tid) { $form['taxonomy'][$vid]['#attributes']['class'] = 'related-tid-' . $associated_tid; } else { $unhandled_vocabs++; } } if ($unhandled_vocabs <= 1) { // If there is at most one unhandled vocabulary, hide the fieldset. unset($form['taxonomy']['#type']); } } // In D6, FAPI changed in such a way that completely unsetting // $form['taxonomy'] causes warnings and errors when the project node is // previewed. In order to prevent these problems, we need to add a // submit handler to the preview button, and this handler needs to put // the taxonomy terms back into $node->taxonomy like they would be if // project wasn't replacing the usual taxonomy selection form elements // with its own elements. if (isset($form['buttons']['preview'])) { $form['buttons']['preview']['#submit'][] = 'project_project_form_preview_submit'; } // If the form has an element for specifying a URL alias, we want // to alter it, since we're just going to automatically override // the specified value. if (variable_get('project_enable_alias', TRUE) && isset($form['path'])) { $url_alias = $form['path']['path']['#default_value']; if (empty($url_alias)) { unset($form['path']); } else { unset($form['path']['path']); $form['path']['value'] = array( '#prefix' => '
', '#value' => t('Automatically generated path alias: %url', array('%url' => $url_alias)), '#suffix' => '
', ); } } break; case 'taxonomy_form_vocabulary': // Add a validate handler to the vocabulary form if the // Project type vocabulary term is being edited so that its // possible to detect if this vocabulary was set to be // a free tagging vocabulary, and if so then throw an error. $project_type_vid = _project_get_vid(); if (isset($project_type_vid) && !empty($form['vid']['#value']) && $form['vid']['#value'] == $project_type_vid) { $form['#validate'][] = 'project_project_type_vocabulary_validate'; } // Do the same for any vocabularies with a helpful link. if (variable_get('project_type_associated_tid_' . $form['vid']['#value'], FALSE)) { $form['#validate'][] = 'project_associated_vocabulary_validate'; } break; } } /** * Implement hook_access(). */ function project_project_access($op, $node, $account) { switch ($op) { case 'view': // Since this function is shared for project_release nodes, we have to // be careful what node we pass to project_user_access(). if ($node->type == 'project_release') { $node = node_load($node->project_release['pid']); } if (project_user_access($node, 'edit project')) { return TRUE; } if (!user_access('access projects')) { return FALSE; } break; case 'create': $project_perms = (user_access('create sandbox projects') || user_access('create full projects')); // Only check for vc perms if versioncontrol_project is enabled. $vc_perms = module_exists('versioncontrol_project') ? user_access('use version control systems') : TRUE; if ($account->uid && $project_perms && $vc_perms) { // Since this CVS access checking is non-standard, we need to // special-case uid 1 to always allow everything. if ($account->uid != 1 && module_exists('cvs') && variable_get('cvs_restrict_project_creation', 1)) { return db_result(db_query("SELECT uid FROM {cvs_accounts} WHERE uid = %d AND status = %d", $account->uid, CVS_APPROVED)) ? TRUE : FALSE; } else { return TRUE; } } break; case 'update': if (project_user_access($node, 'edit project')) { return TRUE; } break; case 'delete': if (user_access('delete any projects', $account) || (user_access('delete own projects', $account) && ($account->uid == $node->uid))) { return TRUE; } break; } } /** * Load all per-project permission information and return it. * * This invokes hook_project_permission_info() and * hook_project_permission_alter(), and caches the results in RAM. * * @param $project * A project object to pass to hook_project_permission_info(). * * @see hook_project_permission_info() * @see hook_project_permission_alter() * @see drupal_alter() */ function project_permission_load($project) { static $project_permissions = array(); if (empty($project_permissions[$project->nid])) { $permissions = module_invoke_all('project_permission_info', $project); drupal_alter('project_permission', $permissions, $project); $project_permissions[$project->nid] = $permissions; } return $project_permissions[$project->nid]; } /** * Implement hook_project_permission_info() */ function project_project_permission_info($project = NULL) { return array( 'edit project' => array( 'title' => t('Edit project'), 'description' => t('Allows a user to edit a project and modify its settings.'), ), 'administer maintainers' => array( 'title' => t('Administer maintainers'), 'description' => t('Allows a user to add and remove other project maintainers and to modify their permissions.'), ), ); } /** * Save the permissions associated with a maintainer for a given project. * * This creates a new maintainer record if none currently exists. Furthermore, * it invokes hook_project_maintainer_save() to give other modules a chance to * act on the fact that a maintainer is being saved. * * @param $nid * The Project NID to update the maintainer for. * @param $uid * The user ID of the maintainer to update. * @param array $permissions * Associative array of which project-level permissions the maintainer * should have. The keys are permission names, and the values are if the * permission should be granted or not. * * @see hook_project_maintainer_save() * @see hook_project_permission_info() */ function project_maintainer_save($nid, $uid, $permissions = array()) { // Try to update an existing record, if any. db_query("UPDATE {project_maintainer} SET edit_project = %d, administer_maintainers = %d WHERE nid = %d AND uid = %d", !empty($permissions['edit project']), !empty($permissions['administer maintainers']), $nid, $uid); if (!db_affected_rows()) { // Didn't update anything, add this as a new maintainer, instead. db_query("INSERT INTO {project_maintainer} (nid, uid, edit_project, administer_maintainers) VALUES (%d, %d, %d, %d)", $nid, $uid, !empty($permissions['edit project']), !empty($permissions['administer maintainers'])); } // Invoke hook_project_maintainer_save() to let other modules know this // maintainer is being saved so they can take any actions or record any // data they need to. module_invoke_all('project_maintainer_save', $nid, $uid, $permissions); } /** * Remove a maintainer from a given project. * * @param $nid * The Project NID to remove the maintainer from. * @param $uid * The user ID of the maintainer to remove. */ function project_maintainer_remove($nid, $uid) { db_query("DELETE FROM {project_maintainer} WHERE nid = %d and uid = %d", $nid, $uid); // Invoke hook_project_maintainer_remove() to let other modules know this // maintainer is being removed so they can take any actions or record any // data they need to. module_invoke_all('project_maintainer_remove', $nid, $uid); } /** * Load all the per-project maintainer info for a given project. * * @param $nid * Node ID of the project to load maintainer info about. * * @return * Array of maintainer info for the given project. * * @see hook_project_maintainer_project_load(). */ function project_maintainer_project_load($nid) { $maintainers = array(); // We don't want to load all the permissions here, just the ones that // Project itself is responsible for, so we use our implementation of the // hook, instead of the global load function. $project_perms = project_project_permission_info(); $query = db_query('SELECT u.name, pm.* FROM {project_maintainer} pm INNER JOIN {users} u ON pm.uid = u.uid WHERE pm.nid = %d ORDER BY u.name', $nid); while ($maintainer = db_fetch_object($query)) { $maintainers[$maintainer->uid]['name'] = $maintainer->name; foreach ($project_perms as $perm_name => $perm_info) { $db_field = str_replace(' ', '_', $perm_name); $maintainers[$maintainer->uid]['permissions'][$perm_name] = $maintainer->$db_field; } } // Invoke hook_project_maintainer_project_load(). We can't use // module_invoke_all() since we want a reference to the $maintainers array. foreach (module_implements('project_maintainer_project_load') as $module) { $function_name = $module . '_project_maintainer_project_load'; $function_name($nid, $maintainers); } return $maintainers; } /** * Implement hook_load(). */ function project_project_load($node) { $additions = db_fetch_array(db_query('SELECT * FROM {project_projects} WHERE nid = %d', $node->nid)); $project = new stdClass; $project->project = $additions; $project->project['maintainers'] = project_maintainer_project_load($node->nid); return $project; } /** * hook_nodeapi() implementation. This just decides what type of node * is being passed, and calls the appropriate type-specific hook. * * @see project_project_nodeapi(). */ function project_nodeapi(&$node, $op, $arg) { switch ($node->type) { case 'project_project': module_load_include('inc', 'project', 'project'); project_project_nodeapi($node, $op, $arg); break; } } /** * Build a SQL statment from a structured array. * * @param $sql_elements * An array with the following keys: * 'fields', 'from', 'joins', 'wheres', 'group_bys', 'order_bys', * 'parameters' * Each value is an array with the following keys: * 'prefix', 'glue', 'pieces' * @return * SQL string. * * @TODO This should move to project_usage.module */ function project_build_query($sql_elements) { $sql = ''; foreach ($sql_elements as $key => $sql_element) { if ($key != 'parameters' && count($sql_element['pieces'])) { $sql .= $sql_element['prefix'] . implode($sql_element['glue'], $sql_element['pieces']); } } return db_rewrite_sql($sql); } /** * Construct an empty query for project_build_query(). * * Set the default elements that will be used to construct the SQL statement. * * @TODO This should move to project_usage.module */ function project_empty_query() { return array( 'fields' => array( 'prefix' => 'SELECT ', 'glue' => ', ', 'pieces' => array(), ), 'from' => array( 'prefix' => ' FROM ', 'glue' => NULL, 'pieces' => array(''), ), 'joins' => array( 'prefix' => '', 'glue' => ' ', 'pieces' => array(), ), 'wheres' => array( 'prefix' => ' WHERE ', 'glue' => ' AND ', 'pieces' => array(), ), 'group_bys' => array( 'prefix' => ' GROUP BY ', 'glue' => ', ', 'pieces' => array(), ), 'order_bys' => array( 'prefix' => ' ORDER BY ', 'glue' => ', ', 'pieces' => array(), ), 'parameters' => array( 'prefix' => NULL, 'glue' => NULL, 'pieces' => array(), ) ); } /** * Helper function for grouping nodes by date. */ function _project_date($timestamp) { static $last; $date = format_date($timestamp, 'custom', 'F j, Y'); if ($date != $last) { $last = $date; return $date; } } /** * Returns an array of all projects on the system for use with select * form elements. The keys are the project nid, and the values are * the project names. If the project taxonomy is in use, the array * will be sorted into the project categories with appropriate headers * for each term. * * @param $project_urls * Reference to be filled with an array of project uri => id mappings. This * array is used by the project_issue search form code. * @param $issues * If TRUE, only projects with issue tracking enabled are returned. * For this option to do much good, the project_issue.module should be * enabled. * @param $key_prefix * Prefix to prepend to all keys in the returned array. * @param string $include_sandbox * Should sandboxes be included in the listing? Can be either 'no' (exclude * sandboxes from the list, the default), 'yes' (include sandboxes) , 'only' * (only show sandboxes). */ function project_projects_select_options(&$project_urls, $issues = TRUE, $key_prefix = NULL, $include_sandbox = 'no') { $projects = array(); $ISSUE_JOIN = ''; $ISSUE_WHERE = ''; switch ($include_sandbox) { case 'no': $SANDBOX_WHERE = 'AND p.sandbox = 0'; break; case 'only': $SANDBOX_WHERE = 'AND p.sandbox = 1'; break; default: // Otherwise, ignore sandboxes entirely. $SANDBOX_WHERE = ''; break; } $args_use_taxonomy = array(1, 0); // n.status, h.parent $args_no_taxonomy = array(1); // n.status if ($issues && module_exists('project_issue')) { $ISSUE_JOIN ='INNER JOIN {project_issue_projects} pip ON n.nid = pip.nid'; $ISSUE_WHERE = 'AND pip.issues = %d'; $args_use_taxonomy[] = 1; $args_no_taxonomy[] = 1; } if (project_use_taxonomy()) { $vid = _project_get_vid(); $args_use_taxonomy[] = $vid; $result = db_query(db_rewrite_sql("SELECT n.nid, n.title, d.name, p.uri FROM {project_projects} p INNER JOIN {node} n ON n.nid = p.nid $ISSUE_JOIN LEFT JOIN {term_node} t ON t.vid = n.vid INNER JOIN {term_data} d ON t.tid = d.tid INNER JOIN {term_hierarchy} h ON t.tid = h.tid WHERE n.status = %d AND h.parent = %d $SANDBOX_WHERE $ISSUE_WHERE AND d.vid = %d GROUP BY n.nid, n.title, d.name, p.uri, d.weight ORDER BY d.weight, n.title"), $args_use_taxonomy); while ($project = db_fetch_object($result)) { if (isset($project->name)) { if (!isset($projects[$project->name])) { $projects[$project->name] = array(); } $projects[$project->name][$key_prefix . $project->nid] = $project->title; } else { $projects[$key_prefix . $project->nid] = $project->title; } if (is_array($project_urls)) { $project_urls[$project->uri] = $project->nid; } } } else { $result = db_query(db_rewrite_sql("SELECT n.nid, p.uri, n.title FROM {project_projects} p INNER JOIN {node} n ON n.nid = p.nid $ISSUE_JOIN WHERE n.status = %d $SANDBOX_WHERE $ISSUE_WHERE ORDER BY n.title"), $args_no_taxonomy); while ($project = db_fetch_object($result)) { $projects[$key_prefix . $project->nid] = $project->title; if (is_array($project_urls)) { $project_urls[$project->uri] = $project->nid; } } } return $projects; } function project_quick_navigate_form() { $uris = NULL; $projects = array(t('- Select a project -')) + project_projects_select_options($uris, FALSE); $form = array(); $form['project_goto'] = array( '#type' => 'select', '#default_value' => '0', '#options' => $projects, '#attributes' => array('title' => t('Select a project to view')), ); $form['project'] = array( '#type' => 'submit', '#value' => t('View project'), '#submit' => array('project_quick_navigate_project_submit'), ); return $form; } function project_quick_navigate_form_validate($form, $form_state) { if (empty($form_state['values']['project_goto'])) { form_set_error('project_goto', t('You must select a project to navigate to.')); } } function project_quick_navigate_project_submit($form, &$form_state) { $form_state['redirect'] = 'node/'. $form_state['values']['project_goto']; } function project_quick_navigate_title_form() { $form = array(); $form['project_title'] = array( '#title' => t('Project name'), '#type' => 'textfield', '#size' => 20, '#autocomplete_path' => 'project/autocomplete/project', ); $form['project'] = array( '#type' => 'submit', '#value' => t('View project'), '#validate' => array('project_quick_navigate_title_project_validate'), '#submit' => array('project_quick_navigate_title_project_submit'), ); return $form; } function project_quick_navigate_title_project_validate($form, &$form_state) { if (empty($form_state['values']['project_title'])) { form_set_error('project_title', t('You must enter a project to navigate to.')); } else { $nid = db_result(db_query("SELECT nid FROM {node} WHERE title = '%s' AND type = '%s'", $form_state['values']['project_title'], 'project_project')); if (empty($nid)) { form_set_error('project_title', t('The name you entered (%title) is not a valid project.', array('%title' => $form_state['values']['project_title']))); } else { $form_state['nid'] = $nid; } } } function project_quick_navigate_title_project_submit($form, &$form_state) { $form_state['redirect'] = 'node/'. $form_state['nid']; } /** * Implementation of hook_views_api(). */ function project_views_api() { return array( 'api' => 2, 'path' => drupal_get_path('module', 'project') .'/views', ); } // Themables /** * Implementation of hook_theme(). */ function project_theme() { return array( 'project_term_list' => array( 'arguments' => array( 'terms' => NULL, 'path' => NULL, ), ), 'project_summary' => array( 'arguments' => array( 'project' => NULL, ), ), 'project_feed_icon' => array( 'arguments' => array( 'url' => NULL, 'title' => NULL, ), ), 'project_maintainers_form' => array( 'file' => 'includes/project_maintainers.inc', 'arguments' => array( 'form' => NULL, ), ), 'project_project_node_form_taxonomy' => array( 'file' => 'project.inc', 'arguments' => array( 'form' => NULL, ), ), 'project_views_project_sort_method_options_form' => array( 'file' => 'theme.inc', 'path' => drupal_get_path('module', 'project') .'/views/theme', 'arguments' => array( 'form' => NULL, ), ), 'project_type' => array( 'arguments' => array( 'term' => NULL, ), ), 'project_type_description' => array( 'arguments' => array( 'term' => NULL, ), ), ); } function theme_project_term_list($terms, $path) { $depth = 0; $output = "\n\n"; return $output; } /** * Theme a compact project view/summary. * * $project has the following fields: * - title: Title * - nid: Node id * - body: Filtered description * - term: String with the selected term name * - version: String with the version * - links: Array of links */ function theme_project_summary($project) { $output = '
'; $output .= '

'. l($project->title, "node/$project->nid") .'

'; if (!empty($project->changed)) { $output .= '

' . t('Last changed: !interval ago', array('!interval' => format_interval(time() - $project->changed, 2))) . '

'; } $output .= $project->body; if (!empty($project->download_table)) { $output .= $project->download_table; } if (!empty($project->links)) { $output .= theme('links', $project->links); } if (!empty($project->terms)) { $output .= theme('links', $project->terms); } $output .= '
'; return $output; } /** * Display an RSS feed icon for use on project nodes. */ function theme_project_feed_icon($url, $title) { if ($icon = theme('image', 'misc/feed.png', $title, $title)) { return ''. $icon .''; } } /** * Return a string of valid project names for use with JS autocomplete fields. */ function project_autocomplete_project($string = '') { $matches = array(); // The user enters a comma-separated list of projects. We only autocomplete // the last one. $array = drupal_explode_tags($string); $last_string = trim(array_pop($array)); if ($last_string != '') { $result = db_query_range(db_rewrite_sql("SELECT n.title FROM {node} n WHERE n.status = %d AND n.type = '%s' AND LOWER(n.title) LIKE LOWER('%%%s%%')"), 1, 'project_project', $last_string, 0, 10); $prefix = count($array) ? implode(', ', $array) .', ' : ''; while ($project = db_fetch_object($result)) { $title = $project->title; // Commas and quotes in terms are special cases, so encode 'em. if (strpos($title, ',') !== FALSE || strpos($title, '"') !== FALSE) { $title = '"'. str_replace('"', '""', $project->title) .'"'; } $matches[$prefix . $title] = check_plain($project->title); } } drupal_json($matches); } /** * Return a string of valid project names owned by a given user. */ function project_autocomplete_project_user($uid, $string = '') { $matches = array(); // The user enters a comma-separated list of projects. We only autocomplete // the last one. $array = drupal_explode_tags($string); $last_string = trim(array_pop($array)); if ($last_string != '') { $result = db_query_range(db_rewrite_sql("SELECT n.title FROM {node} n WHERE n.status = %d AND n.type = '%s' AND n.uid = %d AND LOWER(n.title) LIKE LOWER('%%%s%%')"), 1, 'project_project', $uid, $last_string, 0, 10); $prefix = count($array) ? implode(', ', $array) .', ' : ''; while ($project = db_fetch_object($result)) { $title = $project->title; // Commas and quotes in terms are special cases, so encode 'em. if (strpos($title, ',') !== FALSE || strpos($title, '"') !== FALSE) { $title = '"'. str_replace('"', '""', $project->title) .'"'; } $matches[$prefix . $title] = check_plain($project->title); } } drupal_json($matches); } /** * Returns whether or not the project module should use * taxonomy-specific functionality. */ function project_use_taxonomy() { return module_exists('taxonomy') && taxonomy_get_tree(_project_get_vid()); } /** * Returns whether or not the project uses version control or not. */ function project_use_cvs($project) { if (module_exists('cvs')) { $project = is_numeric($project) ? node_load($project) : $project; return !empty($project->cvs['repository']); } } /** * Determines if the current site supports caching of project-related pages. * * The pages can be cached if: * 1. Anonymous users have the 'access projects' permission. * 2. No node access modules are installed. * * @return TRUE if the output can be cached, FALSE otherwise. */ function project_can_cache() { $grants = module_implements('node_grants'); if (!empty($grants)) { return FALSE; } $allowed_roles = user_roles(FALSE, 'access projects'); if (!isset($allowed_roles[DRUPAL_ANONYMOUS_RID])) { return FALSE; } return TRUE; } /** * Intelligently merge project type terms selected by the * user back into $node->taxonomy during a node preview. */ function project_project_form_preview_submit($form, &$form_state) { $project_type_vid = _project_get_vid(); if (isset($project_type_vid) && isset($form_state['values']['project_type'])) { $project_type_tid = $form_state['values']['project_type']; $project_type_tid_field = 'tid_' . $project_type_tid; $project_type_terms = array(); $project_type_terms[$project_type_tid] = $project_type_tid; if (isset($form_state['values']->$project_type_tid_field)) { foreach ($form_state['values'][$project_type_tid_field] as $tid) { $project_type_terms[$tid] = $tid; } } $form_state['node']['taxonomy'][$project_type_vid] = $project_type_terms; } } /** * Present a validation error if the user tries to make the * Project types vocabulary a free tagging vocabulary. */ function project_project_type_vocabulary_validate($form, &$form_state) { if (!empty($form_state['values']['tags'])) { form_set_error('tags', t('The %name vocabulary does not support tags.', array('%name' => $form_state['values']['name']))); } } /** * Present a validation error if the user tries to make a vocabulary * being used for association with a project type a free tagging vocabulary. */ function project_associated_vocabulary_validate($form, &$form_state) { if (!empty($form_state['values']['tags'])) { form_set_error('tags', t('The %name vocabulary is currently being associated with a project type. This is incompatible with tags. Please visit the settings page and change this if you wish to use freetagging on this vocabulary.', array('%name' => $form_state['values']['name'], '!url' => url('admin/project/project-settings')))); } } /** * Theme a project type item. */ function theme_project_type($term) { $link = l($term->name, check_url('project/'. drupal_strtolower($term->name))); $link = str_replace('%20', '+', $link); $output = "
$link
\n"; if ($term->description) { $output .= '

' . filter_xss($term->description) . '

'; } return $output; } /** * Theme a project type description. */ function theme_project_type_description($term) { $output = ''; if ($term->description) { $output .= '

' . filter_xss($term->description) . '

'; } return $output; } /** * Find a project node ID (nid) based on project short name (uri). * * @param $uri * The project shortname (uri) to search for. * * @return * The project node ID (nid) of the given project, or NULL if not found. */ function project_get_nid_from_uri($uri) { static $nids = array(); if (!isset($nids[$uri])) { $nids[$uri] = db_result(db_query_range("SELECT nid FROM {project_projects} WHERE uri = '%s'", $uri, 0, 1)); } return $nids[$uri]; } /** * Find a project shortname (uri) based on project node ID (nid). * * @param $uri * The project node ID (nid) to search for. * * @return * The project shortname (uri) of the given project, or NULL if not found. */ function project_get_uri_from_nid($nid) { static $uris = array(); if (!isset($uris[$nid])) { $uris[$nid] = db_result(db_query_range("SELECT uri FROM {project_projects} WHERE nid = %d", $nid, 0, 1)); } return $uris[$nid]; } /** * Get the project node context from the currently active menu, if any. * * @return * A fully loaded project $node object if the currently active menu has a * project node context, or NULL if the menu isn't pointing to a project. */ function project_get_project_from_menu() { if (($node = menu_get_object('project_node')) || (($node = menu_get_object()) && $node->type == 'project_project')) { return $node; } } /** * Implement hook_token_list() (from token.module) */ function project_token_list($type) { if ($type == 'node') { $tokens['node'] = array( 'project_shortname' => t("A project's short name"), 'project_homepage' => t("Link to a project's home page"), 'project_changelog' => t("Link to a project's changelog"), 'project_demo' => t('Demonstration link for a project'), 'project_documentation' => t("Link to a project's documentation"), 'project_screenshots' => t("Link to a project's screenshots"), 'project_license' => t("Link to a project's license"), ); return $tokens; } } /** * Implement hook_token_values() (from token.module) */ function project_token_values($type = 'all', $object = NULL) { if ($type == 'node') { if ($object->type == 'project_project') { $values['project_shortname'] = check_plain($object->project['uri']); $values['project_homepage'] = check_url($object->project['homepage']); $values['project_changelog'] = check_url($object->project['changelog']); $values['project_demo'] = check_url($object->project['demo']); $values['project_documentation'] = check_url($object->project['documentation']); $values['project_screenshots'] = check_url($object->project['screenshots']); $values['project_license'] = check_url($object->project['license']); } else { $values['project_shortname'] = ''; $values['project_homepage'] = ''; $values['project_changelog'] = ''; $values['project_demo'] = ''; $values['project_documentation'] = ''; $values['project_screenshots'] = ''; $values['project_license'] = ''; } return $values; } } /** * Validate the short name (uri) of a project node. * * @param $node * A project node with relevant fields pre-filled * @return * A string containing an error message, or FALSE if no errors were found. */ function project_validate_project_shortname(&$node) { if (empty($node->project['uri'])) { if (variable_get('project_sandbox_numeric_shortname', FALSE) && $node->project['sandbox']) { $node->project['uri'] = ''; } else { return t('A short project name is required.'); } } else { // Make sure uri only includes valid characters if (!preg_match('/^[a-zA-Z0-9_-]+$/', $node->project['uri'])) { return t('Please only use alphanumeric characters for the project name.'); } // Make sure uri isn't already in use, or reserved. Includes all X from // project/issues/X paths used in project_issues module $reserved_names = array('user', 'issues', 'releases', 'rss', 'subscribe-mail', 'search', 'add', 'update_project', 'statistics', 'comments', 'autocomplete', 'cvs', 'developers', 'usage'); if (project_use_taxonomy()) { $project_types = project_get_project_types(); foreach ($project_types as $term) { $reserved_names[] = strtolower($term->name); } } if (in_array(strtolower($node->project['uri']), $reserved_names)) { return t('This project name is reserved.'); } $existing_nid = project_get_nid_from_uri($node->project['uri']); if (!empty($existing_nid) && $existing_nid != $node->nid) { return t('This project name is already in use.'); } } return FALSE; }