? modules/project/project_release_debug.php Index: modules/project_issue/issue.inc =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/project_issue/issue.inc,v retrieving revision 1.172.2.22 diff -u -F^f -r1.172.2.22 issue.inc --- modules/project_issue/issue.inc 8 Sep 2006 22:07:43 -0000 1.172.2.22 +++ modules/project_issue/issue.inc 15 Oct 2006 09:41:22 -0000 @@ -442,7 +442,7 @@ function project_issue_form(&$node, &$pa if (trim($project->help)) { drupal_set_message($project->help); } - if ($releases = project_release_load($project)) { + if ($releases = project_release_get_releases($project, 0)) { $releases = array(t('')) + $releases; } $components = array(); @@ -662,7 +662,7 @@ function project_issue_node_form_validat if ($form_values['page'] == 2) { if ($form_values['pid'] && $project = node_load($form_values['pid'])) { $node->title = $form_values['title']; - if ($releases = project_release_load($project)) { + if ($releases = project_release_get_releases($project, 0)) { if (!$form_values['rid'] || !$releases[$form_values['rid']]) { $form_values['rid'] = $project->version; } @@ -702,7 +702,7 @@ function project_issue_comment_validate( // Validate the rest of the form. if (isset($node->title) && !$node->validated) { if ($node->pid && $project = node_load($node->pid)) { - if ($releases = project_release_load($project)) { + if ($releases = project_release_get_releases($project, 0)) { if (!$node->rid || !$releases[$node->rid]) { $node->rid = $project->version; } @@ -766,7 +766,8 @@ function project_issue_view(&$node, $tea if (!$teaser && $page) { $project = node_load(array('nid' => $node->pid, 'type' => 'project_project')); - $release = project_release_load($node->rid); + $release->nid = $node->rid; + $release = project_release_load($release); $assigned = ($node->assigned && ($account = user_load(array('uid' => $node->assigned))) ? $account->name : t('Unassigned')); $output = '
'; @@ -923,7 +924,7 @@ function project_issue_query($project = foreach ($project->components as $component) { $components[$component] = $component; } - $versions = project_release_load($project, 0); + $versions = project_release_get_releases($project, 0); } else { $uris = NULL; @@ -1282,7 +1283,7 @@ function project_issue_query_result($que if ($project) { drupal_set_title(t('issues for %name', array('%name' => theme('placeholder', $project->title)))); - $releases = project_release_load($project, 0); + $releases = project_release_get_releases($project, 0); $query->projects = $project->nid; $links = array(); if (user_access('create project issues')) { Index: modules/project_issue/mail.inc =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/project_issue/mail.inc,v retrieving revision 1.60.2.3 diff -u -F^f -r1.60.2.3 mail.inc --- modules/project_issue/mail.inc 20 Aug 2006 19:06:50 -0000 1.60.2.3 +++ modules/project_issue/mail.inc 15 Oct 2006 09:41:23 -0000 @@ -59,8 +59,8 @@ function project_issue_mailhandler($node } break; case 'rid': - if ($entry->pid && ($rid = db_result(db_query("SELECT rid FROM {project_releases} WHERE nid = %d AND version = '%s'", $entry->pid, $node->$text), 0))) { - $entry->rid = $rid; + if ($entry->pid && ($nid = db_result(db_query("SELECT nid FROM {project_release_nodes} WHERE pid = %d AND version = '%s'", $entry->pid, $node->$text), 0))) { + $entry->rid = $nid; } break; case 'assigned': @@ -172,7 +172,8 @@ function project_mail_summary($field, $v return $value ? project_issue_priority($value) : t(''); case 'rid': if ($value) { - $release = project_release_load($value); + $release->nid = $value; + $release = project_release_load($release); return $release->version; } return t(''); cvs diff: Diffing modules/project Index: modules/project/CHANGELOG.txt =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/project/CHANGELOG.txt,v retrieving revision 1.5.2.1 diff -u -F^f -r1.5.2.1 CHANGELOG.txt --- modules/project/CHANGELOG.txt 25 Jul 2006 00:12:26 -0000 1.5.2.1 +++ modules/project/CHANGELOG.txt 15 Oct 2006 09:41:23 -0000 @@ -1,3 +1,8 @@ +XX. September 2006 +----------------- +- Split out releases into project_release.module and made them real nodes + (dww and merlinofchaos) + 21. July 2006 ----------------- - Split out issue tracking to project_issue.module (dww) Index: modules/project/project.inc =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/project/project.inc,v retrieving revision 1.75.2.16 diff -u -F^f -r1.75.2.16 project.inc --- modules/project/project.inc 26 Jul 2006 07:18:01 -0000 1.75.2.16 +++ modules/project/project.inc 15 Oct 2006 09:41:23 -0000 @@ -173,16 +173,6 @@ function project_project_form(&$node) { '#description' => t('Link to a live demo.'), ); - if ($releases = project_release_load($node)) { - $form['project']['version'] = array( - '#type' => 'select', - '#title' => t('Default version'), - '#default_value' => $node->version, - '#options' => $releases, - '#description' => t('Default version for downloading.'), - ); - } - $form['#suffix'] = '
'; return $form; } @@ -303,44 +293,8 @@ function project_project_view(&$node, $t $breadcrumb = project_project_set_location($node, $breadcrumb); menu_set_location($breadcrumb); - if ($releases = project_release_load($node)) { - // Determine most suitable release - $overview = variable_get('project_release_overview', -1); - foreach ($releases as $rid => $version) { - // If the project has a preferred release, choose that. - if ($rid == $node->version) { - $release = $rid; - break; - } - // If the version matches the default on the overview, prefer the first. - if (!isset($release) && strpos($version, $overview) === 0) { - $release = $rid; - } - } - // Fallback to the latest - if (!isset($release)) { - reset($releases); - list($release) = each($releases); - } - $release = project_release_load($release); - - // Download section - $links = array(); - - $links[] = '' . t('Download latest release (%version, %date, %size)', array('%version' => $release->version, '%date' => format_date($release->changed, 'small'), '%size' => format_size(filesize($release->path)))) . ''; - - if ($release->changes) { - $links[] = l(t('Read release notes'), 'node/'. $node->nid .'/release', null, null, 'version-'. $release->version); - } - if (count($releases) > 1) { - $links[] = l(t('View other releases'), 'node/'. $node->nid .'/release'); - } - } - if (node_access('update', $node) && !variable_get('project_release_directory', '')) { - $links[] = l(t('Add new release'), 'node/'. $node->nid .'/release/add'); - } - if ($links) { - $output .= theme('item_list', $links, t('Releases')); + if (function_exists('project_release_project_download_table')) { + $output = project_release_project_download_table($node); } // Misc section @@ -402,22 +356,6 @@ function project_project_view(&$node, $t } } -function project_project_releases() { - $node = node_load(arg(1)); - - // Breadcrumb navigation - $breadcrumb[] = array('path' => 'node/'. $node->nid .'/', 'title' => $node->title); - $breadcrumb[] = array('path' => 'node/'. arg(1) .'/release', 'title' => t('Releases')); - $breadcrumb = project_project_set_location($node, $breadcrumb); - - menu_set_location($breadcrumb); - - $output = project_release_list($node); - - drupal_set_title(check_plain($node->title)); - return $output; -} - function project_project_load($node) { $project = db_fetch_object(db_query('SELECT * FROM {project_projects} WHERE nid = %d', $node->nid)); @@ -449,17 +387,16 @@ function project_project_nodeapi(&$node, function project_project_insert($node) { db_query("INSERT INTO {project_projects} (nid, uri, homepage, changelog, cvs, demo, release_directory, mail, version, screenshots, documentation, license) VALUES (%d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', %d, '%s', '%s', '%s')", $node->nid, $node->uri, $node->homepage, $node->changelog, $node->cvs, $node->demo, $node->release_directory, $node->mail, $node->version, $node->screenshots, $node->documentation, $node->license); - project_release_scan_directory($node->uri); +// project_release_scan_directory($node->uri); } function project_project_update($node) { db_query("UPDATE {project_projects} SET uri = '%s', homepage = '%s', changelog = '%s', cvs = '%s', demo = '%s', release_directory = '%s', mail = '%s', version = %d, screenshots = '%s', documentation = '%s', license = '%s' WHERE nid = %d", $node->uri, $node->homepage, $node->changelog, $node->cvs, $node->demo, $node->release_directory, $node->mail, $node->version, $node->screenshots, $node->documentation, $node->license, $node->nid); - project_release_scan_directory($node->uri); +// project_release_scan_directory($node->uri); } function project_project_delete($node) { db_query('DELETE FROM {project_projects} WHERE nid = %d', $node->nid); - db_query('DELETE FROM {project_releases} WHERE nid = %d', $node->nid); } function project_project_access($op, $node) { Index: modules/project/project.install =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/project/project.install,v retrieving revision 1.2.2.6 diff -u -F^f -r1.2.2.6 project.install --- modules/project/project.install 10 Sep 2006 06:37:17 -0000 1.2.2.6 +++ modules/project/project.install 15 Oct 2006 09:41:23 -0000 @@ -22,25 +22,7 @@ function project_install() { KEY project_projects_uri (uri(8)) ) TYPE=MyISAM /*!40100 DEFAULT CHARACTER SET utf8 */;"); - db_query("CREATE TABLE {project_releases} ( - rid int(10) unsigned NOT NULL default '0', - nid int(10) unsigned NOT NULL default '0', - fid int(10) unsigned NOT NULL default '0', - path varchar(255) NOT NULL default '', - created int(10) unsigned NOT NULL default '0', - version varchar(255) NOT NULL default '', - status tinyint(1) unsigned NOT NULL default '1', - changes text, - weight tinyint(3) unsigned NOT NULL default '0', - changed int(10) unsigned NOT NULL default '0', - hash varchar(32) NOT NULL default '', - UNIQUE(path), - PRIMARY KEY (`rid`), - KEY project_releases_nid (nid) - ) TYPE=MyISAM - /*!40100 DEFAULT CHARACTER SET utf8 */;"); break; - case 'pgsql': db_query("CREATE TABLE {project_projects} ( nid int NOT NULL default '0', @@ -57,20 +39,6 @@ function project_install() { license varchar(255) default '' not null, PRIMARY KEY (nid) );"); - db_query("CREATE TABLE {project_releases} ( - rid int NOT NULL default '0', - nid int NOT NULL default '0', - fid int NOT NULL default '0', - path varchar(255) NOT NULL default '', - created int NOT NULL default '0', - hash varchar(32) NOT NULL default '', - version varchar(255) NOT NULL default '', - changes text, - weight smallint NOT NULL default '0', - changed int NOT NULL default '0', - status smallint default '1' not null, - PRIMARY KEY (rid) - );"); break; } Index: modules/project/project.module =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/project/project.module,v retrieving revision 1.239.2.13 diff -u -F^f -r1.239.2.13 project.module --- modules/project/project.module 7 Sep 2006 19:25:39 -0000 1.239.2.13 +++ modules/project/project.module 15 Oct 2006 09:41:24 -0000 @@ -1,21 +1,17 @@ project_projects -// project releases -> project_releases - $path = drupal_get_path('module', 'project'); if (file_exists("$path/project.inc")) { require_once "$path/project.inc"; - require_once "$path/release.inc"; } function project_help($section) { switch ($section) { case 'admin/modules#description': - return t('Provides a project node type and manages downloads of project releases.'); + return t('Provides a project node type and browsing of projects.'); case 'node/add#project_project': - return t('A project is anything that has releases which can be downloaded'); + return 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.' ); case 'node/add/project_project': if (project_use_taxonomy()) { $tree = taxonomy_get_tree(_project_get_vid()); @@ -89,69 +85,27 @@ function project_perm() { * Callback for the main settings page. */ function project_settings() { - $versions = array(-1 => t('all')) + project_releases_list(); $sort_methods = drupal_map_assoc(array_keys(module_invoke_all('project_sort_methods', 'methods'))); - $form['releases'] = array('#type' => 'fieldset', - '#title' => t('Releases'), - '#collapsible' => TRUE, - ); - $form['releases']['project_release_directory'] = array( - '#type' => 'textfield', - '#title' => t('Release directory'), - '#default_value' => variable_get('project_release_directory', ''), - '#size' => 50, - '#maxlength' => 255, - '#description' => t('Leave this blank if project maintainers are to create their own release packages. This is useful if releases are generated by an external tool.'), - ); - - $form['releases']['project_release_unmoderate'] = array( - '#type' => 'radios', - '#title' => t('Unmoderate projects with releases'), - '#default_value' => variable_get('project_release_unmoderate', 0), - '#options' => array('Disabled', 'Enabled'), - ); - - $form['releases']['project_browse_releases'] = array( - '#type' => 'checkbox', - '#title' => t('Browse projects by releases'), - '#default_value' => variable_get('project_browse_releases', 0), - '#description' => t('Checking this box will cause the project browsing page to have a version select.'), - ); - - $form['releases']['project_release_overview'] = array( - '#type' => 'radios', - '#title' => t('Default release overview'), - '#default_value' => variable_get('project_release_overview', -1), - '#options' => $versions, - '#description' => t('Default release version to list on the overview page'), - ); - - $form['browsing'] = array( - '#type' => 'fieldset', - '#title' => t('Browsing options'), - '#collapsible' => TRUE, - ); - if (project_use_taxonomy()) { // For now, date-based browsing doesn't work once you disable // taxonomy (only because the code involved is rather complicated // and needs to be majorly refactored and cleaned up). - $form['browsing']['project_sort_method'] = array( + $form['project_sort_method'] = array( '#type' => 'radios', '#title' => t('Default sort option'), '#default_value' => variable_get('project_sort_method', 'category'), '#options' => $sort_methods, '#description' => t('Default sorting option to use on the overview page'), ); - $form['browsing']['sort_methods'] = array( + $form['sort_methods'] = array( '#type' => 'fieldset', '#title' => t('Enabled sorting options'), ); $tree = taxonomy_get_tree(_project_get_vid(), 0 , -1, 1); foreach ($tree as $term) { - $form['browsing']['sort_methods']['project_sort_method_used_' . $term->tid] = array( + $form['sort_methods']['project_sort_method_used_' . $term->tid] = array( '#type' => 'checkboxes', '#title' => $term->name, '#default_value' => variable_get('project_sort_method_used_' . $term->tid, array_keys($sort_methods)), @@ -160,7 +114,7 @@ function project_settings() { ); } } - $form['browsing']['project_browse_nodes'] = array( + $form['project_browse_nodes'] = array( '#type' => 'select', '#title' => t('Number of projects to list in paged browsing'), '#default_value' => variable_get('project_browse_nodes', 30), '#options' => drupal_map_assoc(array(5, 10, 15, 20, 25, 30, 35, 40, 45, 50)), @@ -176,13 +130,6 @@ function project_settings() { return $form; } -function project_cron() { - if (variable_get('project_release_directory', '') && time() - variable_get('project_release_directory_last', 0) > variable_get('project_release_directory_interval', 6 * 60 * 60)) { - variable_set('project_release_directory_last', time()); - project_release_cron(); - } -} - function project_link($type, $node = 0, $main = 0) { $links = array(); switch ($type) { @@ -284,7 +231,7 @@ function project_menu($may_cache) { if (project_use_taxonomy()) { $sort_methods = module_invoke_all('project_sort_methods', 'methods'); $terms = taxonomy_get_tree(_project_get_vid()); - $releases = variable_get('project_browse_releases', 0); + $releases = variable_get('project_release_browse_versions', 0); foreach ($terms as $i => $term) { // Only use the first-level terms. if ($term->depth == 0) { @@ -319,32 +266,9 @@ function project_menu($may_cache) { $access = project_project_access('create', NULL); $items[] = array('path' => 'node/add/project_project', 'title' => t('project'), 'callback' => 'node_page', 'access' => $access, 'type' => MENU_NORMAL_ITEM); - // Releases - if (variable_get('project_release_directory', '')) { - $access = user_access('administer projects'); - $items[] = array('path' => 'admin/settings/project/reload', 'title' => t('scan for releases'), 'callback' => 'project_release_scan', 'access' => $access, 'type' => MENU_NORMAL_ITEM); - } } else { theme_add_style(drupal_get_path('module', 'project') .'/project.css'); - if (arg(0) == 'node' && is_numeric(arg(1))) { - $node = node_load(arg(1)); - if ($node->type == 'project_project') { - $items[] = array('path' => 'node/'. arg(1) .'/release', 'title' => t('releases'), 'callback' => 'project_project_releases', 'access' => node_access('view', $node), 'weight' => 0, 'type' => MENU_CALLBACK); - - if (node_access('update', $node)) { - $items[] = array('path' => 'node/'. arg(1) .'/release/overview', 'title' => t('overview'), 'callback' => 'project_project_releases', 'access' => node_access('view', $node), 'weight' => 0, 'type' => MENU_CALLBACK); - } - - if (!variable_get('project_release_directory', '')) { - $items[] = array('path' => 'node/'. arg(1) .'/release/add', 'title' => t('add'), 'callback' => 'project_release_submit', 'access' => node_access('update', $node), 'weight' => 1, 'type' => MENU_CALLBACK); - } - if (arg(2) == 'release' && is_numeric(arg(4))) { - $items[] = array('path' => 'node/'. arg(1) .'/release/edit/'. arg(4), 'title' => t('edit'), 'callback' => 'project_release_submit', 'access' => node_access('update', $node), 'weight' => 1, 'type' => MENU_CALLBACK); - $items[] = array('path' => 'node/'. arg(1) .'/release/delete/'. arg(4), 'title' => t('edit'), 'callback' => 'project_release_submit', 'access' => node_access('update', $node), 'weight' => 1, 'type' => MENU_CALLBACK); - } - } - } } return $items; @@ -386,9 +310,6 @@ function project_form_alter($form_id, &$ /** * hook_nodeapi() implementation. This just decides what type of node * is being passed, and calls the appropriate type-specific hook. - * NOTE: there's currently only 1 value left here, but I'm planning to - * make project_release its own node-type, so we'll probably want to - * leave this plumbing in place for that... -dww * * @see project_project_nodeapi(). */ @@ -429,22 +350,22 @@ function project_page_overview($termname $breadcrumb = array(l(t('Home'), NULL)); $sort_methods = module_invoke_all('project_sort_methods', 'methods'); - // Read in requested version, if any. - if ($_POST['edit']['rid']) { - $version = $_POST['edit']['rid']; - } - else if ($_SESSION['project_version']) { - $version = $_SESSION['project_version']; - } - else { - $version = variable_get('project_release_overview', -1); - } - $form = project_version_filter_form($version); - $version_form = drupal_get_form('project_version_filter_form', $form); - if ($_POST['edit']['rid']) { - $_SESSION['project_version'] = $form_values['rid']; - } - if (variable_get('project_browse_releases', 0)) { + if (module_exist('project_release') && variable_get('project_release_browse_versions', 0)) { + // Read in requested version, if any. + if ($_POST['edit']['version_tid']) { + $version = $_POST['edit']['version_tid']; + } + else if ($_SESSION['project_version']) { + $version = $_SESSION['project_version']; + } + else { + $version = variable_get('project_release_overview', -1); + } + $form = project_release_version_filter_form($version); + $version_form = drupal_get_form('project_release_version_filter_form', $form); + if ($_POST['edit']['version_tid']) { + $_SESSION['project_version'] = $form_values['version_tid']; + } $output .= $version_form; } @@ -475,16 +396,16 @@ function project_page_overview($termname $terms = array(); if ($tree) { $tids = array(); - if (variable_get('project_browse_releases', 0) && $version != -1) { + if (variable_get('project_release_browse_versions', 0) && $version != -1) { // Find all terms associated with the requested version. - $result = db_query("SELECT t.tid, COUNT(DISTINCT(n.nid)) AS count FROM {term_node} t INNER JOIN {project_releases} p ON t.nid = p.nid INNER JOIN {node} n ON n.nid = t.nid WHERE (p.version LIKE '%s%%') AND (p.path <> '') AND (n.status = 1) GROUP BY t.tid", $version); + $result = db_query("SELECT tp.tid, COUNT(DISTINCT(n.nid)) AS count FROM {term_node} tp INNER JOIN {project_release_nodes} p ON tp.nid = p.pid INNER JOIN {node} n ON n.nid = p.pid INNER JOIN {term_node} tr ON tr.nid = p.nid WHERE tr.tid = %d AND (p.file_path <> '') AND (n.status = 1) GROUP BY tp.tid", $version); $tids = array(); while ($item = db_fetch_object($result)) { $tids[$item->tid] = $item->count; } } foreach ($tree as $cterm) { - if (!variable_get('project_browse_releases', 0) || ($version == -1) || array_key_exists($cterm->tid, $tids)) { + if (!variable_get('project_release_browse_versions', 0) || ($version == -1) || array_key_exists($cterm->tid, $tids)) { if ($tids[$cterm->tid]) { $cterm->count = $tids[$cterm->tid]; } @@ -499,9 +420,9 @@ function project_page_overview($termname } } - $tid = (arg(3) && is_numeric(arg(3))) ? arg(3) : project_default_tid(); - $term = taxonomy_get_term($tid); - + if (arg(3) && is_numeric(arg(3))) { + $term = taxonomy_get_term(arg(3)); + } } // Set the default elements that will be used to construct the SQL statement. @@ -572,16 +493,17 @@ function project_page_overview($termname // Only the 'pieces' are returned; the prefix and glue elements remain unchanged. $sql_settings = module_invoke($module, 'project_sort_methods', 'sql_settings', $sort_method); - if (variable_get('project_browse_releases', 0)) { + if (variable_get('project_release_browse_versions', 0)) { $release_settings = array( - 'fields' => array('pr.path', 'pr.version', 'MAX(pr.changed) AS changed', 'COUNT(*) AS release_count'), - 'joins' => array('INNER JOIN {project_releases} pr ON n.nid = pr.nid'), - 'wheres' => array('pr.status = 1', "pr.path <> ''"), + 'fields' => array('prn.file_path', 'prn.version', 'MAX(prn.file_date) AS changed', 'COUNT(*) AS release_count'), + 'joins' => array('INNER JOIN {project_release_nodes} prn ON n.nid = prn.pid'), + 'wheres' => array("prn.file_path <> ''"), 'group_bys' => array('n.nid'), - 'parameters' => array($version) ); if ($version != -1) { - $release_settings['wheres'][] = "pr.version LIKE '%s%%'"; + $release_settings['joins'][] = "INNER JOIN {term_node} tr ON tr.nid = prn.nid"; + $release_settings['wheres'][] = "tr.tid = %d"; + $release_settings['parameters'][] = $version; } $sql_settings = array_merge_recursive($sql_settings, $release_settings); } @@ -659,16 +581,24 @@ function project_page_overview($termname } // Make sure we have the latest release + // TODO: this needs help to handle projects with N branches for the + // same compatibility version... if ($project->release_count > 1) { - $latest = db_fetch_object(db_query_range("SELECT path, version FROM {project_releases} WHERE nid = %d AND version LIKE '%s%%' ORDER BY version DESC", $project->nid, $version, 0, 1)); - $project->path = $latest->path; + $values[] = $project->nid; + if ($version != -1) { + $VERSION_JOIN = 'INNER JOIN {term_node} t ON p.nid = t.nid'; + $VERSION_WHERE = 'AND t.tid = %d'; + $values[] = $version; + } + $latest = db_fetch_object(db_query_range("SELECT file_path, version, file_date FROM {project_release_nodes} p $VERSION_JOIN WHERE p.pid = %d $VERSION_WHERE ORDER BY file_date DESC", $values, 0, 1)); + $project->file_path = $latest->file_path; $project->version = $latest->version; } $project->links = array(); - if ($project->path) { - $project->links[] = '' . t('Download') . ''; + if ($project->file_path) { + $project->links[] = l(t('Download'), file_create_url($project->file_path)); } $project->links[] = l(t('Find out more'), "node/$project->nid"); @@ -733,12 +663,16 @@ function project_project_sort_methods($o 'order_bys' => array('n.sticky DESC', 'n.title ASC') ); case 'date': - return array( - 'fields' => array('MAX(pr.changed) AS changed'), - 'joins' => array('INNER JOIN {project_releases} pr ON n.nid = pr.nid'), + // TODO: this should all be conditional on project_release.module + $date_sql = array( + 'order_bys' => array('changed DESC', 'n.sticky DESC', 'n.title ASC'), 'group_bys' => array('n.nid'), - 'order_bys' => array('pr.changed DESC', 'n.sticky DESC', 'n.title ASC') ); + if (!variable_get('project_release_browse_versions', 0)) { + $date_sql['fields'] = array('MAX(prn.file_date) AS changed'); + $date_sql['joins'] = array('INNER JOIN {project_release_nodes} prn ON n.nid = prn.nid'); + } + return $date_sql; case 'name': return array( 'order_bys' => array('n.title ASC', 'n.sticky DESC') @@ -779,19 +713,6 @@ function project_project_sort_methods($o return; } -function project_default_tid() { - static $tid; - if ($tid === NULL) { - if (variable_get('project_release_overview', -1) != -1) { - $tid = db_result(db_query("SELECT t.tid FROM {term_node} t INNER JOIN {project_releases} p ON t.nid = p.nid WHERE (p.version LIKE '%s%%')", $version)); - } - else { - $tid = db_result(db_query("SELECT t.tid FROM {term_node} t INNER JOIN {project_releases} p ON t.nid = p.nid")); - } - } - return $tid; -} - /** * Helper function for grouping nodes by date. */ @@ -857,25 +778,6 @@ function project_projects_select_options return $projects; } -function project_version_filter_form($version) { - $releases = array(-1 => t('')) + project_releases_list(); - $form['rid'] = array( - '#type' => 'select', - '#default_value' => $version, - '#options' => $releases - ); - $form['submit'] = array( - '#type' => 'button', - '#value' => t('Go') - ); - return $form; -} - -function theme_project_version_filter_form($form) { - $output = '
' . t('Filter by version:') . form_render($form) . '
'; - return $output; -} - function project_quick_navigate_form($projects) { $form = array(); $form['project_goto'] = array( Index: modules/project/project_release.css =================================================================== RCS file: modules/project/project_release.css diff -N modules/project/project_release.css --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ modules/project/project_release.css 15 Oct 2006 09:41:24 -0000 @@ -0,0 +1,17 @@ +/* $Id$ */ + +.node-form .project-release .version-elements fieldset .form-item { + float: left; + padding-right: 0.8em; + margin: 0.05em 0.1em; +} + +.node-form .project-release fieldset:after { + content: "."; + display: block; + height: 0; + clear: both; + visibility: hidden; +} + +.node-form .project-release .version-elements fieldset {display: inline-table;} Index: modules/project/project_release.install =================================================================== RCS file: modules/project/project_release.install diff -N modules/project/project_release.install --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ modules/project/project_release.install 15 Oct 2006 09:41:24 -0000 @@ -0,0 +1,81 @@ +type == 'project_project') { + $items[] = array('path' => 'node/'. arg(1) .'/release', 'title' => t('releases'), 'callback' => 'project_release_project_releases', 'access' => node_access('view', $node), 'weight' => 0, 'type' => MENU_CALLBACK); + if (node_access('update', $node)) { + $items[] = array('path' => 'node/'. arg(1) .'/release/overview', 'title' => t('overview'), 'callback' => 'project_release_project_releases', 'access' => node_access('view', $node), 'weight' => 0, 'type' => MENU_CALLBACK); + } + } + } + theme_add_style(drupal_get_path('module', 'project_release') .'/project_release.css'); + project_release_get_api_taxonomy(); + } + return $items; +} + +/** + * Callback for the main settings page. + * @ingroup project_release_core + */ +function project_release_settings() { + // TODO: do we even need the release directory setting anymore? + $form['project_release_directory'] = array( + '#type' => 'textfield', + '#title' => t('Release directory'), + '#default_value' => variable_get('project_release_directory', ''), + '#size' => 50, + '#maxlength' => 255, + '#description' => t('Leave this blank if project maintainers are to create their own release packages. This is useful if releases are generated by an external tool.'), + ); + + $form['project_release_default_version_format'] = array( + '#type' => 'textfield', + '#title' => t('Default version format string'), + '#default_value' => variable_get('project_release_default_version_format', PROJECT_RELEASE_DEFAULT_VERSION_FORMAT), + '#size' => 50, + '#maxlength' => 255, + '#description' => t('Customize the default format of the version strings for releases of projects on this site. Users with "administer projects" permissions can override this setting for each project.') .' '. t('Available variables are:') .' %api, %major, %minor, %patch, %extra. '. t("The percent sign ('%') at the front of the variable name indicates that a period ('.') should be inserted as a delimiter before the value of the variable. The '%' can be replaced with a hash mark ('#') to use a hyphen ('-') delimiter, or with an exclaimation point ('!') to have the value printed without a delimiter. Any variable in the format string that has no value will be removed entirely from the final string."), + ); + + // TODO: isn't this a dead setting, too? + $form['project_release_unmoderate'] = array( + '#type' => 'radios', + '#title' => t('Unmoderate projects with releases'), + '#default_value' => variable_get('project_release_unmoderate', 0), + '#options' => array('Disabled', 'Enabled'), + ); + + if ($tree = project_release_get_api_taxonomy()) { + // TODO: put these 2 in a fieldset? + $form['project_release_browse_versions'] = array( + '#type' => 'checkbox', + '#title' => t('Browse projects by release versions'), + '#default_value' => variable_get('project_release_browse_versions', 0), + '#description' => t('Checking this box will cause the project browsing page to have a version select.'), + ); + $terms[-1] = t('all'); + foreach ($tree as $term) { + $terms[$term->tid] = $term->name; + } + $form['project_release_overview'] = array( + '#type' => 'radios', + '#title' => t('Default release overview'), + '#default_value' => variable_get('project_release_overview', -1), + '#options' => $terms, + '#description' => t('Default release version to list on the overview page.'), + ); + } + return $form; +} + + +/** + * @defgroup project_release_node Drupal node-type related hooks + */ + +/** + * Implementation of hook_access(). + * @ingroup project_release_node + * + * TODO: Maybe we should add new permissions for accessing release + * nodes, but for now, we're just using the existing project perms. + */ +function project_release_access($op, $node) { + global $user; + switch ($op) { + case 'view': + return user_access('access projects') || (user_access('access own projects') && $node->uid == $user->uid); + case 'create': + // We only want to allow creating releases if the project is + // specified in the URL + if (is_numeric(arg(3))) { + return project_release_access_check(arg(3)); + } + return FALSE; + case 'update': + return project_release_access_check($node->pid); + case 'delete': + // No one should ever delete a release node, only unpublish it. + return FALSE; + } +} + +function project_release_access_check($project) { + global $user; + if (empty($user->uid)) { + return FALSE; + } + $project_obj = is_numeric($project) ? node_load($project) : $project; + if (!isset($project_obj) || $project_obj->type != 'project_project') { + return FALSE; + } + if (user_access('administer projects')) { + return TRUE; + } + if (user_access('maintain projects')) { + if ($user->uid == $project_obj->uid) { + return TRUE; + } + if (module_exist('cvs')) { + if (db_num_rows(db_query("SELECT * FROM {cvs_project_maintainers} WHERE uid = %d AND nid = %d", $user->uid, $project_obj->nid))) { + return TRUE; + } + } + } + return FALSE; +} + +/** + * Implementation of hook_node_info(). + * @ingroup project_release_node + */ +function project_release_node_info() { + return array( + 'project_release' => array('name' => t('project release'), 'base' => 'project_release'), + ); +} + +/** + * Implementation of hook_form(). + * @ingroup project_release_node + */ +function project_release_form(&$release, &$param) { + if (arg(1) == 'add') { + if (!is_numeric(arg(3))) { + drupal_set_message(t('You can only add releases from a project page.')); + drupal_not_found(); + module_invoke_all('exit'); + exit; + } + $release->pid = arg(3); + $project = node_load($release->pid); + if (!isset($project) || $project->type != 'project_project') { + drupal_set_message(t('Node %nid is not a valid project.', array('%nid' => $release->pid))); + drupal_not_found(); + module_invoke_all('exit'); + exit; + } + $form['project'] = array( + '#type' => 'value', + '#value' => $project, + ); + $format = project_release_get_version_format($project); + } + else { + global $user; + $admin = user_access('administer projects'); + $is_edit = true; + $project->nid = $release->pid; + $format = project_release_get_version_format($project); + } + + $form['#attributes'] = array("enctype" => "multipart/form-data"); + $form['#prefix'] = '
'; + $form['#suffix'] = '
'; + + _project_release_form_add_text_element($form['title'], t('Title'), $release->title, $is_edit, $admin); + + _project_release_form_add_text_element($form['version']['version'], t('Version string'), $release->version, $is_edit, $admin); + + $form['pid'] = array( + '#type' => 'value', + '#value' => $release->pid, + ); + $form['version']['num'] = array( + '#type' => 'fieldset', + '#title' => t('Version number elements'), + '#prefix' => '
', + '#suffix' => '
', + ); + $modify = $admin || !$is_edit; + $form['validate_version'] = array('#type' => 'hidden', '#value' => 1); + _project_release_form_add_version_element($form, $release, $modify, $format, + 'major', t('Major')); + _project_release_form_add_version_element($form, $release, $modify, $format, + 'minor', t('Minor')); + _project_release_form_add_version_element($form, $release, $modify, $format, + 'patch', t('Patch-level')); + _project_release_form_add_version_element($form, $release, $modify, $format, + 'extra', t('Extra identifier'), t('Optionally specify other identifying information for this version, for example "beta-1", "rc-1" or "dev". In most cases, this should be left blank.'), 40); + + if (!variable_get('project_release_directory', '') && empty($release->file_path)) { + $form['file']['file'] = array( + '#type' => 'file', + '#title' => t('File'), + '#size' => 40, + '#description' => ($release->file_path) ? + t('A file already exists, if you upload another file the current file (%file) will be replaced.', array('%file' => $release->file_path)) : + t('Choose the file that will be associated with this release.'), + ); + } + + $form['body_filter']['body'] = array( + '#type' => 'textarea', + '#title' => t('Description'), + '#default_value' => $release->body, + '#rows' => 10, + '#cols' => 40, + '#required' => TRUE, + '#description' => t('Enter a description of this release, such as a list of the major changes or updates.'), + ); + $form['body_filter']['format'] = filter_form($node->format); + + _project_release_form_add_text_element($form['tag']['tag'], t('Tag'), $release->tag, $is_edit, $admin); + if ($admin) { + $form['tag']['rebuild'] = array( + '#type' => 'checkbox', + '#title' => t('Rebuild this release regularly?'), + '#default_value' => $release->rebuild, + ); + } + else { + $form['tag']['rebuild'] = array( + '#type' => 'value', + '#value' => $release->rebuild, + ); + } + _project_release_form_add_text_element($form['file']['file_path'], t('File path'), $release->file_path, $is_edit, $admin, false); + _project_release_form_add_text_element($form['file']['file_hash'], t('File md5 hash'), $release->file_hash, $is_edit, false); + _project_release_form_add_text_element($form['file']['file_date'], t('File date'), $release->file_date, $is_edit, false); + + return $form; +} + +/** + * @defgroup project_release_internal Internal module functions + */ + +/** + * Modifies the given $form array to add the appropriate form element + * for the requested version field. Since the 20+ lines of code in + * here have to be duplicated 6 times in project_release_form(), this + * function exists so we can reuse the code. + * @see project_release_form + * @ingroup project_release_internal + * + * @param $form Form array to modify + * @param $release Relase node the form is for + * @param $modify Boolean indicating if we should allow modifications + * @param $format Version format string for this project + * @param $name Name of this version element + * @param $title Translatable title of the form element + * @param $description Translatable description of the form element. + * @param $required Boolean for if the form element should be required + */ +function _project_release_form_add_version_element(&$form, $release, $modify, $format, $name, $title, $description = '', $size = 10) { + $var_name = 'version_' . $name; + $regexp = "@.*[!#%]$name.*@"; + if (preg_match($regexp, $format)) { + $form['version']['num'][$var_name] = array( + '#type' => 'textfield', + '#title' => $title, + '#default_value' => $release->$var_name, + '#size' => $size, + '#maxlength' => $size+10, + '#attributes' => array('style' => 'width:auto'), + ); + if ($required) { + // TODO: handle this more flexibly for sites not using CVS + // perhaps if the format variable is UPPERCASE it's required, + // and lowercase is optional or something crazy? + $form[$var_name]['#required'] = TRUE; + } + if ($description) { + $form['version']['num'][$var_name]['#description'] = $description; + } + if (!$modify) { + $form['version']['num'][$var_name]['#attributes'] = array('disabled' => 'disabled'); + } + } + else { + $form['version']['num'][$var_name] = array( + '#type' => 'value', + '#value' => $release->$var_name, + ); + } +} + +/** + * Modifies the given $form array to add the appropriate form element + * for the desired text field. Since the 16+ lines of code in here + * have to be duplicated 5 times in project_release_form(), this + * function exists so we can reuse the code. + * @see project_release_form + * @ingroup project_release_internal + * + * @param $form Reference to form element to add + * @param $title Translatable title of the form element + * @param $value The value to use in the form + * @param $is_edit Boolean indicating if we're editing or creating + * @param $admin Boolean for if the edit is by a project administrator + * @param $required Boolean for if the field should be required + */ +function _project_release_form_add_text_element(&$form, $title, $value, $is_edit, $admin, $required = true) { + if ($is_edit && !empty($value)) { + $form = array( + '#type' => 'textfield', + '#title' => $title, + '#default_value' => $value, + '#required' => $required, + ); + if(!$admin) { + $form['#attributes']['disabled'] = 'disabled'; + } + } + else { + $form = array( + '#type' => 'value', + '#value' => $value, + ); + } +} + +/** + * Implementation of hook_validate(). + * @ingroup project_release_node + */ +function project_release_validate(&$edit, $form) { + if ($_POST['edit']['validate_version']) { + if (!isset($edit->version_major) && !isset($edit->version_minor) && + !isset($edit->version_patch) && + (!($edit->version_extra) || $edit->version_extra === '')) { + form_set_error('version_major', t('You must fill in some version information.')); + // TODO: find a better form value to mark as the error? + } + foreach (array('version_major' => 'Major version number', 'version_minor' => 'Minor version number') as $field => $name) { + $val = $edit->$field; + if (isset($val) && $val !== '' && !is_numeric($val)) { + form_set_error($field, t('%name must be a number.', array('%name' => $name))); + } + } + $val = $edit->version_patch; + if (isset($val) && $val !== '' && !is_numeric($val) && $val != 'x') { + form_set_error('version_patch', t("Patch-level version number must be numeric or the letter 'x'.")); + } + } + + if ($file = file_check_upload('file')) { + $file = file_save_upload('file', file_directory_path()); + if ($file) { + $filepath = file_create_path($file->filepath); + form_set_value($form['file']['file_path'], $file->filepath); + form_set_value($form['file']['file_date'], filemtime($filepath)); + form_set_value($form['file']['file_hash'], md5_file($filepath)); + } + else { + form_set_error('file', t('There was a problem uploading the specified file.')); + } + } + if (project_release_get_api_taxonomy()) { + $vid = _project_release_get_api_vid(); + if (isset($edit->taxonomy)) { + $tid = $edit->taxonomy[$vid]; + } + elseif (isset($edit->$vid)) { + $tid = $edit->$vid; + } + if (isset($tid) && is_numeric($tid)) { + $edit->version_api_tid = $tid; + } + } + + // With cvs.module installed, this validation is already handled. + // We only want to do it here if we're *not* doing the N-page form... + if (!module_exist('cvs') && project_release_exists($edit)) { + // TODO: is there a better form element to mark with this error? + form_set_error('version_patch', t('This version already exists for this project.')); + } + + // TODO: it'd be nice to automagically reset the version string and + // title based on changes to the version elements on an edit, but we + // have to be careful not to break the fancy N-page form when + // cvs_form_alter() is involved... + if (isset($edit->project->uri)) { + $project_name = $edit->project->uri; + } + elseif (isset($edit->pid)) { + $project_name = db_result(db_query("SELECT uri FROM {project_projects} WHERE nid = %d", $edit->pid)); + } + if (isset($edit->title)) { + // TODO: Magic re-setting to "%project_name %version" ?? + } + elseif (isset($edit->version) && $edit->version !== '') { + form_set_value($form['title'], t('%project %version', array('%project' => check_plain($project_name), '%version' => check_plain($edit->version)))); + } + elseif (isset($edit->project)) { + $version = project_release_get_version((object)$edit, $edit->project); + form_set_value(array('#parents' => array('version')), $version); + form_set_value($form['title'], t('%project %version', array('%project' => $edit->project->title, '%version' => $version))); + } +} + +/** + * Implementation of hook_load(). + * @ingroup project_release_node + */ +function project_release_load($node) { + $additions = db_fetch_object(db_query("SELECT * FROM {project_release_nodes} WHERE nid = %d", $node->nid)); + $tid = db_result(db_query("SELECT tn.tid FROM {term_node} tn INNER JOIN {term_data} td ON tn.tid = td.tid WHERE tn.nid = %d", $node->nid)); + $additions->version_api_tid = $tid; + return $additions; +} + +/** + * Implementation of hook_insert(). + * @ingroup project_release_node + */ +function project_release_insert($node) { + project_release_db_save($node, true); +} + +/** + * Implementation of hook_update(). + * @ingroup project_release_node + */ +function project_release_update($node) { + project_release_db_save($node, false); +} + +/** + * Helper method to take data out of a $node object and store it into + * the DB as necessary. Sadly, db_query() doesn't let us store NULL in + * the DB, since those get cast to 0. Therefore, we have to do some + * manual effort to dynamically create the appropriate SQL depending + * on which version fields are set in the release node. + * @see project_release_insert + * @see project_release_update + * @see db_query + * @ingroup project_release_internal + * + * @param $node Node object to save + * @param $is_new Is this a new release node, or are we updating? + */ +function project_release_db_save($node, $is_new) { + // If the patch field is set to a non-numeric value, we just want to + // keep it as a NULL in the DB, instead of casting it to a 0. + if (isset($node->version_patch) && !is_numeric($node->version_patch)) { + unset($node->version_patch); + } + $types = array( 'pid' => "%d", 'version' => "'%s'", 'tag' => "'%s'", + 'file_path' => "'%s'", 'file_date' => "%d", 'file_hash' => "'%s'", + 'rebuild' => "%d", + ); + $values = array( 'pid' => $node->pid, 'version' => $node->version, + 'tag' => $node->tag, 'file_path' => $node->file_path, + 'file_date' => $node->file_date, 'file_hash' => $node->file_hash, + 'rebuild' => $node->rebuild, + ); + $fields = array('version_major', 'version_minor', 'version_patch'); + foreach ($fields as $field) { + if (isset($node->$field) && is_numeric($node->$field)) { + $types[$field] = "%d"; + $values[$field] = $node->$field; + } + } + if (!empty($node->version_extra)) { + $types['version_extra'] = "'%s'"; + $values['version_extra'] = $node->version_extra; + } + + if ($is_new) { + $types['nid'] = "%d"; + $sql = 'INSERT INTO {project_release_nodes} ('. implode(', ', array_keys($types)) .') VALUES ('. implode(', ', $types) .')'; + } + else { + $arr = array(); + foreach ($types as $key => $value) { + $arr[] = $key .' = '. $value; + } + $sql = 'UPDATE {project_release_nodes} SET '. implode(',', $arr) .' WHERE nid = %d'; + } + $values['nid'] = $node->nid; + db_query($sql, $values); +} + +/** + * Implementation of hook_delete(). + * @ingroup project_release_node + */ +function project_release_delete($node) { + if ($node->file_path) { + file_delete(file_create_path($node->file_path)); + } + db_query("DELETE FROM {project_release_nodes} WHERE nid = %d", $node->nid); +} + + +/** + * @defgroup project_release_api Project release functions that other + * modules might want to use + */ + +/** + * Returns the version format string for a given project + * @ingroup project_release_api + */ +function project_release_get_version_format($project) { + if (!empty($project->version_format)) { + return $project->version_format; + } + + $db_format = db_result(db_query("SELECT version_format FROM {project_release_projects} WHERE nid = %d", $project->nid)); + if (!empty($db_format)) { + return $db_format; + } + + return variable_get('project_release_default_version_format', PROJECT_RELEASE_DEFAULT_VERSION_FORMAT); +} + +/** + * Returns the formatted version string for a given release node. + * @ingroup project_release_api + */ +function project_release_get_version($release, $project = NULL) { + if (isset($project)) { + $node = $project; + } + else { + $node->nid = $release->pid; + } + $variables = array(); + foreach (array('major', 'minor', 'patch', 'extra') as $field) { + $var = "version_$field"; + if (isset($release->$var) && $release->$var !== '') { + $variables["!$field"] = $release->$var; + $variables["%$field"] = '.' . $release->$var; + $variables["#$field"] = '-' . $release->$var; + } + else { + $variables["!$field"] = ''; + $variables["%$field"] = ''; + $variables["#$field"] = ''; + } + } + $variables["!api"] = ''; + $variables["%api"] = ''; + $variables["#api"] = ''; + $vid = _project_release_get_api_vid(); + if (project_release_get_api_taxonomy()) { + if (isset($release->version_api_tid)) { + $tid = $release->version_api_tid; + } + elseif (isset($release->$vid)) { + $tid = $release->$vid; + } + if (isset($tid)) { + $term = taxonomy_get_term($tid); + $variables["!api"] = $term->name; + $variables["%api"] = '.' . $term->name; + $variables["#api"] = '-' . $term->name; + } + } + $version_format = project_release_get_version_format($node); + return strtr($version_format, $variables); +} + +/** + * Implementation of hook_view(). + * @ingroup project_release_node + */ +function project_release_view(&$release, $teaser = FALSE, $page = FALSE) { + $project = node_load($release->pid); + + if ($page) { + // Breadcrumb navigation + $breadcrumb[] = array('path' => 'node/'. $project->nid, 'title' => $project->title); + $breadcrumb[] = array('path' => 'node/'. $node->nid, 'title' => $node->title); + $breadcrumb = project_project_set_location($project, $breadcrumb); + menu_set_location($breadcrumb); + } + + $links = array(); + if ($release->rebuild) { + $output .= t('Nightly development snapshot from %tag', array('%tag' => check_plain($release->tag))) . '
'; + } + if ($release->file_path) { + $output .= '' . t('Download: %file', array('%file' => l(basename($release->file_path), file_create_url($release->file_path)))) . '
'; + $output .= '' . t('Size: %size', array('%size' => format_size(filesize(file_create_path($release->file_path))))) . '
'; + $output .= '' . t('md5_file hash: %file_hash', array('%file_hash' => $release->file_hash)) . '
'; + } + if ($release->created) { + $output .= '' . t('First released: %created', array('%created' => format_date($release->created))) . '
'; + } + if ($release->changed && ($release->changed != $release->created)) { + $output .= '' . t('Last updated: %changed', array('%changed' => format_date($release->changed))) . '
'; + } + $output .= theme('links', $links); + + $release->body = '
' . $output . '
' . check_markup($release->body, $release->format); +} + +/** + * Display a list of releases for a given project + * @ingroup project_release_api + */ +function project_release_project_releases() { + $node = node_load(arg(1)); + + // Breadcrumb navigation + $breadcrumb[] = array('path' => 'node/'. $node->nid .'/', 'title' => $node->title); + $breadcrumb[] = array('path' => 'node/'. arg(1) .'/release', 'title' => t('Releases')); + $breadcrumb = project_project_set_location($node, $breadcrumb); + + menu_set_location($breadcrumb); + + $output = project_release_list($node); + + drupal_set_title(t('Releases for %project', array('%project' => theme('placeholder', $node->title)))); + return $output; +} + +/** + * Get an array of release nodes + * @ingroup project_release_api + */ +function project_release_get_releases($project, $nodes = true, $sort_by = 'version') { + if ($sort_by == 'date') { + $order_by = 'n.created'; + } + else { + $order_by = 'r.version'; + } + $result = db_query(db_rewrite_sql("SELECT n.nid, r.* FROM {node} n INNER JOIN {project_release_nodes} r ON r.nid = n.nid WHERE r.pid = %d AND n.status = 1 ORDER BY $order_by DESC"), $project->nid); + $releases = array(); + while ($obj = db_fetch_object($result)) { + if ($nodes) { + $releases[$obj->nid] = node_load($obj->nid); + } + else { + $releases[$obj->nid] = $obj->version; + } + } + return $releases; +} + + +/** + * @defgroup project_release_callback Menu callback functions + */ + +/** + * Returns a listing of project release nodes + * @ingroup project_release_callback + */ +function project_release_list($project, $main = 0) { + if ($releases = project_release_get_releases($project, 1, 'date')) { + foreach ($releases as $release) { + $output .= node_view($release); + } + } + else { + $output = t('There are no published releases for this project. Recently added releases will not be published until the packaging scripts have run.'); + } + return $output; +} + +/** + * Returns a listing of all active project release compatibility terms + * in the system. + * @ingroup project_release_api + */ +function project_release_compatibility_list() { + if ($tree = project_release_get_api_taxonomy()) { + // TODO: perhaps this could JOIN against some table or we'd have a + // setting to control which terms are "active" and should be available. + $terms[-1] = '<' . t('all') . '>'; + foreach ($tree as $term) { + $terms[$term->tid] = $term->name; + } + } + return $terms; +} + +/** + * Creates a form array for the "filter by version" selector when browsing + * projects on a site with 'project_release_browse_versions' enabled. + * @see project_page_overview + */ +function project_release_version_filter_form($version) { + if ($terms = project_release_compatibility_list()) { + $form['version_tid'] = array( + '#type' => 'select', + '#default_value' => $version, + '#options' => $terms, + ); + $form['submit'] = array( + '#type' => 'button', + '#value' => t('Go') + ); + } + return $form; +} + +function theme_project_release_version_filter_form($form) { + if (project_release_get_api_taxonomy()) { + $vocab = taxonomy_get_vocabulary(_project_release_get_api_vid()); + $label = $vocab->name; + } + else { + $label = t('version'); + } + $output = '
' . t('Filter by %label:', array('%label' => check_plain($label))) . form_render($form) . '
'; + return $output; +} + + +/** + * @defgroup project_release_fapi Form API hooks + */ + +/** + * Implementation of hook_form_alter(). + * @ingroup project_release_fapi + */ +function project_release_form_alter($form_id, &$form) { + if ($form_id == 'project_project_node_form') { + return project_release_alter_project_form($form); + } + if ($form_id == 'project_release_node_form') { + return project_release_alter_release_form($form); + } +} + +/** + * Alters the project_project node form to add release settings. + * @ingroup project_release_fapi + * @see project_release_form_alter + */ +function project_release_alter_project_form(&$form) { + $node = $form['#node']; + $form['release'] = array( + '#type' => 'fieldset', + '#title' => t('Release information'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + ); + $form['release']['releases'] = array( + '#type' => 'checkbox', + '#title' => t('Enable releases'), + '#return_value' => 1, + '#weight' => -10, + '#default_value' => isset($node->releases) ? $node->releases : 1, + '#description' => t('Allow releases of this project with specific versions.'), + ); + if ($releases = project_release_get_releases($node, false)) { + $form['release']['version'] = array( + '#type' => 'select', + '#title' => t('Default version'), + '#default_value' => $node->version, + '#options' => $releases, + '#description' => t('Default version for downloading.'), + ); + } + + // TODO: disable this form element if the "Enable releases" checkbox + // is not checked? + if (user_access('administer projects')) { + $form['release']['version_format'] = array( + '#type' => 'textfield', + '#title' => t('Version format string'), + '#default_value' => $node->version_format, + '#size' => 50, + '#maxlength' => 255, + '#description' => t('Customize the format of the version strings for releases of this project.') .' '. t('Available variables are:') .' %api, %major, %minor, %patch, %extra. '. t("The percent sign ('%') at the front of the variable name indicates that a period ('.') should be inserted as a delimiter before the value of the variable. The '%' can be replaced with a hash mark ('#') to use a hyphen ('-') delimiter, or with an exclaimation point ('!') to have the value printed without a delimiter. Any variable in the format string that has no value will be removed entirely from the final string.") .' '. t('If blank, this project will use the site-wide default (currently set to: %default)', array('%default' => theme('placeholder', variable_get('project_release_default_version_format', PROJECT_RELEASE_DEFAULT_VERSION_FORMAT)))), + ); + } +} + +/** + * Alters the project_release node form to handle the API taxonomy. + * If the vocabulary is empty, this removes the form elements. + * @ingroup project_release_fapi + * @see project_release_form_alter + */ +function project_release_alter_release_form(&$form) { + global $user; + $node = $form['#node']; + $vid = _project_release_get_api_vid(); + if (!project_release_get_api_taxonomy() && isset($form['taxonomy'][$vid])) { + unset($form['taxonomy'][$vid]); + } + else { + if (isset($node->pid)) { + $project->nid = $node->pid; + $tid = $node->version_api_tid; + } + elseif (arg(1) == 'add' && is_numeric(arg(3))) { + $project->nid = arg(3); + } + if (isset($project->nid)) { +/* + $format = project_release_get_version_format($project); + if (!preg_match('/[!#%]api/', $format)) { + $vid = _project_release_get_api_vid(); + if (isset($form['taxonomy'][$vid])) { + unset($form['taxonomy'][$vid]); + } + } + elseif (isset($tid) && !user_access('administer projects')) { +*/ + if (isset($tid) && !user_access('administer projects')) { + // We're editing a release node, the user doesn't have + // 'administer projects' permission, and we already have the + // api compatibility term, so we want to force it to stay. + $options[$tid] = $form['taxonomy'][$vid]['#options'][$tid]; + $form['taxonomy'][$vid]['#options'] = $options; + $form['taxonomy'][$vid]['#default_value'] = $tid; + } + } + } + // If there are no children elements, we should unset the entire + // thing so we don't end up with an empty fieldset. + if (!element_children($form['taxonomy'])) { + unset($form['taxonomy']); + } +} + + +/** + * @defgroup project_release_nodeapi Node API hooks + */ + +/** + * hook_nodeapi() implementation. This just decides what type of node + * is being passed, and calls the appropriate type-specific hook. + * @ingroup project_release_nodeapi + * @see project_release_project_nodeapi(). + */ +function project_release_nodeapi(&$node, $op, $arg) { + switch ($node->type) { + case 'project_project': + project_release_project_nodeapi($node, $op, $arg); + break; + } +} + +/** + * hook_nodeapi implementation specific to "project_project" nodes + * (from the project.module) + * @ingroup project_release_nodeapi + * @see project_release_nodeapi(). + */ +function project_release_project_nodeapi(&$node, $op, $arg) { + switch ($op) { + case 'load': + $project = db_fetch_object(db_query('SELECT * FROM {project_release_projects} WHERE nid = %d', $node->nid)); + foreach (array('releases', 'version_format') as $field) { + $node->$field = $project->$field; + } + break; + + case 'insert': + db_query("INSERT INTO {project_release_projects} (nid, releases, version_format) VALUES (%d, %d, '%s')", $node->nid, $node->releases, $node->version_format); + break; + + case 'update': + db_query("UPDATE {project_release_projects} SET releases = %d, version_format = '%s' WHERE nid = %d", $node->releases, $node->version_format, $node->nid); + break; + + case 'delete': + // TODO: unpublish (delete?) all release nodes associated with + // this project, too. + db_query('DELETE FROM {project_release_projects} WHERE nid = %d', $node->nid); + + } +} + +/** + * Returns the appropriate release download table for a project node. + * TODO: this should be a full-blown table + * TODO: this should be themeable. + */ +function project_release_project_download_table($node) { + $release = node_load($node->version); + $links[] = l(t('Download default release (%version, %date, %size)', array('%version' => $release->version, '%date' => format_date($release->file_date, 'small'), '%size' => format_size(filesize($release->file_path)))), 'node/' . $node->version); + $links[] = l(t('View all releases'), 'node/'. $node->nid .'/release'); + $links[] = l(t('Add new release'), 'node/add/project_release/'. $node->nid); + return theme('item_list', $links, t('Releases')); +} + +/** + * Implementation of hook_taxonomy(). + */ +function project_release_taxonomy($op, $type, $object = NULL) { + if ($op == 'delete' && $type == 'vocabulary' && $object->vid == _project_release_get_api_vid()) { + variable_del('project_release_api_vocabulary'); + } + elseif ($type == 'term' && $object->vid == _project_release_get_api_vid()) { + menu_rebuild(); + } +} + +/** + * If taxonomy is enabled, returns the taxonomy tree for the + * API compatibility vocabulary, otherwise, it returns false. + */ +function project_release_get_api_taxonomy() { + if (!module_exist('taxonomy')) { + return false; + } + return taxonomy_get_tree(_project_release_get_api_vid()); +} + +/** + * Returns the vocabulary id for project release API + */ +function _project_release_get_api_vid() { + $vid = variable_get('project_release_api_vocabulary', ''); + if (empty($vid)) { + // Check to see if a project release module vocabulary exists. + $vid = db_result(db_query("SELECT vid FROM {vocabulary} WHERE module='%s'", 'project_release')); + if (!$vid) { + $edit = array('name' => t('Project release API compatibility'), 'multiple' => 0, 'hierarchy' => 1, 'relations' => 0, 'module' => 'project_release', 'nodes' => array('project_release' => 1)); + // If there is already a vocabulary assigned to 'project_project' nodes, use it. + $vocabularies = taxonomy_get_vocabularies('project_release'); + if (count($vocabularies)) { + $vocabulary = reset($vocabularies); + $edit['vid'] = $vocabulary->vid; + } + taxonomy_save_vocabulary($edit); + $vid = $edit['vid']; + } + variable_set('project_release_api_vocabulary', $vid); + } + return $vid; +} + +function project_release_exists($version) { + $fields = array('version_major', 'version_minor', 'version_patch'); + foreach ($fields as $field) { + if (isset($version->$field) && is_numeric($version->$field)) { + $types[$field] = "%d"; + $values[$field] = $version->$field; + $foo = $version->$field; + } + } + $fields = array('version', 'version_extra'); + foreach ($fields as $field) { + if (isset($version->$field) && $version->$field !== '') { + $types[$field] = "'%s'"; + $values[$field] = $version->$field; + $str = $version->$field; + } + } + if (!isset($types)) { + // We have nothing to query, yet... + return false; + } + + if (isset($version->version_api_tid)) { + $taxo_join = ' INNER JOIN {term_node} t ON p.nid = t.nid'; + $taxo_where = ' AND t.tid = %d'; + $values['tid'] = $version->version_api_tid; + $tid = $version->version_api_tid; + } + + $sql = 'SELECT * FROM {project_release_nodes} p'. $taxo_join .' WHERE p.pid = %d'; + foreach ($types as $field => $type) { + $sql .= " AND p.$field = $type"; + } + $sql .= $taxo_where; + // we put pid as the first WHERE, so stick it on the front + $values = array_merge(array('pid' => $version->pid), $values); + return db_num_rows(db_query($sql, $values)); +} + Index: modules/project/project_release_scan.inc =================================================================== RCS file: modules/project/project_release_scan.inc diff -N modules/project/project_release_scan.inc --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ modules/project/project_release_scan.inc 15 Oct 2006 09:41:25 -0000 @@ -0,0 +1,143 @@ + 'admin/settings/project/reload', 'title' => t('scan for releases'), 'callback' => 'project_release_scan', 'access' => $access, 'type' => MENU_NORMAL_ITEM); + } + } +} + +/** + * This is not a real hook. Just saving this code since it really + * belongs in project_release_cron() if we ever want it to happen again. + */ +function project_release_scan_cron() { + if (variable_get('project_release_directory', '') && time() - variable_get('project_release_directory_last', 0) > variable_get('project_release_directory_interval', 6 * 60 * 60)) { + variable_set('project_release_directory_last', time()); + // TODO: should be able to optimize this to use less file system calls. + project_release_scan_directory(); + } +} + +function project_release_scan() { + project_release_scan_directory(); + drupal_set_title(t('Release directory scan')); + foreach (project_release_scan_directory_results() as $type => $count) { + drupal_set_message(t('Releases %type: %count.', array('%type' => $type, '%count' => $count))); + } + return t('Scan completed.'); +} + +function project_release_scan_directory($project = NULL) { + if ($dir = file_create_path(variable_get('project_release_directory', ''))) { + if ($project) { + $regexp = "($project)" .'-(.+)\.(tar.gz|zip)'; + } + else { + $regexp = '(.+)-(.+)\.(tar.gz|zip)'; + } + + file_scan_directory($dir, $regexp, array('.', '..', 'CVS'), 'project_release_scan_parse'); + + // If any releases were previously present but are no longer, unpublish them. + foreach (project_release_scan_parse() as $project) { + if ($project->releases) { + foreach ($project->releases as $rid) { + project_release_delete($rid); + project_release_scan_directory_results(t('unpublished')); + } + } + } + } + project_releases_list(TRUE); +} + +function project_release_scan_directory_results($type = NULL) { + static $results; + if ($type) { + if (!$results[$type]) { + $results[$type] = 1; + } + else { + $results[$type]++; + } + } + return $results; +} + +function project_release_scan_parse($path = NULL) { + static $projects = array(); + + if (is_null($path)) { + return $projects; + } + else { + $dir = dirname($path); + $file = basename($path); + //preg_match('/^(.+?)-([0-9.]+(?:-.*)|[^-]+)\.(tar\.gz|zip)$/', $file, $matches); + preg_match('/^(.+?)-([0-9.]+(?:-.*)|[^-]+)\.(tar.gz$|zip$)/', $file, $matches); + list($filename, $name, $version) = $matches; + + // If the project was not previously loaded, load its data, including previous releases. + if (!$projects[$name]) { + if ($project = db_fetch_object(db_query(db_rewrite_sql("SELECT n.nid, n.type, n.moderate FROM {node} n INNER JOIN {project_projects} p ON n.nid = p.nid WHERE p.uri = '%s'"), $name))) { + if ($releases = project_release_load($project, 0)) { + // Releases are loaded in an array where the key is the rid and the value the version. + // We flip the array to more readily test for releases. + $project->releases = array_flip($releases); + } + // Unmoderate nodes that have gotten releases + if (variable_get('project_release_unmoderate', 0) && $project->moderate) { + db_query("UPDATE {node} SET moderate = 0, status = 1 WHERE nid = %d", $project->nid); + } + } + else { + // No project found for this id. + return; + } + $projects[$name] = & $project; + } + else { + $project = & $projects[$name]; + } + + $release->scan = 1; + $release->nid = $project->nid; + // If the current version was previously released, add its rid value, so that + // it will be updated rather than inserted by project_release_save(); + if (is_array($project->releases) && array_key_exists($version, $project->releases)) { + $release->rid = $project->releases[$version]; + project_release_scan_directory_results(t('updated')); + } + else { + project_release_scan_directory_results(t('created')); + } + $release->version = $version; + $release->path = $path; + project_release_save($release); + // This release version has been resaved, so take it out of the array. + // That way, any releases no longer present can be unpublished in project_release_scan_directory(). + if ($project->releases[$version]) { + unset($project->releases[$version]); + } + } +} + + Index: modules/project/project_release_update.php =================================================================== RCS file: modules/project/project_release_update.php diff -N modules/project/project_release_update.php --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ modules/project/project_release_update.php 15 Oct 2006 09:41:25 -0000 @@ -0,0 +1,436 @@ +version_major . '-'; + if (isset($node->version_minor)) { + $tag .= $node->version_minor . '-'; + } + $tag .= $node->version_patch; + if (!empty($node->version_extra)) { + $tag .= strtoupper(preg_replace('/(.+)(\d+)/', '\1-\2', $node->version_extra)); + } + return $tag; +} + +/** + * Iterates through the {project_projects} table and adds the + * appropriate record to the {project_release_projects} table for each + * entry. + * + * BEWARE: This function contains drupal.org-specific code. Please + * modify the arrays and setting commented below to suit your own + * site's needs. + */ +function populate_project_release_projects() { + list($usec, $sec) = explode(' ', microtime()); + $start = (float)$usec + (float)$sec; + + $num_prp = db_result(db_query("SELECT count(nid) FROM {project_release_projects}")); + $num_projects = db_result(db_query("SELECT count(nid) FROM {project_projects}")); + if ($num_prp == $num_projects) { + print("The {project_release_projects} table is already full
"); + return; + } + // First, insert a record with the right nid for all projects + db_query("DELETE FROM {project_release_projects}"); + db_query("INSERT INTO {project_release_projects} (nid, releases) SELECT nid, 1 FROM {project_projects}"); + + // Now, special-cases we need to handle: + + // All the projects that will not want releases enabled + // BEWARE: drupal.org-specific + $no_release_projects = array( + 3202 => 'drupal.org maintenance', + 3213 => 'user experience', + 18753 => 'documentation', + 43378 => 'worldpay (ecommerce contrib)', + 67060 => 'event_views (event contrib)', + 67375 => 'location_views (location contrib)', + 75541 => 'inventorymangement (ecommerce contrib)', + ); + // Any projects with a custom version format string (just core) + // BEWARE: drupal.org-specific + $version_formats = array( + 3060 => '!major%minor%patch#extra', + 11093 => '!major%minor%patch#extra', + ); + // Set the right site-wide default for everything else... + // BEWARE: drupal.org-specific + variable_set('project_release_default_version_format', '!api#major%patch#extra'); + + foreach ($no_release_projects as $nid => $name) { + db_query("UPDATE {project_release_projects} SET releases = 0 WHERE nid = %d", $nid); + } + foreach ($version_formats as $nid => $format) { + db_query("UPDATE {project_release_projects} SET version_format = '%s' WHERE nid = %d", $format, $nid); + } + + $num_prp = db_result(db_query("SELECT count(nid) FROM {project_release_projects}")); + list($usec, $sec) = explode(' ', microtime()); + $stop = (float)$usec + (float)$sec; + $diff = round(($stop - $start) * 1000, 2); + print t('Added %num records to the {project_release_projects} table in %ms ms
', array('%num' => $num_prp, '%ms' => $diff)); +} + +/** + * Iterates through the entire {project_releases} table and converts + * each entry into a new release node. + */ +function convert_all_releases() { + // First, re-load into memory mappings that we completed on previous runs + global $nids_by_rid, $api_taxonomy; + $api_taxonomy = project_release_get_api_taxonomy(); + + $query = db_query("SELECT nid, rid FROM {project_release_legacy}"); + while ($result = db_fetch_object($query)) { + $nids_by_rid[$result->rid] = $result->nid; + if (module_exist('project_issue')) { + db_query("UPDATE {project_issues} SET rid = %d WHERE rid = %d", $result->nid, $result->rid); + } + } + $num = 0; + $start_time = time(); + + // We desperately need an index on rid for the {project_issues} + // table while doing the conversion, otherwise we spend between + // 30-60 minutes with the UPDATE to repair the rid. With this key, + // it only takes about 1 minute on the whole d.o DB. + db_query("ALTER TABLE {project_issues} ADD KEY rid (rid)"); + + $releases = db_query("SELECT pr.*, n.uid, n.title AS project_title FROM {project_releases} pr INNER JOIN {node} n ON pr.nid = n.nid LEFT JOIN {project_release_legacy} prl ON pr.rid = prl.rid WHERE prl.rid IS NULL"); + while ($old_release = db_fetch_object($releases)) { + convert_release($old_release); + $num++; + } + + print t('Converted %num releases into nodes in %interval
', array('%num' => $num, '%interval' => format_interval(time() - $start_time))); +} + +/** + * Determines if a given entry in the {project_releases} table + * corresponds to a real release, or a nightly development snapshot, + * and converts it into the appropriate kind of release node. + * + * BEWARE: This is drupal.org specific code. In our case, the core + * drupal system (project nid #3060) is the only project that's ever + * had "real" releases so far. Everything else has been a nightly dev + * snapshot release. If your site has a different usage, please modify + * the logic in here to meet your needs. + */ +function convert_release($old_release) { + list($usec, $sec) = explode(' ', microtime()); + $start = (float)$usec + (float)$sec; + + global $nids_by_rid, $api_taxonomy; + + // First, save everything that's shared, regardless of the version/type + + // Things that go in {node} or {node_revisions} + $node->type = 'project_release'; + $node->uid = $old_release->uid ? $old_release->uid : 1; + $node->created = $old_release->created; + $node->changed = $old_release->changed; + $node->body = $old_release->changes ? $old_release->changes : ''; + $node->teaser = node_teaser($node->body); + $node->filter = variable_get('filter_default_format', 1); + $node->status = $old_release->status; + $node->revision = 1; + $node->promote = 0; + $node->comment = 0; + + // Things that go in {project_release_nodes} (that don't depend on version) + $node->pid = $old_release->nid; + $node->file_path = $old_release->path; + $node->file_date = $old_release->changed; + $node->file_hash = $old_release->hash; + + // Now, depending on the project and version, fill in the rest. + if ($old_release->nid == 3060) { + if ($old_release->version == 'cvs') { + // TODO: maybe we should just leave this "release" alone, + $node->version_major = 5; + $node->version_patch = 0; + $node->version_extra = 'dev'; + $node->rebuild = 1; + $node->tag = 'HEAD'; + $target_api = '5.x'; + } + else { + preg_match('/(\d+)\.(\d+)\.(\d+)(-.+)?/', $old_release->version, $matches); + $node->version_major = $matches[1]; + $node->version_minor = $matches[2]; + $node->version_patch = $matches[3]; + $node->version_extra = $matches[4]; + $node->tag = generate_core_tag($node); + $node->rebuild = 0; + $target_api = "$matches[1].$matches[2].x"; + } + } + elseif ($old_release->version == 'cvs') { + // The "cvs" version is a nightly tarball from the trunk + $node->version_major = 0; + $node->version_patch = 0; + $node->version_extra = 'dev'; + $node->tag = 'HEAD'; + $node->rebuild = 1; + } + else { + // Nightly tarball from a specific branch. + preg_match('/(\d+)\.(\d+)\.(\d+)/', $old_release->version, $matches); + if ($matches[3] != 0) { + print("warning: release $old_release->rid of $old_release->project_title has unexpected patch-level version ($matches[3])
"); + } + $target_api = "$matches[1].$matches[2].x"; + $node->version_major = 0; + $node->version_patch = 0; + $node->version_extra = 'dev'; + $node->tag = 'DRUPAL-' . $matches[1] . '-' . $matches[2]; + $node->rebuild = 1; + } + + if (isset($target_api)) { + foreach ($api_taxonomy as $i => $term) { + if ($term->name == $target_api) { + $node->taxonomy[$term->tid] = $term->tid; + $node->version_api_tid = $term->tid; + break; + } + } + } + + // Now, set the right kind of title. + $version = ''; + if ($node->tag == 'HEAD') { + $version = t('HEAD'); + } + else { + $version = project_release_get_version($node); + } + $node->title = t('%project %version', array('%project' => $old_release->project_title, '%version' => $version)); + $node->version = $version; + + list($usec, $sec) = explode(' ', microtime()); + $pre_save = (float)$usec + (float)$sec; + // Now, we can actually create the node. + node_save($node); + list($usec, $sec) = explode(' ', microtime()); + $post_save = (float)$usec + (float)$sec; + + // Grab these values for a few additional conversions. + $nid = $node->nid; + $rid = $old_release->rid; + + // While we're iterating over all the old releases, we can already + // convert all the project_issue nodes to the new value. We'll have + // to fix all the followup comments only after we have the complete + // mapping of rid -> nid + if (module_exist('project_issue')) { + list($usec, $sec) = explode(' ', microtime()); + $pre_update = (float)$usec + (float)$sec; + db_query("UPDATE {project_issues} SET rid = %d WHERE rid = %d", $nid, $rid); + list($usec, $sec) = explode(' ', microtime()); + $post_update = (float)$usec + (float)$sec; + } + + // Keep track of it in our array in RAM for converting issue comments + $nids_by_rid[$rid] = $nid; + + // See how long it took. + list($usec, $sec) = explode(' ', microtime()); + $stop = (float)$usec + (float)$sec; + $diff = round(($stop - $start) * 1000, 2); + + $save_diff = round(($post_save - $pre_save) * 1000, 2); + $update_diff = round(($post_update - $pre_update) * 1000, 2); + + // Finally, add an entry to the {project_release_legacy} table so we + // know the mapping of the old rid to the new nid. + db_query("INSERT INTO {project_release_legacy} (rid, nid, pid, time, save_ms, update_ms) VALUES (%d, %d, %d, %d, %d, %d)", $rid, $nid, $old_release->pid, $diff, $save_diff, $update_diff); +} + +function convert_issue_followups() { + if (!module_exist('project_issue')) { + return; + } + global $nids_by_rid; + $start_time = time(); + $num = 0; + $errors = 0; + + $query = db_query("SELECT pc.*, pi.pid FROM {project_comments} pc INNER JOIN {project_issues} pi ON pc.nid = pi.nid WHERE pc.data RLIKE 'rid'"); + while ($comment = db_fetch_object($query)) { + $error_old = 0; + $error_new = 0; + $data = unserialize($comment->data); + $old_rid = $data['old']->rid; + $new_rid = $data['new']->rid; + if ($old_rid) { + if (isset($nids_by_rid[$old_rid])) { + $data['old']->rid = $nids_by_rid[$old_rid]; + } + else { + $error_old = $old_rid; + $data['old']->rid = 0; + } + } + if ($new_rid) { + if (isset($nids_by_rid[$new_rid])) { + $data['new']->rid = $nids_by_rid[$new_rid]; + } + else { + $error_new = $new_rid; + $data['new']->rid = 0; + } + } + if ($error_old || $error_new) { + // Evil, this comment refers to a rid that wasn't in + // {project_releases}, record it for post-mortem analysis... + db_query("INSERT INTO {project_comments_conversion_errors} (cid, pid, old_rid, new_rid) VALUES (%d, %d, %d, %d)", $comment->cid, $comment->pid, $error_old, $error_new); + $errors++; + } + db_query("UPDATE {project_comments} SET data = '%s' WHERE cid = %d", serialize($data), $comment->cid); + $num++; + } + print t('Converted %num issue followups in %interval', array('%num' => $num, '%interval' => format_interval(time() - $start_time))) . '
'; + if ($errors) { + print '' . t('ERROR: problem during conversion of %num issue followups', array('%num' => $errors)) . '
'; + } +} + +function create_legacy_tables() { + switch ($GLOBALS['db_type']) { + case 'mysql': + case 'mysqli': + db_query("CREATE TABLE IF NOT EXISTS {project_release_legacy} ( + rid int(10) unsigned NOT NULL default '0', + nid int(10) unsigned NOT NULL default '0', + pid int(10) unsigned NOT NULL default '0', + time int(10) unsigned NOT NULL default '0', + save_ms int(10) unsigned NOT NULL default '0', + update_ms int(10) unsigned NOT NULL default '0', + PRIMARY KEY (`rid`), + KEY project_release_legacy_pid (`pid`), + KEY project_release_legacy_nid (`nid`) + ) TYPE=MyISAM + /*!40100 DEFAULT CHARACTER SET utf8 */;"); + db_query("CREATE TABLE IF NOT EXISTS {project_comments_conversion_errors} ( + cid int(10) unsigned NOT NULL default '0', + pid int(10) unsigned NOT NULL default '0', + old_rid int(10) unsigned NOT NULL default '-1', + new_rid int(10) unsigned NOT NULL default '-1', + PRIMARY KEY (`cid`), + KEY project_comments_conversion_errors_pid (`pid`), + KEY project_comments_conversion_errors_old_rid (`old_rid`), + KEY project_comments_conversion_errors_new_rid (`new_rid`) + ) TYPE=MyISAM + /*!40100 DEFAULT CHARACTER SET utf8 */;"); + break; + case 'pgsql': + if (!project_db_table_exists('project_release_legacy')) { + db_query("CREATE TABLE {project_release_legacy} ( + rid int(10) unsigned NOT NULL default '0', + nid int(10) unsigned NOT NULL default '0', + pid int(10) unsigned NOT NULL default '0', + time int(10) unsigned NOT NULL default '0', + save_ms int(10) unsigned NOT NULL default '0', + update_ms int(10) unsigned NOT NULL default '0', + PRIMARY KEY (`rid`), + KEY project_release_legacy_pid (`pid`), + KEY project_release_legacy_nid (`nid`) + );"); + } + if (!project_db_table_exists('project_comments_conversion_errors')) { + db_query("CREATE TABLE {project_comments_conversion_errors} ( + cid int(10) unsigned NOT NULL default '0', + pid int(10) unsigned NOT NULL default '0', + old_rid int(10) unsigned NOT NULL default '-1', + new_rid int(10) unsigned NOT NULL default '-1', + PRIMARY KEY (`cid`), + KEY project_comments_conversion_errors_pid (`pid`), + KEY project_comments_conversion_errors_old_rid (`old_rid`), + KEY project_comments_conversion_errors_new_rid (`new_rid`) + );"); + } + break; + } +} + +function populate_project_release_api_taxonomy() { + $vid = _project_release_get_api_vid(); + + // First, customize the vocabulary itself for our needs: + $vocab['vid'] = $vid; + $vocab['name'] = 'Drupal Core compatibility'; + $vocab['nodes']['project_release'] = 1; + $vocab['help'] = 'Specify what version of Drupal Core this release is compatible with.'; + $vocab['hierarchy'] = 0; + $vocab['required'] = 1; + $vocab['weight'] = -5; + taxonomy_save_vocabulary($vocab); + + // Now, populate the terms we'll need: + $terms[] = '5.x'; + for ($i=7; $i>=0; $i--) { + $terms[] = "4.$i.x"; + } + foreach ($terms as $weight => $name) { + $edit = array(); + $edit['vid'] = $vid; + $edit['name'] = $name; + $edit['description'] = "Releases of Drupal contributions that are compatible with version $name of Drupal Core"; + $edit['weight'] = $weight; + taxonomy_save_term($edit); + } +} + + +/* + *------------------------------------------------------------ + * Real work of this script + *------------------------------------------------------------ + */ +include_once './includes/bootstrap.inc'; +drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL); + +// If not in 'safe mode', increase the maximum execution time: +if (!ini_get('safe_mode')) { + set_time_limit(2000); +} + +if (!module_exist('project_release')) { + print '' . t('ERROR: project_release_update.php requires that you first install the project_release.module') . ''; + exit(1); +} + +// Pull in the copy of project_db_table_exists() +$path = drupal_get_path('module', 'project'); +if (file_exists("$path/project.install")) { + require_once "$path/project.install"; +} + +$nids_by_rid = array(); + +populate_project_release_api_taxonomy(); + +create_legacy_tables(); + +populate_project_release_projects(); + +convert_all_releases(); + +convert_issue_followups(); + +// TODO: more user feedback, progress, etc. +// TODO: LOCK relevant tables during conversion? Index: modules/project/release.inc =================================================================== RCS file: modules/project/release.inc diff -N modules/project/release.inc --- modules/project/release.inc 4 Aug 2006 03:19:50 -0000 1.70.2.3 +++ /dev/null 1 Jan 1970 00:00:00 -0000 @@ -1,437 +0,0 @@ - $count) { - drupal_set_message(t('Releases %type: %count.', array('%type' => $type, '%count' => $count))); - } - return t('Scan completed.'); -} - -function project_release_scan_directory($project = NULL) { - if ($dir = file_create_path(variable_get('project_release_directory', ''))) { - if ($project) { - $regexp = "($project)" .'-(.+)\.(tar.gz|zip)'; - } - else { - $regexp = '(.+)-(.+)\.(tar.gz|zip)'; - } - - file_scan_directory($dir, $regexp, array('.', '..', 'CVS'), 'project_release_scan_parse'); - - // If any releases were previously present but are no longer, unpublish them. - foreach (project_release_scan_parse() as $project) { - if ($project->releases) { - foreach ($project->releases as $rid) { - project_release_delete($rid); - project_release_scan_directory_results(t('unpublished')); - } - } - } - } - project_releases_list(TRUE); -} - -function project_release_scan_directory_results($type = NULL) { - static $results; - if ($type) { - if (!$results[$type]) { - $results[$type] = 1; - } - else { - $results[$type]++; - } - } - return $results; -} - -function project_release_scan_parse($path = NULL) { - static $projects = array(); - - if (is_null($path)) { - return $projects; - } - else { - $dir = dirname($path); - $file = basename($path); - //preg_match('/^(.+?)-([0-9.]+(?:-.*)|[^-]+)\.(tar\.gz|zip)$/', $file, $matches); - preg_match('/^(.+?)-([0-9.]+(?:-.*)|[^-]+)\.(tar.gz$|zip$)/', $file, $matches); - list($filename, $name, $version) = $matches; - - // If the project was not previously loaded, load its data, including previous releases. - if (!$projects[$name]) { - if ($project = db_fetch_object(db_query(db_rewrite_sql("SELECT n.nid, n.type, n.moderate FROM {node} n INNER JOIN {project_projects} p ON n.nid = p.nid WHERE p.uri = '%s'"), $name))) { - if ($releases = project_release_load($project, 0)) { - // Releases are loaded in an array where the key is the rid and the value the version. - // We flip the array to more readily test for releases. - $project->releases = array_flip($releases); - } - // Unmoderate nodes that have gotten releases - if (variable_get('project_release_unmoderate', 0) && $project->moderate) { - db_query("UPDATE {node} SET moderate = 0, status = 1 WHERE nid = %d", $project->nid); - } - } - else { - // No project found for this id. - return; - } - $projects[$name] = & $project; - } - else { - $project = & $projects[$name]; - } - - $release->scan = 1; - $release->nid = $project->nid; - // If the current version was previously released, add its rid value, so that - // it will be updated rather than inserted by project_release_save(); - if (is_array($project->releases) && array_key_exists($version, $project->releases)) { - $release->rid = $project->releases[$version]; - project_release_scan_directory_results(t('updated')); - } - else { - project_release_scan_directory_results(t('created')); - } - $release->version = $version; - $release->path = $path; - project_release_save($release); - // This release version has been resaved, so take it out of the array. - // That way, any releases no longer present can be unpublished in project_release_scan_directory(). - if ($project->releases[$version]) { - unset($project->releases[$version]); - } - } -} - -function project_release_cron() { - // TODO: should be able to optimize this to use less file system calls. - if ($dir = variable_get('project_release_directory', '')) { - project_release_scan_directory(); - } -} - -function project_release_submit() { - switch ($_POST['op'] ? $_POST['op'] : arg(3)) { - case 'add': - $node = node_load(arg(1)); - $breadcrumb = array(l(t('Home'), NULL), l(t('Projects'), 'project')); - if (project_use_taxonomy()) { - $taxonomy_terms = taxonomy_node_get_terms($node->nid); - $term = reset($taxonomy_terms); - $breadcrumb[] = l($term->name, 'project', NULL, "tid=$term->tid"); - } - $breadcrumb[] = l($node->title, "node/$node->nid"); - $error = 1; - $release->nid = $node->nid; - $form = project_release_form($release, $param); - $output = drupal_get_form('project_release_form', $form); - - drupal_set_title(t('Add release to %project', array('%project' => $node->title))); - drupal_set_breadcrumb($breadcrumb); - return $output; - break; - case 'edit': - if ($release = project_release_load(arg(4))) { - $node = node_load($release->nid); - $output = project_release_view($release); - $form = project_release_form($release, $param); - $output = drupal_get_form('project_release_form', $form); - drupal_set_title(t('Edit release %version for %project', array('%version' => $release->version, '%project' => $node->title))); - return $output; - } - break; - case 'delete': - if (($release = project_release_load(arg(4))) && $release->nid == arg(1)) { - project_release_delete(arg(4)); - drupal_goto("node/$release->nid/release"); - } - break; - case t('Submit'): - $edit = (object) $_POST['edit']; - $node = node_load($edit->nid); - if (node_access('update', $node)) { - project_release_validate($edit); - if (form_get_errors()) { - $form = project_release_form($release, $param); - $output = drupal_get_form('project_release_form', $form); - return $output; - } - else { - // Save the release and re-direct to the overview page for the project - $edit->status = 1; - project_release_save($edit); - drupal_goto("node/$node->nid/release"); - } - } - } -} - -/** - * Implementation of hook_form(). - */ -function project_release_form(&$release, &$param) { - /* TODO: pending final fileapi - // Set form parameters so we can accept file uploads. - $param['options'] = array('enctype' => 'multipart/form-data'); - */ - - $form['version'] = array( - '#type' => 'textfield', - '#title' => t('Version'), - '#default_value' => $release->version, - '#size' => 40, - '#maxlength' => 255, - '#required' => TRUE, - ); - $form['changes'] = array( - '#type' => 'textarea', - '#title' => t('Changes'), - '#default_value' => $release->changes, - '#rows' => 10, - '#cols' => 40, - '#required' => TRUE, - ); - if (!variable_get('project_release_directory', '')) { - $form['path'] = array( - '#type' => 'file', - '#title' => t('File'), - '#size' => 40, - '#description' => ($release->fid) ? - t('A file already exists, if you upload another file the current file will be replaced.') : - '' . $error['file'], - '#required' => $release->fid, - ); - } - if ($release->rid) { - $form['rid'] = array( - '#type' => 'hidden', - '#default_value' => $release->rid, - ); - } - $form['nid'] = array( - '#type' => 'hidden', - '#default_value' => $release->nid, - ); - - if (!form_get_errors()) { - $form['submit'] = array( - '#type' => 'button', - '#value' => t('Submit'), - ); - } - - return $form; -} - -function project_release_validate(&$edit) { - if ($edit->rid) { - $release = project_release_load($edit->rid); - $edit->nid = $release->nid; - $edit->fid = $release->fid; - } - - if (empty($edit->version)) { - form_set_error('version', t('You must specify a valid version.')); - } - else { - $result = db_query("SELECT * FROM {project_releases} WHERE nid = %d AND version = '%s'", $edit->nid, trim($edit->version)); - if ($release = db_fetch_object($result)) { - if ($edit->rid != $release->rid) { - if ($release->status) { - form_set_error('version', t('This version already exists for this project.')); - } - else { - $edit->rid = $release->rid; - } - } - } - } - - if (empty($edit->changes)) { - form_set_error('changes', t('You must specify changes.')); - } - - /* TODO: pending final fileapi - $file = check_file('file'); - - if ($file && !$file->fid && !empty($file->error)) { - $error['file'] = theme('error', $file->error); - } - else { - $edit->fid = $file->fid; - } - */ -} - -function project_release_view($release, $project = 0) { - if (!$project) { - $project = node_load($release->nid); - } - - $links = array(); - if ($release->rid && node_access('update', $project)) { - $links[] = l('edit', "node/$project->nid/release/edit/$release->rid"); - //$links[] = l('delete', "node/$project->nid/release/delete/$release->rid"); - } - - if ($release->path) { - $output .= '' . t('Download: %path', array('%path' => base_path() . $release->path)) . '
'; - $output .= '' . t('Size: %size', array('%size' => format_size(filesize($release->path)))) . '
'; - $output .= '' . t('md5_file hash: %hash', array('%hash' => $release->hash)) . '
'; - } - if ($release->created) { - $output .= '' . t('First released: %created', array('%created' => format_date($release->created))) . '
'; - } - if ($release->changed && ($release->changed != $release->created)) { - $output .= '' . t('Last updated: %changed', array('%changed' => format_date($release->changed))) . '
'; - } - - $output .= $release->changes; - $output .= theme('links', $links); - - return $output; -} - -function project_release_list($project, $main = 0) { - $result = db_query('SELECT * FROM {project_releases} WHERE nid = %d AND status = 1 ORDER BY created DESC', $project->nid); - if (db_num_rows($result)) { - $header = array(t('Version'), t('Released'), t('Changes')); - if (node_access('update', $project) || node_access('delete', $project)) { - $header[] = array('data' => t('Operations'), 'colspan' => 2); - } - while ($release = db_fetch_object($result)) { - $output .= theme('box', l(t('%project %version', array('%project' => $project->title, '%version' => $release->version)), 'node/'. $project->nid .'/release', array('id' => 'version-'. $release->version), NULL, 'version-'. $release->version), project_release_view($release, $project)); - } - return $output; - } -} - -function project_release_load($rid, $status = 1) { - if ($rid->type == 'project_project') { - $status = $status ? 'AND status = 1' : ''; - $result = db_query("SELECT rid,version FROM {project_releases} WHERE nid = %d $status ORDER BY version DESC", $rid->nid); - $releases = array(); - while ($release = db_fetch_object($result)) { - $releases[$release->rid] = $release->version; - } - return $releases; - } - elseif (is_numeric($rid)) { - return db_fetch_object(db_query('SELECT * FROM {project_releases} WHERE rid = %d ORDER BY created DESC', $rid)); - } -} - -function project_release_save($release) { - $fields = array('rid' => '%d', 'nid' => '%d', 'fid' => '%d', - 'path' => "'%s'", 'created' => '%d', - 'changed' => '%d', 'hash' => "'%s'", 'version' => "'%s'", - 'changes' => "'%s'", 'status' => '%d'); - - $node = node_load($release->nid); - - /* TODO: pending final fileapi - $file = check_file('file'); - */ - // If the version was not previously released, insert it. - if (empty($release->rid)) { - - // Make sure this release version doesn't already exist for the given project. - // This test should not be needed, but duplicate releases on drupal.org suggest - // that an unidentified problem is causing duplicate versions to be saved. - if (db_num_rows(db_fetch_object(db_query("SELECT rid FROM {project_releases} WHERE nid = %d AND version = '%s'", $release->nid, $release->version)))) { - return FALSE; - } - $release->rid = db_next_id('project_rid'); - if (!variable_get('project_release_directory', '') && $file->filename) { - $release->fid = drupal_file_save($file); - } - if ($release->path) { - $release->created = ($release->version == 'cvs') ? 0 : filectime($release->path); - $release->changed = filemtime($release->path); - $release->hash = md5_file($release->path); - } - else { - $release->created = ($release->version == 'cvs') ? 0 : time(); - $release->changed = time(); - } - - // Prepare the query: - foreach ($release as $key => $value) { - if (in_array($key, array_keys($fields))) { - $input[db_escape_string($key)] = $fields[$key]; - $values[] = $value; - } - } - - // Insert the node into the database: - db_query('INSERT INTO {project_releases} ('. implode(', ', array_keys($input)) .') VALUES('. implode(', ', $input) .')', $values); - watchdog('project', t("added release %version to %project.", array('%version' => theme('placeholder', $release->version), '%project' => theme('placeholder', $node->title)))); - } - // Otherwise, update the existing record. - else { - /* TODO: pending final fileapi - // Replace current file if new file upload exists. - if (!variable_get('project_release_directory', '') && $file->filename) { - $file->fid = $release->fid; - drupal_file_save($file); - } - */ - - unset($release->created); - if($release->path) { - if ($release->scan) { - // Load previous md5_file hash value, and test it agains the current one to see if the file has changed. - $previous = db_fetch_object(db_query("SELECT hash FROM {project_releases} WHERE rid = %d", $release->rid)); - if (md5_file($release->path) != $previous->hash) { - $release->hash = md5_file($release->path); - $release->changed = time(); - } - } - else { - $release->hash = md5_file($release->path); - $release->changed = filemtime($release->path); - } - } - - // Prepare the query: - foreach ($release as $key => $value) { - - if (in_array($key, array_keys($fields))) { - $q[] = db_escape_string($key) ." = " . $fields[$key]; - $v[] = $value; - } - } - $v[] = $release->nid; - $v[] = $release->rid; - - db_query('UPDATE {project_releases} SET '. implode(', ', $q) .' WHERE nid = %d AND rid = %d', $v); - watchdog('project', t("updated release %project %version", array('%project' => theme('placeholder', $node->title), '%version' => theme('placeholder', $release->version)))); - } - return $release->rid; -} - -function project_release_delete($rid) { - $release = project_release_load($rid); - watchdog('project', t("unpublished release %version", array('%version' => theme('placeholder', $release->version)))); - db_query('UPDATE {project_releases} SET status = 0 WHERE rid = %d', $rid); -} - -function project_releases_list($refresh = FALSE) { - if (!$refresh && ($versions = cache_get('project_releases'))) { - $versions = unserialize($versions->data); - } - else { - $result = db_query("SELECT version FROM {project_releases} WHERE status = 1 GROUP BY version, created ORDER BY created DESC;"); - $versions = array(); - while ($object = db_fetch_object($result)) { - $version = preg_replace('/^(.+?)\.?(?:\d+)?$/', '\1', $object->version); - $versions[$version] = $version == $object->version ? $version : $version .'.x'; - } - cache_set('project_releases', serialize($versions)); - menu_rebuild(); - } - - return $versions; -} cvs diff: Diffing modules/project/po cvs diff: Diffing modules/project/po/ar