diff --git a/assets/css/translation-events.css b/assets/css/translation-events.css index 8559c100..8fc4a311 100644 --- a/assets/css/translation-events.css +++ b/assets/css/translation-events.css @@ -53,6 +53,7 @@ .event-details-stats table { margin: 1rem; } + .event-details-stats table { width: 100%; table-layout: fixed; @@ -348,6 +349,7 @@ a.event-page-edit-link:hover { border-bottom: var(--gp-color-btn-primary-bg) thin solid; text-decoration: none; } + ul.text-snippets { padding: 0; margin-left: 160px; @@ -355,6 +357,11 @@ ul.text-snippets { .first-time-contributor-tada::after { content: ' 🎉'; } + +ul#translation-links li { + margin-bottom: .5em; +} + .icons li .name { display: none; } @@ -365,6 +372,7 @@ ul.text-snippets { cursor: pointer; display: inline-block; } + /* show the event-details-right below instead of on the right on mobile */ @media (max-width: 768px) { diff --git a/autoload.php b/autoload.php index 1eefc5b3..cb68aeef 100644 --- a/autoload.php +++ b/autoload.php @@ -8,6 +8,7 @@ require_once __DIR__ . '/includes/routes/event/details.php'; require_once __DIR__ . '/includes/routes/event/edit.php'; require_once __DIR__ . '/includes/routes/event/list.php'; +require_once __DIR__ . '/includes/routes/event/translations.php'; require_once __DIR__ . '/includes/routes/user/attend-event.php'; require_once __DIR__ . '/includes/routes/user/host-event.php'; require_once __DIR__ . '/includes/routes/user/my-events.php'; diff --git a/includes/routes/event/translations.php b/includes/routes/event/translations.php new file mode 100644 index 00000000..e75f846e --- /dev/null +++ b/includes/routes/event/translations.php @@ -0,0 +1,178 @@ +event_repository = Translation_Events::get_event_repository(); + } + + public function handle( string $event_slug, string $locale, string $status = 'any' ): void { + $user = wp_get_current_user(); + $event = get_page_by_path( $event_slug, OBJECT, Translation_Events::CPT ); + if ( ! $event ) { + $this->die_with_404(); + } + $event = $this->event_repository->get_event( $event->ID ); + if ( ! $event ) { + $this->die_with_404(); + } + + global $wpdb, $gp_table_prefix; + + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery + // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching + $translation_sets = $wpdb->get_results( + $wpdb->prepare( + " + SELECT DISTINCT ts.id as translation_set_id, ts.name, o.project_id as project_id + FROM {$gp_table_prefix}event_actions ea + JOIN {$gp_table_prefix}originals o ON ea.original_id = o.id + JOIN {$gp_table_prefix}translation_sets ts ON o.project_id = ts.project_id AND ea.locale = ts.locale + WHERE ea.event_id = %d + AND ea.locale = %s + ", + $event->id(), + $locale + ) + ); + $projects = array(); + $translations = array(); + $locale = GP_Locales::by_slug( $locale ); + foreach ( $translation_sets as $ts ) { + $projects[ $ts->translation_set_id ] = GP::$project->get( $ts->project_id ); + + } + gp_tmpl_load( 'event-translations-header', get_defined_vars(), $this->template_path ); + + foreach ( $translation_sets as $ts ) { + $rows = $wpdb->get_results( + $wpdb->prepare( + " + SELECT + t.*, + o.*, + t.id as id, + o.id as original_id, + t.status as translation_status, + o.status as original_status, + t.date_added as translation_added, + o.date_added as original_added + FROM {$gp_table_prefix}event_actions ea + JOIN {$gp_table_prefix}originals o ON ea.original_id = o.id + JOIN {$gp_table_prefix}translations t ON t.original_id = ea.original_id + WHERE ea.event_id = %d + AND t.translation_set_id = %d + AND t.user_id = ea.user_id + AND t.status LIKE %s + ", + $event->id(), + $ts->translation_set_id, + trim( $status, '/' ) === 'waiting' ? 'waiting' : '%' + ) + ); + // phpcs:enable + if ( empty( $rows ) ) { + echo ''; + continue; + } + $translations = array(); + $project = $projects[ $ts->translation_set_id ]; + $translation_set = GP::$translation_set->get( $ts->translation_set_id ); + $filters = array(); + $sort = array(); + $glossary = GP::$glossary->get( $project->id, $locale ); + $page = 1; + $per_page = 10000; + $total_translations_count = 0; + $text_direction = 'ltr'; + $locale_slug = $translation_set->locale; + $translation_set_slug = $translation_set->slug; + $word_count_type = $locale->word_count_type; + $can_edit = $this->can( 'edit', 'translation-set', $translation_set->id ); + $can_write = $this->can( 'write', 'project', $project->id ); + $can_approve = $this->can( 'approve', 'translation-set', $translation_set->id ); + $can_import_current = $can_approve; + $can_import_waiting = $can_approve || $this->can( 'import-waiting', 'translation-set', $translation_set->id ); + $url = gp_url_project( $project, gp_url_join( $translation_set->locale, $translation_set->slug ) ); + $set_priority_url = gp_url( '/originals/%original-id%/set_priority' ); + $discard_warning_url = gp_url_project( $project, gp_url_join( $translation_set->locale, $translation_set->slug, '-discard-warning' ) ); + $set_status_url = gp_url_project( $project, gp_url_join( $translation_set->locale, $translation_set->slug, '-set-status' ) ); + $bulk_action = gp_url_join( $url, '-bulk' ); + + $editor_options[ $translation_set->id ] = compact( 'can_approve', 'can_write', 'url', 'discard_warning_url', 'set_priority_url', 'set_status_url', 'word_count_type' ); + + foreach ( (array) $rows as $row ) { + $row->user = null; + $row->user_last_modified = null; + + if ( $row->user_id ) { + $user = get_userdata( $row->user_id ); + if ( $user ) { + $row->user = (object) array( + 'ID' => $user->ID, + 'user_login' => $user->user_login, + 'display_name' => $user->display_name, + 'user_nicename' => $user->user_nicename, + ); + } + } + + if ( $row->user_id_last_modified ) { + $user = get_userdata( $row->user_id_last_modified ); + if ( $user ) { + $row->user_last_modified = (object) array( + 'ID' => $user->ID, + 'user_login' => $user->user_login, + 'display_name' => $user->display_name, + 'user_nicename' => $user->user_nicename, + ); + } + } + + $row->translations = array(); + for ( $i = 0; $i < $locale->nplurals; $i++ ) { + $row->translations[] = $row->{'translation_' . $i}; + } + $row->references = $row->references ? preg_split( '/\s+/', $row->references, -1, PREG_SPLIT_NO_EMPTY ) : array(); + $row->extracted_comments = $row->comment; + $row->warnings = $row->warnings ? maybe_unserialize( $row->warnings ) : null; + unset( $row->comment ); + + // Reduce range by one since we're starting at 0, see GH#516. + foreach ( range( 0, 5 ) as $i ) { + $member = "translation_$i"; + unset( $row->$member ); + } + + $row->row_id = $row->original_id . ( $row->id ? "-$row->id" : '' ); + + if ( '0' !== $row->priority ) { + $row->flags = array( + 'gp-priority: ' . GP_Original::$priorities[ $row->priority ], + ); + } + + $translations[ $row->row_id ] = new Translation_Entry( (array) $row ); + } + gp_tmpl_load( 'translations', get_defined_vars(), $this->template_path ); + } + + gp_tmpl_load( 'event-translations-footer', get_defined_vars(), $this->template_path ); + } +} diff --git a/includes/stats/stats-calculator.php b/includes/stats/stats-calculator.php index 0df792f3..e34b5b8c 100644 --- a/includes/stats/stats-calculator.php +++ b/includes/stats/stats-calculator.php @@ -14,12 +14,14 @@ class Stats_Row { public int $created; public int $reviewed; + public int $waiting; public int $users; public ?GP_Locale $language = null; - public function __construct( $created, $reviewed, $users, ?GP_Locale $language = null ) { + public function __construct( $created, $reviewed, $waiting, $users, ?GP_Locale $language = null ) { $this->created = $created; $this->reviewed = $reviewed; + $this->waiting = $waiting; $this->users = $users; $this->language = $language; } @@ -103,8 +105,10 @@ public function for_event( int $event_id ): Event_Stats { select locale, sum(action = 'create') as created, count(*) as total, - count(distinct user_id) as users - from {$gp_table_prefix}event_actions + sum(t.status = 'waiting') as waiting, + count(distinct ea.user_id) as users + from {$gp_table_prefix}event_actions ea + left join {$gp_table_prefix}translations t ON ea.original_id = t.original_id and ea.user_id = t.user_id where event_id = %d group by locale with rollup ", @@ -130,9 +134,15 @@ public function for_event( int $event_id ): Event_Stats { $lang = null; } + if ( is_null( $row->waiting ) ) { + // The corresponding translations are missing. Could be a unit test or data corruption. + $row->waiting = 0; + } + $stats_row = new Stats_Row( $row->created, $row->total - $row->created, + $row->waiting, $row->users, $lang ); diff --git a/includes/urls.php b/includes/urls.php index 5e197736..4532cf55 100644 --- a/includes/urls.php +++ b/includes/urls.php @@ -18,6 +18,10 @@ public static function event_details_absolute( int $event_id ): string { return get_site_url() . gp_url( wp_make_link_relative( $permalink ) ); } + public static function event_translations( int $event_id, string $locale, string $status = '' ): string { + return gp_url_join( self::event_details( $event_id ), 'translations', $locale, $status ); + } + public static function event_edit( int $event_id ): string { return gp_url( '/events/edit/' . $event_id ); } diff --git a/templates/event-translations-footer.php b/templates/event-translations-footer.php new file mode 100644 index 00000000..f740bdca --- /dev/null +++ b/templates/event-translations-footer.php @@ -0,0 +1,36 @@ + +
+ + diff --git a/templates/event-translations-header.php b/templates/event-translations-header.php new file mode 100644 index 00000000..92dd2a42 --- /dev/null +++ b/templates/event-translations-header.php @@ -0,0 +1,63 @@ +title() ) ) ); +gp_breadcrumb_translation_events( array( '' . esc_html( $event->title() ) . '', __( 'Translations', 'glotpress' ), $locale->english_name ) ); +gp_enqueue_scripts( array( 'gp-editor', 'gp-translations-page' ) ); +wp_localize_script( + 'gp-translations-page', + '$gp_translations_options', + array( + 'sort' => __( 'Sort', 'glotpress' ), + 'filter' => __( 'Filter', 'glotpress' ), + ) +); + +gp_tmpl_header(); +?> + +
+

+ title() ); ?> + status() ) : ?> + status() ); ?> + +

+
+
+

+ english_name + ) + ); + ?> +

+ + + + + + + +
diff --git a/templates/event.php b/templates/event.php index 35b12ef9..7a0b7a0b 100644 --- a/templates/event.php +++ b/templates/event.php @@ -118,9 +118,10 @@ - - - + + + + @@ -129,7 +130,8 @@ rows() as $_locale => $row ) : ?> - + + @@ -137,6 +139,7 @@ + diff --git a/templates/translations.php b/templates/translations.php new file mode 100644 index 00000000..4a03cdc0 --- /dev/null +++ b/templates/translations.php @@ -0,0 +1,153 @@ + +
+
+

+ locale, $translation_set->slug ), + esc_html( + gp_project_names_from_root( $project ) + ) + ), + array( + 'a' => array( + 'href' => array(), + 'title' => array(), + ), + ) + ), + esc_html( $locale->name ) + ); + ?> +

+
+
+ +
+ +text_direction ? ' translation-sets-rtl' : ''; ?> + +
language->english_name ); ?>created ); ?>created ); ?>waiting ); ?> reviewed ); ?> users ); ?>
Total totals()->created ); ?>totals()->waiting ); ?> totals()->reviewed ); ?> totals()->users ); ?>
+ + + + + + + + + + + +translation_set_id ) { + $translation->translation_set_id = $translation_set->id; + } + + $can_approve_translation = GP::$permission->current_user_can( 'approve', 'translation', $translation->id, array( 'translation' => $translation ) ); + gp_tmpl_load( 'translation-row', get_defined_vars() ); +} +?> + + + + +
+ + +
+ +
+
+ get_static( 'statuses' ) as $legend_status ) : + if ( ( 'changesrequested' === $legend_status ) && ( ! apply_filters( 'gp_enable_changesrequested_status', false ) ) ) { // todo: delete when we merge the gp-translation-helpers in GlotPress. + continue; + } + ?> +
+
+ +
+ +
+
+
+
+
+
diff --git a/tests/urls.php b/tests/urls.php index af2eda20..5172ebd0 100644 --- a/tests/urls.php +++ b/tests/urls.php @@ -40,6 +40,17 @@ public function test_event_details_absolute() { $this->assertEquals( $expected, Urls::event_details_absolute( $event_id ) ); } + public function test_event_translations() { + $event_id = $this->event_factory->create_active(); + $event = $this->event_repository->get_event( $event_id ); + + $expected = "/glotpress/events/{$event->slug()}/translations/pt"; + $this->assertEquals( $expected, Urls::event_translations( $event_id, 'pt' ) ); + + $expected = "/glotpress/events/{$event->slug()}/translations/pt/waiting"; + $this->assertEquals( $expected, Urls::event_translations( $event_id, 'pt', 'waiting' ) ); + } + public function test_event_edit() { $event_id = $this->event_factory->create_active(); diff --git a/wporg-gp-translation-events.php b/wporg-gp-translation-events.php index 0965e8e3..abad4fd9 100644 --- a/wporg-gp-translation-events.php +++ b/wporg-gp-translation-events.php @@ -22,6 +22,7 @@ use DateTimeZone; use Exception; use GP; +use GP_Locales; use WP_Post; use WP_Query; use Wporg\TranslationEvents\Attendee\Attendee; @@ -89,13 +90,20 @@ public function __construct() { } public function gp_init() { + $locale = '(' . implode( '|', wp_list_pluck( GP_Locales::locales(), 'slug' ) ) . ')'; + $slug = '([a-z0-9_-]+)'; + $status = '(waiting)'; + $id = '(\d+)'; + GP::$router->add( '/events?', array( 'Wporg\TranslationEvents\Routes\Event\List_Route', 'handle' ) ); GP::$router->add( '/events/new', array( 'Wporg\TranslationEvents\Routes\Event\Create_Route', 'handle' ) ); - GP::$router->add( '/events/edit/(\d+)', array( 'Wporg\TranslationEvents\Routes\Event\Edit_Route', 'handle' ) ); - GP::$router->add( '/events/attend/(\d+)', array( 'Wporg\TranslationEvents\Routes\User\Attend_Event_Route', 'handle' ), 'post' ); - GP::$router->add( '/events/host/(\d+)/(\d+)', array( 'Wporg\TranslationEvents\Routes\User\Host_Event_Route', 'handle' ), 'post' ); + GP::$router->add( '/events/edit/', array( 'Wporg\TranslationEvents\Routes\Event\Edit_Route', 'handle' ) ); + GP::$router->add( "/events/attend/$id", array( 'Wporg\TranslationEvents\Routes\User\Attend_Event_Route', 'handle' ), 'post' ); + GP::$router->add( "/events/host/$id/$id", array( 'Wporg\TranslationEvents\Routes\User\Host_Event_Route', 'handle' ), 'post' ); GP::$router->add( '/events/my-events', array( 'Wporg\TranslationEvents\Routes\User\My_Events_Route', 'handle' ) ); - GP::$router->add( '/events/([a-z0-9_-]+)', array( 'Wporg\TranslationEvents\Routes\Event\Details_Route', 'handle' ) ); + GP::$router->add( "/events/$slug/translations/$locale/$status", array( 'Wporg\TranslationEvents\Routes\Event\Translations_Route', 'handle' ) ); + GP::$router->add( "/events/$slug/translations/$locale", array( 'Wporg\TranslationEvents\Routes\Event\Translations_Route', 'handle' ) ); + GP::$router->add( "/events/$slug", array( 'Wporg\TranslationEvents\Routes\Event\Details_Route', 'handle' ) ); $stats_listener = new Stats_Listener( self::get_event_repository(),