Index: l10n_packager/l10n_packager.admin.inc =================================================================== RCS file: l10n_packager/l10n_packager.admin.inc diff -N l10n_packager/l10n_packager.admin.inc --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ l10n_packager/l10n_packager.admin.inc 7 Oct 2009 13:28:42 -0000 @@ -0,0 +1,176 @@ + t('Generate packages on every Drupal cron run'), + '#type' => 'checkbox', + '#default_value' => variable_get('l10n_packager_cron', 0), + '#description' => t("You can opt to run l10n_packager_cli.php to do packaging outside Drupal's cron system to decouple packaging from Drupal's cron. However, you need to follow manual setup instructions in the module README.txt in that case."), + ); + $form['l10n_packager_directory'] = array( + '#title' => t('Directory for generated packages'), + '#description' => t('The directory on the local file system to use to store packages generated. Either relative to the Drupal site files or an absolute path on your file system. Drupal should have read and write access to the files and directories found there.'), + '#type' => 'textfield', + '#required' => TRUE, + '#default_value' => variable_get('l10n_packager_directory', 'l10n_packager'), + // Create directory by default if possible. + '#after_build' => array('l10n_packager_admin_check_directory'), + ); + $form['l10n_packager_format'] = array( + '#title' => t('Package format'), + '#type' => 'select', + '#options' => array('drupal-6' => t('Drupal 6 package format'), 'drupal-5' => t('Drupal 5 package format for autolocale module'), 'flat-package' => t('Flat package for CVS commit'), 'all-in-one' => t('All in one file')), + '#default_value' => variable_get('l10n_packager_format', 'all-in-one'), + '#description' => t("Drupal 5's autolocale module and Drupal 6 use different conventions for directory naming in packages. Temporarily, as long as you need to use CVS to distribute translations the flat package helps you generate a package for committing to Drupal.org. Additionally, for testing, spell checking and smaller modules, you can also export in one file without packaging as well. It is not advised to use big .po files for importing in Drupal though, as it might lead to incomplete imports. Use packages for usage with Drupal importing."), + ); + $form['l10n_packager_check'] = array( + '#title' => t('Number of releases to check at once'), + '#description' => t('The number of releases to check on a manual or cron run.'), + '#type' => 'select', + '#options' => drupal_map_assoc(array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 20, 30, 40, 50)), + '#default_value' => variable_get('l10n_packager_check', 10), + ); + $form['l10n_packager_file_limit'] = array( + '#title' => t('Maximum number of files to package at once'), + '#description' => t('The number of files to package on a manual or cron run.'), + '#type' => 'select', + '#options' => drupal_map_assoc(array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 20, 30, 40, 50)), + '#default_value' => variable_get('l10n_packager_file_limit', 1), + ); + // Logging settings + $period = array(0 => t('Never')) + drupal_map_assoc(array(3600, 10800, 21600, 32400, 43200, 86400, 172800, 259200, 604800, 1209600, 2419200, 4838400, 9676800), 'format_interval'); + $form['l10n_packager_update'] = array( + '#title' => t('Repackaging interval'), + '#type' => 'select', + '#options' => $period, + '#default_value' => variable_get('l10n_packager_update', 0), + '#description' => t('Time interval for the translations to be automatically repackaged.'), + ); + + return system_settings_form($form); +} + +/** + * Check/create directory if not there already. + */ +function l10n_packager_admin_check_directory($form_element) { + file_check_directory(file_create_path($form_element['#value']), FILE_CREATE_DIRECTORY, $form_element['#parents'][0]); + return $form_element; +} + +/** + * Form callback. Repackage translations manually. + */ +function l10n_packager_admin_repackage_form() { + $form['help']['#value'] = t('Repackage all translations or a single project or release.'); + $form['project'] = array( + '#title' => t('Project'), + '#type' => 'textfield', + '#autocomplete_path' => 'translate/projects/autocomplete', + '#description' => t('Type a project URI. Example: drupal'), + ); + $form['release'] = array( + '#title' => t('Release'), + '#type' => 'textfield', + //'#autocomplete_path' => 'translate/project_releases/autocomplete', + '#description' => t('Optionally select a release name like 6.x-1.0-beta1 or a partial release name like 6.x%.'), + ); + $form['lang-wrapper'] = array( + '#type' => 'fieldset', + '#title' => t('Restrict languages'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#description' => t('Select none for all languages. Otherwise check the languages you want repackaged.'), + ); + $form['lang-wrapper']['languages'] = array( + '#type' => 'checkboxes', + '#default_value' => array(), + '#options' => l10n_community_get_languages('name'), + ); + + $form['buttons']['repackage'] = array('#type' => 'submit', '#value' => t('Repackage now')); + $form['buttons']['mark'] = array('#type' => 'submit', '#value' => t('Mark for repackaging')); + return $form; +} + +/** + * Repackage form validation + */ +function l10n_packager_admin_repackage_form_validate($form, &$form_state) { + if (!empty($form_state['values']['project'])) { + if ($project = l10n_community_get_projects(array('uri' => $form_state['values']['project']))) { + $form_state['values']['pid'] = $project->pid; + } + else { + form_set_error('project', t('Invalid project name.')); + } + } + if (!empty($form_state['values']['release'])) { + $query = "SELECT COUNT(rid) FROM {l10n_community_release} WHERE title LIKE '%s'"; + $args = array($form_state['values']['release']); + if ($project) { + $query .= " AND pid = %d"; + $args[] = $project->pid; + } + if (!db_result(db_query($query, $args))) { + form_set_error('release', t('Invalid release name.')); + } + } + if (empty($form_state['values']['project']) && empty($form_state['values']['release'])) { + form_set_error('', t('You must select a project and optionally a release')); + } +} + +/** + * Repackage form submission + */ +function l10n_packager_admin_repackage_form_submit($form, $form_state) { + module_load_include('inc', 'l10n_packager'); + + $op = $form_state['values']['op']; + $languages = array_filter($form_state['values']['languages']); + $pid = !empty($form_state['values']['pid']) ? $form_state['values']['pid'] : NULL; + $release = !empty($form_state['values']['release']) ? $form_state['values']['release'] : NULL; + + // Prepare search parameters + $where = $args = array(); + if ($pid) { + $where[] = "pid = %d"; + $args[] = $pid; + } + if ($release) { + $where[] = "title LIKE '%s'"; + $args[] = $release; + } + // Build the query that will be used in different ways depending on the operation + $query = "SELECT rid FROM {l10n_community_release} WHERE " . implode(' AND ', $where); + + // Now check results and run operations + if ($op == t('Repackage now')) { + $result = db_query($query, $args); + while ($release = db_fetch_object($result)) { + $rids[] = $release->rid; + } + if (!empty($rids)) { + $batch = l10n_packager_release_batch($rids, $languages); + batch_set($batch); + } + else { + drupal_set_message(t('No releases found for repackaging.'), 'error'); + } + } + elseif ($op == t('Mark for repackaging')) { + $args = array_merge(array(L10N_PACKAGER_ACTIVE), $args); + db_query("UPDATE {l10n_packager_release} SET updated = 0, checked = 0, package = %d WHERE rid IN ($query)", $args); + drupal_set_message(t("Marked %count releases for repackaging.", array('%count' => db_affected_rows()))); + } +} Index: l10n_packager/l10n_packager.inc =================================================================== RCS file: l10n_packager/l10n_packager.inc diff -N l10n_packager/l10n_packager.inc --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ l10n_packager/l10n_packager.inc 7 Oct 2009 13:28:42 -0000 @@ -0,0 +1,257 @@ +uri .'-'. $release->title; + } + else { + return ''; + } +} + +/** + * Check release translations and repackage if needed + * + * For each release we store packaging data in {l10n_packager_release} + * - The 'checked' timestamp is the last time all languages for this release were checked + * - The 'updated' timestamp is the last time a file was updated for this release + * + * We don't updated the 'checked' field untile we've checked all the languages for this release + * so we can keep track of releases and files and package a few languages on every cron + * + * @param $release + * Release object + * @param $force + * Force repackage even when strings not updated + * @param $limit + * Maximum number of files to create + * @param $language + * Optional language object to check only this one + * @param $cron + * This is a cron run, a release may be packaged partially, for some languages + */ +function l10n_packager_release_check($release, $force = FALSE, $limit = 0, $language = NULL, $cron = FALSE) { + $check_languages = $language ? array($language->language => $language) : l10n_community_get_languages(); + $updated = array(); + // We get update time before creating files so the release checked time is <= file timestamp + $timestamp = time(); + + $files = l10n_packager_release_get_files($release->rid); + $last_updated = l10n_packager_translation_last_updated($release->rid); + + // Get only the languages we have translations for, that need updating + $languages = array(); + foreach($check_languages as $lang => $language) { + if (!empty($last_updated[$lang]) && ($force || empty($downloads[$lang]) || $last_updated[$lang] > $files[$lang]->checked)) { + $languages[$lang] = $language; + } + } + + // For this special case we check we didn't stop before in the middle of a release + // Otherwise it could stick on a release for ever when forcing + if ($force && $cron && $release->checked < $release->updated) { + foreach($files as $lang => $file) { + if (!empty($file->checked) && $file->checked > $release->checked) { + unset($languages[$lang]); + } + } + } + + // Repackage this relase for the remaining language list + while((!$limit || $limit > count($updated)) && ($language = array_shift($languages))) { + $lang = $language->language; + // Warning: this may be upload release data with or wthout file + $existing = !empty($files[$lang]) ? $files[$lang] : NULL; + if ($file = l10n_packager_release_package($release, $language, $existing, $timestamp)) { + $updated[$lang] = $file; + } + else { + $updated[$lang] = FALSE; + } + } + + // Update the release data. We just mark it as checked if there are no languages left + if (!count($languages)) { + $release->checked = $timestamp; + } + if ($updated) { + $release->updated = $timestamp; + } + // If it is the first time we check this release, we need to create the record + if (isset($release->package)) { + drupal_write_record('l10n_packager_release', $release, 'rid'); + } + else { + $release->package = L10N_PACKAGER_ACTIVE; + drupal_write_record('l10n_packager_release', $release); + } + return $updated; +} + +/** + * Generate a new file for a given release or update an existing one + * + * @param $release + * Release object with uri and rid properties + * @param $language + * Language object + * @param $file + * Release file object to be updated + * @param $timestamp + * Timestamp to mark the files, for it to be consistent across tables + * @return File object + */ +function l10n_packager_release_package($release, $language, $file = NULL, $timestamp = NULL) { + $timestamp = $timestamp ? $timestamp : time(); + $variables = array('%release' => l10n_packager_release_name($release), '%language' => $language->name); + if (!$file) { + $file = new stdClass(); + $file->rid = $release->rid; + $file->language = $language->language; + } + // Generate tarball or PO file and get file name. + $export_result = l10n_community_export($release->uri, $release->rid, $language, FALSE, variable_get('l10n_packager_format', 'all-in-one')); + if (!empty($export_result) && is_array($export_result)) { + // If we got an array back from the export build, tear that into pieces. + list($mime_type, $file_name, $serve_name) = $export_result; + + // Now build the Drupal file object or update old one + if (!empty($file->fid) && !empty($file->filepath)) { + file_delete($file->filepath); + } + // Check / upate / create all file data + $file->status = FILE_STATUS_PERMANENT; + $file->timestamp = $file->checked = $timestamp; + $file->filename = $serve_name; + $file->filemime = $mime_type; + $file->filepath = file_create_path(variable_get('l10n_packager_directory', 'l10n_packager') . '/' . $serve_name); + file_move($file_name, $file->filepath, FILE_EXISTS_REPLACE); + $file->filesize = filesize($file->filepath); + // Create / update file record and link to release + drupal_write_record('files', $file, !empty($file->fid) ? 'fid' : array()); + drupal_write_record('l10n_packager_file', $file, !empty($file->drid) ? 'drid' : array()); + $variables['%filename'] = $file->filename; + watchdog('l10n_packager', 'Packaged release %release for language %language, created file %filename', $variables); + return $file; + } + else { + watchdog('l10n_packager', 'Failed packaging release %release for language %language', $variables); + return FALSE; + } +} + +/** + * Get timestamp of the last updated string for a release, for each language. + */ +function l10n_packager_translation_last_updated($rid) { + $updated = array(); + $result = db_query("SELECT t.language, MAX(t.time_entered) AS entered, MAX(t.time_approved) AS approved FROM {l10n_community_translation} t INNER JOIN {l10n_community_line} l ON t.sid = l.sid INNER JOIN {l10n_community_file} f ON f.fid = l.fid WHERE t.is_active = 1 AND f.rid = %d GROUP BY t.language", $rid); + while ($latest = db_fetch_object($result)) { + $updated[$latest->language] = max($latest->entered, $latest->approved); + } + return $updated; +} + +/** + * Get files for a release, indexed by language. + */ +function l10n_packager_release_get_files($rid) { + $files = array(); + $result = db_query('SELECT * FROM {l10n_packager_file} r LEFT JOIN {files} f ON r.fid = f.fid WHERE r.rid = %d', $rid); + while ($file = db_fetch_object($result)) { + $files[$file->language] = $file; + } + return $files; +} + +/** + * Batch callback: repackage project. + * + * @param $release + * Release id or release object + */ +function _l10n_packager_batch_repackage($rid, $langcode) { + if ($release = l10n_packager_get_release($rid)) { + $languages = l10n_community_get_languages(); + $language = $languages[$langcode]; + $updates = l10n_packager_release_check($release, TRUE, 0, $language); + if ($file = current($updates) ) { + drupal_set_message(t("Repackaged release %release for %language. Created file %filename.", array('%release' => l10n_packager_release_name($release), '%filename' => $file->filename, '%language' => $language->name))); + } + } +} + +/** + * Create batch for repackaging all releases for a project. + */ +function l10n_packager_project_batch($pid, $languages = NULL) { + $result = db_query("SELECT rid FROM {l10n_community_release} WHERE pid = %d", $pid); + while ($release = db_fetch_object($result)) { + $rids[] = $release->rid; + } + if (!empty($rids)) { + return l10n_packager_release_batch($rids, $languages); + } +} + +/** + * Create batch for repackaging releases. + * + * @param $rid + * Release id or array of release ids + * @param $languages + * Array of language codes to repackage or none + * @return unknown_type + */ +function l10n_packager_release_batch($rid, $languages = NULL) { + $rids = is_array($rid) ? $rid : array($rid); + // All languages if no languages passed + $languages = $languages ? $languages : array_keys(l10n_community_get_languages()); + foreach ($rids as $rid) { + foreach ($languages as $lang) { + $operations[] = array('_l10n_packager_batch_repackage', array($rid, $lang)); + } + } + if (!empty($operations)) { + return _l10n_packager_build_batch($operations, t('Repackaging translation files.')); + } + break; +} + + +/** + * Get batch stub. + */ +function _l10n_packager_build_batch($operations = array(), $title = '') { + $batch = array( + 'title' => $title ? $title : t('Translations packager.'), + 'operations' => $operations, + // tell the batch engine which file to load before calling them. + 'file' => drupal_get_path('module', 'l10n_packager') .'/l10n_packager.inc', + ); + return $batch; +} Index: l10n_packager/l10n_packager.install =================================================================== RCS file: l10n_packager/l10n_packager.install diff -N l10n_packager/l10n_packager.install --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ l10n_packager/l10n_packager.install 7 Oct 2009 13:28:42 -0000 @@ -0,0 +1,94 @@ + 'Information about repackaging for each release.', + 'fields' => array( + 'rid' => array( + 'description' => 'Reference to {l10n_community_release}.rid', + 'type' => 'int', + 'not null' => TRUE, + 'unsigned' => TRUE, + ), + 'package' => array( + 'description' => 'Packaging status for this release.', + 'type' => 'int', + 'not null' => TRUE, + 'default value' => 1, + 'disp-width' => '11' + ), + 'checked' => array( + 'description' => 'Unix timestamp of last time this release was checked.', + 'type' => 'int', + 'not null' => FALSE, + 'disp-width' => '11' + ), + 'updated' => array( + 'description' => 'Unix timestamp of last time files for this release were updated.', + 'type' => 'int', + 'not null' => FALSE, + 'disp-width' => '11' + ), + ), + 'primary key' => array('rid'), + ); + $schema['l10n_packager_file'] = array( + 'description' => 'Links releases and languages to files.', + 'fields' => array( + 'drid' => array( + 'description' => 'Internal numeric identifier for a release file.', + 'type' => 'serial', + 'not null' => TRUE, + ), + 'rid' => array( + 'description' => 'Reference to {l10n_community_release}.rid', + 'type' => 'int', + 'not null' => TRUE, + 'unsigned' => TRUE, + ), + 'language' => array( + 'description' => 'Reference to the {languages}.language for the translation package.', + 'type' => 'varchar', + 'length' => '12', + 'not null' => TRUE + ), + 'fid' => array( + 'description' => 'Reference to {files}.fid.', + 'type' => 'int', + 'not null' => TRUE, + 'unsigned' => TRUE, + ), + 'checked' => array( + 'description' => 'Unix timestamp of last time translation for this language was checked.', + 'type' => 'int', + 'not null' => FALSE, + 'disp-width' => '11' + ), + ), + 'primary key' => array('drid'), + ); + return $schema; +} + +/** + * Implementation of hook_install() + */ +function l10n_packager_install() { + drupal_install_schema('l10n_packager'); +} + +/** + * Implementation of hook_uninstall() + */ +function l10n_packager_uninstall() { + drupal_uninstall_schema('l10n_packager'); +} Index: l10n_packager/l10n_packager.module =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/l10n_server/l10n_packager/Attic/l10n_packager.module,v retrieving revision 1.1.2.2 diff -u -p -u -p -r1.1.2.2 l10n_packager.module --- l10n_packager/l10n_packager.module 5 Sep 2009 17:15:58 -0000 1.1.2.2 +++ l10n_packager/l10n_packager.module 7 Oct 2009 13:28:42 -0000 @@ -7,20 +7,81 @@ */ /** + * Release packager status: do not repackage anymore. + */ +define('L10N_PACKAGER_DISABLED', 0); + +/** + * Release packager status: keep repackaging. + */ +define('L10N_PACKAGER_ACTIVE', 1); + +/** + * Release packager status: error. + */ +define('L10N_PACKAGER_ERROR', 2); + +/** * Implementation of hook_menu(). */ function l10n_packager_menu() { - $items['admin/settings/language/packager'] = array( + // Settings and administration + $items['admin/l10n_server/packager'] = array( + 'title' => 'Packaging tools', + 'description' => 'Configure automatic packaging for translations.', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('l10n_packager_settings_form'), + 'file' => 'l10n_packager.admin.inc', + 'access arguments' => array('administer localization community'), + ); + $items['admin/l10n_server/packager/configure'] = array( + 'title' => 'Configure', + 'type' => MENU_DEFAULT_LOCAL_TASK, + ); + $items['admin/l10n_server/packager/repackage'] = array( + 'title' => 'Repackage', + 'description' => 'Select project, releases and languages to repackage', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('l10n_packager_admin_repackage_form'), + 'file' => 'l10n_packager.admin.inc', + 'access arguments' => array('administer localization community'), + 'type' => MENU_LOCAL_TASK, + ); + $items['admin/l10n_server/packager/language'] = array( 'title' => 'Export language list', - 'page callback' => 'l10n_packager_page', + 'page callback' => 'l10n_packager_language_page', 'access arguments' => array('administer languages'), - 'weight' => 20, 'type' => MENU_LOCAL_TASK, + 'weight' => 20, ); return $items; } /** + * Implementation of hook_cron(). + */ +function l10n_packager_cron() { + if (variable_get('l10n_packager_cron', 0) && ($interval = variable_get('l10n_packager_update', 0))) { + module_load_include('inc', 'l10n_packager'); + $timestamp = time() - $interval; + $file_limit = variable_get('l10n_packager_file_limit', 1); + $count_files = $count_check = 0; + + // Go for it: check releases for repackaging. We need project_uri for later + $result = db_query_range("SELECT r.rid, r.pid, r.title, pr.checked, pr.updated, pr.package, p.uri FROM {l10n_community_release} r INNER JOIN {l10n_community_project} p ON r.pid = p.pid LEFT JOIN {l10n_packager_release} pr ON pr.rid = r.rid WHERE pr.package IS NULL OR (pr.package = %d AND (pr.checked < %d OR pr.updated < %d)) ORDER BY pr.checked", L10N_PACKAGER_ACTIVE, $timestamp, $timestamp, 0, variable_get('l10n_packager_check', 10)); + + while ((!$file_limit || $file_limit > $count_files) && ($release = db_fetch_object($result))) { + $updates = l10n_packager_release_check($release, FALSE, $file_limit - $count_files, NULL, TRUE); + $count_files += count($updates); + $count_check++; + } + watchdog('l10n_packager', 'Checked %checked releases. Repackaged %repack translations.', array('%checked' => $count_check, '%repack' => $count_files)); + } +} + +// == Language list export ===================================================== + +/** * Implementation of hook_form_alter(). */ function l10n_packager_form_alter(&$form, $form_state, $form_id) { @@ -32,14 +93,14 @@ function l10n_packager_form_alter(&$form 'locale_languages_custom_form', ); if (in_array($form_id, $languages_forms)) { - $form['#submit'][] = 'l10n_packager_export'; + $form['#submit'][] = 'l10n_packager_export_language_list'; } } /** * Export languages in a simple XML format for remote use. */ -function l10n_packager_export() { +function l10n_packager_export_language_list() { $languages = language_list('language', TRUE); $xml = simplexml_load_string("\n"); @@ -49,7 +110,7 @@ function l10n_packager_export() { $item->addChild($key, $language->$key); } } - $xml_path = file_create_path('l10n_packager'); + $xml_path = file_create_path(variable_get('l10n_packager_directory', 'l10n_packager')); file_check_directory($xml_path, FILE_CREATE_DIRECTORY); if ($xml->asXML($xml_path .'/languages.xml')) { drupal_set_message(t('Languages XML exported.')); @@ -60,9 +121,9 @@ function l10n_packager_export() { } /** - * Dummy page for export menu. + * Dummy page for language export menu item. */ -function l10n_packager_page() { - l10n_packager_export(); - drupal_goto('admin/settings/language/overview'); +function l10n_packager_language_page() { + l10n_packager_export_language_list(); + drupal_goto('admin/l10n_server/packager'); }