├── .nvmrc ├── themes └── wporg-translate-events-2024 │ ├── index.php │ ├── templates │ └── index.html │ ├── theme.json │ ├── blocks │ ├── pages │ │ └── events │ │ │ ├── event-create │ │ │ ├── render.php │ │ │ └── index.php │ │ │ ├── event-edit │ │ │ ├── render.php │ │ │ └── index.php │ │ │ ├── my-events │ │ │ ├── render.php │ │ │ └── index.php │ │ │ ├── home │ │ │ ├── index.php │ │ │ └── render.php │ │ │ ├── event-attendees │ │ │ ├── render.php │ │ │ └── index.php │ │ │ └── event-details │ │ │ ├── index.php │ │ │ └── render.php │ ├── footer │ │ ├── index.php │ │ └── render.php │ ├── event-excerpt │ │ └── index.php │ ├── remote-attendance-icon │ │ └── index.php │ ├── header │ │ ├── render.php │ │ ├── index.php │ │ └── site-header.php │ ├── event-attendance-mode │ │ └── index.php │ ├── event-description │ │ └── index.php │ ├── event-load-more-button │ │ └── index.php │ ├── event-edit-link │ │ └── index.php │ ├── attendee-avatar-name │ │ └── index.php │ ├── attendee-list │ │ ├── index.php │ │ ├── render-list.php │ │ └── render-table.php │ ├── event-title │ │ └── index.php │ ├── event-date │ │ └── index.php │ ├── event-template │ │ └── index.php │ ├── event-flag │ │ └── index.php │ ├── event-trash-link │ │ └── index.php │ ├── event-host-list │ │ └── index.php │ ├── event-nav-links │ │ └── index.php │ ├── event-projects │ │ └── index.php │ ├── contributor-list │ │ └── index.php │ ├── event-stats │ │ └── index.php │ ├── event-list │ │ └── index.php │ ├── event-attend-button │ │ └── index.php │ └── event-contribution-summary │ │ └── index.php │ ├── README.md │ └── patterns │ └── front-cover.php ├── .gitignore ├── assets └── fonts │ └── eb-garamond │ ├── EBGaramond-Regular.ttf │ └── license.txt ├── templates ├── parts │ ├── footer.php │ ├── breadcrumbs.php │ └── header.php ├── event-create.php ├── trashed-events.php ├── event-edit.php ├── my-events.php ├── translations │ ├── footer.php │ ├── header.php │ └── table.php ├── home.php └── event-attendees.php ├── src └── blocks │ └── example │ ├── editor.scss │ ├── style.scss │ ├── block.json │ ├── index.php │ ├── save.js │ ├── view.js │ ├── index.js │ └── edit.js ├── .wp-env.json ├── tests ├── base-test.php ├── lib │ ├── translation-factory.php │ ├── stats-factory.php │ └── event-factory.php ├── event │ ├── event-date.php │ ├── event.php │ └── event-image.php ├── bootstrap.php ├── urls.php └── stats │ └── stats-calculator.php ├── package.json ├── phpunit.xml.dist ├── includes ├── event-text-snippet.php ├── templates.php ├── event │ ├── event-query-result.php │ └── event.php ├── routes │ ├── event │ │ ├── create.php │ │ ├── edit.php │ │ ├── delete.php │ │ ├── list-trashed.php │ │ ├── trash.php │ │ ├── image.php │ │ ├── details.php │ │ └── list.php │ ├── user │ │ ├── my-events.php │ │ ├── attendance-mode.php │ │ ├── host-event.php │ │ └── attend-event.php │ ├── route.php │ └── attendee │ │ ├── list.php │ │ └── remove.php ├── project │ └── project-repository.php ├── theme-loader.php ├── attendee │ ├── attendee.php │ └── attendee-adder.php ├── notifications │ └── notifications-schedule.php ├── urls.php ├── stats │ ├── stats-listener.php │ └── stats-calculator.php └── upgrade.php ├── .github └── workflows │ ├── coding-standards.yml │ └── tests.yml ├── composer.json ├── README.md ├── phpcs.xml └── autoload.php /.nvmrc: -------------------------------------------------------------------------------- 1 | 20 2 | -------------------------------------------------------------------------------- /themes/wporg-translate-events-2024/index.php: -------------------------------------------------------------------------------- 1 | hello 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /node_modules/ 3 | /build/ 4 | .phpunit.result.cache 5 | -------------------------------------------------------------------------------- /themes/wporg-translate-events-2024/theme.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schemas.wp.org/trunk/theme.json", 3 | "version": 3, 4 | "settings": {} 5 | } 6 | -------------------------------------------------------------------------------- /assets/fonts/eb-garamond/EBGaramond-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WordPress/wporg-gp-translation-events/HEAD/assets/fonts/eb-garamond/EBGaramond-Regular.ttf -------------------------------------------------------------------------------- /templates/parts/footer.php: -------------------------------------------------------------------------------- 1 | 5 | 6 |
7 | 8 | -------------------------------------------------------------------------------- /themes/wporg-translate-events-2024/blocks/pages/events/event-create/render.php: -------------------------------------------------------------------------------- 1 | 5 | 6 | -------------------------------------------------------------------------------- /src/blocks/example/editor.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * The following styles get applied inside the editor only. 3 | * 4 | * Replace them with your own styles or remove the file completely. 5 | */ 6 | 7 | .wp-block-create-block-example { 8 | border: 1px dotted #f00; 9 | } 10 | -------------------------------------------------------------------------------- /.wp-env.json: -------------------------------------------------------------------------------- 1 | { 2 | "core": null, 3 | "phpVersion": "8.3", 4 | "plugins": [ 5 | "GlotPress/GlotPress", 6 | "https://downloads.wordpress.org/plugin/sqlite-object-cache.1.3.7.zip", 7 | "." 8 | ], 9 | "config": { 10 | "SAVEQUERIES": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/blocks/example/style.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * The following styles get applied both on the front of your site 3 | * and in the editor. 4 | * 5 | * Replace them with your own styles or remove the file completely. 6 | */ 7 | 8 | .wp-block-create-block-example { 9 | background-color: #21759b; 10 | color: #fff; 11 | padding: 2px; 12 | } 13 | -------------------------------------------------------------------------------- /themes/wporg-translate-events-2024/blocks/pages/events/event-edit/render.php: -------------------------------------------------------------------------------- 1 | $attributes['event_id'] ?? null, 5 | 'is_create_event' => false, 6 | ); 7 | ?> 8 | 9 | -------------------------------------------------------------------------------- /themes/wporg-translate-events-2024/blocks/footer/index.php: -------------------------------------------------------------------------------- 1 | function () { 7 | ob_start(); 8 | require __DIR__ . '/render.php'; 9 | return ob_get_clean(); 10 | }, 11 | ) 12 | ); 13 | -------------------------------------------------------------------------------- /themes/wporg-translate-events-2024/blocks/footer/render.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | ' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /themes/wporg-translate-events-2024/blocks/pages/events/my-events/render.php: -------------------------------------------------------------------------------- 1 | $event_ids, 7 | 'show_flag' => true, 8 | ); 9 | ?> 10 | 11 | -------------------------------------------------------------------------------- /tests/base-test.php: -------------------------------------------------------------------------------- 1 | now = Translation_Events::now(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /themes/wporg-translate-events-2024/blocks/pages/events/home/index.php: -------------------------------------------------------------------------------- 1 | function ( array $attributes ) { 8 | render_page( 9 | __DIR__ . '/render.php', 10 | __( 'Translation Events', 'wporg-translate-events-2024' ), 11 | $attributes 12 | ); 13 | }, 14 | ) 15 | ); 16 | -------------------------------------------------------------------------------- /themes/wporg-translate-events-2024/blocks/pages/events/event-edit/index.php: -------------------------------------------------------------------------------- 1 | function ( array $attributes ) { 8 | render_page( 9 | __DIR__ . '/render.php', 10 | __( 'Edit event', 'wporg-translate-events-2024' ), 11 | $attributes 12 | ); 13 | }, 14 | ) 15 | ); 16 | -------------------------------------------------------------------------------- /themes/wporg-translate-events-2024/blocks/pages/events/event-create/index.php: -------------------------------------------------------------------------------- 1 | function ( array $attributes ) { 8 | render_page( 9 | __DIR__ . '/render.php', 10 | __( 'Create event', 'wporg-translate-events-2024' ), 11 | $attributes 12 | ); 13 | }, 14 | ) 15 | ); 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wporg-translate-events", 3 | "version": "1.0.0", 4 | "description": "Plugin that powers translate.wordpress.org/events", 5 | "main": "build/index.js", 6 | "scripts": { 7 | "start": "wp-scripts start", 8 | "build": "wp-scripts build", 9 | "format": "wp-scripts format", 10 | "lint:css": "wp-scripts lint-style", 11 | "lint:js": "wp-scripts lint-js" 12 | }, 13 | "devDependencies": { 14 | "@wordpress/scripts": "^28.3.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /themes/wporg-translate-events-2024/blocks/event-excerpt/index.php: -------------------------------------------------------------------------------- 1 | function ( array $attributes ) { 9 | return '

' . esc_html( get_the_excerpt( $attributes['id'] ) ) . '

'; 10 | }, 11 | ) 12 | ); 13 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | ./tests/ 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/blocks/example/block.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schemas.wp.org/trunk/block.json", 3 | "apiVersion": 3, 4 | "name": "wporg-translate-events/example", 5 | "version": "0.1.0", 6 | "title": "Example block", 7 | "category": "widgets", 8 | "icon": "smiley", 9 | "description": "Example block.", 10 | "example": {}, 11 | "supports": { 12 | "html": false 13 | }, 14 | "textdomain": "gp-translation-events", 15 | "editorScript": "file:./index.js", 16 | "editorStyle": "file:./index.css", 17 | "style": "file:./style-index.css", 18 | "viewScript": "file:./view.js" 19 | } 20 | -------------------------------------------------------------------------------- /themes/wporg-translate-events-2024/blocks/remote-attendance-icon/index.php: -------------------------------------------------------------------------------- 1 | function ( $attributes ) { 9 | $css_class = isset( $attributes['css_class'] ) ? esc_attr( $attributes['css_class'] ) : ''; 10 | 11 | return sprintf( '', $css_class ); 12 | }, 13 | ) 14 | ); 15 | -------------------------------------------------------------------------------- /src/blocks/example/index.php: -------------------------------------------------------------------------------- 1 | get_event( $event_id ); 8 | if ( ! $event ) { 9 | return ''; 10 | } 11 | 12 | ?> 13 | 24 | -------------------------------------------------------------------------------- /templates/parts/breadcrumbs.php: -------------------------------------------------------------------------------- 1 | '; 15 | foreach ( $snippets as $snippet ) { 16 | $snippets_link_list .= sprintf( '
  • %s
  • ', esc_html( $snippet['snippet'] ), esc_html( $snippet['title'] ) ); 17 | } 18 | $snippets_link_list .= ''; 19 | return $snippets_link_list; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /includes/templates.php: -------------------------------------------------------------------------------- 1 | function ( array $attributes ) { 8 | add_filter( 9 | 'wporg_block_site_breadcrumbs', 10 | function ( $breadcrumbs ): array { 11 | return array_merge( 12 | $breadcrumbs, 13 | array( 14 | array( 15 | 'title' => __( 'My Events', 'wporg-translate-events-2024' ), 16 | 'url' => null, 17 | ), 18 | ) 19 | ); 20 | } 21 | ); 22 | 23 | render_page( 24 | __DIR__ . '/render.php', 25 | __( 'My Events', 'wporg-translate-events-2024' ), 26 | $attributes 27 | ); 28 | }, 29 | ) 30 | ); 31 | -------------------------------------------------------------------------------- /.github/workflows/coding-standards.yml: -------------------------------------------------------------------------------- 1 | name: Coding standards 2 | 3 | on: 4 | pull_request: 5 | types: [ opened, synchronize ] 6 | 7 | jobs: 8 | phpcs: 9 | name: phpcs 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Check out repository 14 | uses: actions/checkout@v4 15 | 16 | - name: Install PHP 17 | uses: shivammathur/setup-php@v2 18 | with: 19 | php-version: '8.3' 20 | coverage: 'none' 21 | tools: composer, cs2pr 22 | 23 | - name: Install dependencies 24 | uses: ramsey/composer-install@v2 25 | with: 26 | composer-options: "--no-progress --no-ansi --no-interaction" 27 | 28 | - name: Run PHPCS 29 | run: vendor/bin/phpcs --standard=phpcs.xml -q --report=checkstyle | cs2pr --notices-as-warnings 30 | -------------------------------------------------------------------------------- /themes/wporg-translate-events-2024/blocks/pages/events/event-attendees/index.php: -------------------------------------------------------------------------------- 1 | function ( array $attributes ) { 8 | add_filter( 9 | 'wporg_block_site_breadcrumbs', 10 | function ( $breadcrumbs ): array { 11 | return array_merge( 12 | $breadcrumbs, 13 | array( 14 | array( 15 | 'title' => __( 'Manage Attendees', 'wporg-translate-events-2024' ), 16 | 'url' => null, 17 | ), 18 | ) 19 | ); 20 | } 21 | ); 22 | 23 | render_page( 24 | __DIR__ . '/render.php', 25 | __( 'Manage Attendees', 'wporg-translate-events-2024' ), 26 | $attributes 27 | ); 28 | }, 29 | ) 30 | ); 31 | -------------------------------------------------------------------------------- /themes/wporg-translate-events-2024/blocks/header/render.php: -------------------------------------------------------------------------------- 1 | 9 | > 10 | 11 | 12 | 13 | <?php echo esc_html( $html_title ); ?> 14 | 15 | 16 | > 17 | 18 |
    19 | 20 | 21 | -------------------------------------------------------------------------------- /themes/wporg-translate-events-2024/blocks/event-attendance-mode/index.php: -------------------------------------------------------------------------------- 1 | function ( array $attributes, $content, $block ) { 10 | if ( ! isset( $block->context['postId'] ) ) { 11 | return ''; 12 | } 13 | $event_id = $block->context['postId']; 14 | $event = Translation_Events::get_event_repository()->get_event( $event_id ); 15 | if ( ! $event ) { 16 | return ''; 17 | } 18 | 19 | return '
    20 | ' . esc_html( $event->attendance_mode() ) . '
    '; 21 | }, 22 | ) 23 | ); 24 | -------------------------------------------------------------------------------- /includes/event/event-query-result.php: -------------------------------------------------------------------------------- 1 | events = $events; 21 | $this->event_ids = array_map( 22 | function ( $event ) { 23 | return $event->id(); 24 | }, 25 | $events, 26 | ); 27 | // The call to intval() is required because WP_Query::max_num_pages is sometimes a float, despite being type-hinted as int. 28 | $this->page_count = intval( $page_count ); 29 | $this->current_page = intval( $current_page ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /templates/event-create.php: -------------------------------------------------------------------------------- 1 | __( 'Translation Events', 'gp-translation-events' ) . ' - ' . esc_html( $page_title ), 19 | 'page_title' => $page_title, 20 | 'breadcrumbs' => array( esc_html( $page_title ) ), 21 | ), 22 | ); 23 | ?> 24 | 25 |
    26 | 27 | 28 |
    29 | 30 | 31 | -------------------------------------------------------------------------------- /src/blocks/example/save.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React hook that is used to mark the block wrapper element. 3 | * It provides all the necessary props like the class name. 4 | * 5 | * @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-block-editor/#useblockprops 6 | */ 7 | import { useBlockProps } from '@wordpress/block-editor'; 8 | 9 | /** 10 | * The save function defines the way in which the different attributes should 11 | * be combined into the final markup, which is then serialized by the block 12 | * editor into `post_content`. 13 | * 14 | * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-edit-save/#save 15 | * 16 | * @return {Element} Element to render. 17 | */ 18 | export default function save() { 19 | return ( 20 |

    21 | { 'Example – hello from the saved content!' } 22 |

    23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /themes/wporg-translate-events-2024/blocks/event-description/index.php: -------------------------------------------------------------------------------- 1 | function ( array $attributes ) { 11 | if ( ! isset( $attributes['id'] ) ) { 12 | return ''; 13 | } 14 | $event_id = $attributes['id']; 15 | $event = Translation_Events::get_event_repository()->get_event( $event_id ); 16 | if ( ! $event ) { 17 | return ''; 18 | } 19 | ob_start(); 20 | ?> 21 | 22 | description() ) ) ); ?> 23 | 24 | array( 7 | 'title' => array( 8 | 'type' => 'string', 9 | ), 10 | ), 11 | // The $attributes argument cannot be removed despite not being used in this function, 12 | // because otherwise it won't be available in render.php. 13 | // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found 14 | 'render_callback' => function ( array $attributes ) { 15 | // The site header must be rendered before the call to wp_head() in render.php, so that styles and 16 | // scripts of the referenced blocks are registered. 17 | ob_start(); 18 | require __DIR__ . '/site-header.php'; 19 | $site_header = do_blocks( ob_get_clean() ); 20 | 21 | ob_start(); 22 | require __DIR__ . '/render.php'; 23 | return ob_get_clean(); 24 | }, 25 | ) 26 | ); 27 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wordpress/wporg-gp-translation-events", 3 | "description": "", 4 | "license": "GPL-2.0-only", 5 | "require-dev": { 6 | "phpunit/phpunit": "^9.6.16", 7 | "yoast/phpunit-polyfills": "^2.0.0", 8 | "wp-coding-standards/wpcs": "^3.0" 9 | }, 10 | "scripts":{ 11 | "lint": "phpcs --standard=phpcs.xml -s", 12 | "lint:fix": "phpcbf --standard=phpcs.xml", 13 | "dev:start": "wp-env start && wp-env run cli wp rewrite structure '/%postname%/'", 14 | "dev:debug": "wp-env start --xdebug", 15 | "dev:stop": "wp-env stop", 16 | "dev:db:schema": "wp-env run cli --env-cwd=wp-content/plugins/wporg-gp-translation-events sh -c 'wp db query < schema.sql'", 17 | "dev:test": "wp-env run tests-cli --env-cwd=wp-content/plugins/wporg-gp-translation-events ./vendor/bin/phpunit" 18 | }, 19 | "config": { 20 | "allow-plugins": { 21 | "dealerdirect/phpcodesniffer-composer-installer": true 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /themes/wporg-translate-events-2024/blocks/pages/events/event-details/index.php: -------------------------------------------------------------------------------- 1 | function ( array $attributes ) { 10 | $event_id = $attributes['event_id'] ?? array(); 11 | $event = Translation_Events::get_event_repository()->get_event( $event_id ); 12 | add_filter( 13 | 'wporg_block_site_breadcrumbs', 14 | function ( $breadcrumbs ) use( $event ): array { 15 | return array_merge( 16 | $breadcrumbs, 17 | array( 18 | array( 19 | 'title' => esc_html( $event->title() ), 20 | 'url' => null, 21 | ), 22 | ) 23 | ); 24 | } 25 | ); 26 | 27 | if ( ! $event ) { 28 | return ''; 29 | } 30 | $attributes['event'] = $event; 31 | 32 | render_page( 33 | __DIR__ . '/render.php', 34 | esc_html( $event->title() ), 35 | $attributes 36 | ); 37 | }, 38 | ) 39 | ); 40 | -------------------------------------------------------------------------------- /themes/wporg-translate-events-2024/blocks/event-load-more-button/index.php: -------------------------------------------------------------------------------- 1 | function ( $attributes ) { 11 | $event_filter = $attributes['filter'] ?? ''; 12 | $next_page = $attributes['next_page'] ?? 1; 13 | 14 | if ( ! $event_filter || ! $next_page ) { 15 | return; 16 | } 17 | ob_start(); 18 | ?> 19 | 20 |
    21 | 22 |
    23 | 24 | __( 'Deleted Translation Events', 'gp-translation-events' ), 12 | 'page_title' => __( 'Deleted Translation Events', 'gp-translation-events' ), 13 | ), 14 | ); 15 | ?> 16 | 17 |
    18 |
    19 | events ) ) : ?> 20 | 21 | 22 | $trashed_events_query, 27 | 'pagination_query_param' => 'page', 28 | 'show_start' => true, 29 | 'show_end' => true, 30 | 'relative_time' => false, 31 | 'show_permanent_delete' => true, 32 | ), 33 | ); 34 | ?> 35 | 36 |
    37 |
    38 | 39 | 40 | -------------------------------------------------------------------------------- /themes/wporg-translate-events-2024/blocks/event-edit-link/index.php: -------------------------------------------------------------------------------- 1 | function ( array $attributes, $content, $block ) { 12 | if ( ! isset( $block->context['postId'] ) ) { 13 | return ''; 14 | } 15 | $event_id = $block->context['postId']; 16 | $event = Translation_Events::get_event_repository()->get_event( $event_id ); 17 | if ( ! $event ) { 18 | return ''; 19 | } 20 | 21 | ob_start(); 22 | if ( ! current_user_can( 'edit_translation_event', $event->id() ) ) { 23 | return ''; 24 | } 25 | ?> 26 | 29 | 30 | 31 | __( 'Translation Events', 'gp-translation-events' ) . ' - ' . esc_html( $page_title . ' - ' . $event->title() ), 19 | 'page_title' => $page_title, 20 | 'breadcrumbs' => array( esc_html( $page_title ) ), 21 | ), 22 | ); 23 | ?> 24 | 25 |
    26 | 27 | 28 |
    29 | 30 |
    31 | id() ) ) : ?> 32 | 33 | 34 |
    35 | 36 | 37 | -------------------------------------------------------------------------------- /themes/wporg-translate-events-2024/blocks/attendee-avatar-name/index.php: -------------------------------------------------------------------------------- 1 | function ( array $attributes ) { 8 | if ( ! isset( $attributes['user_id'] ) ) { 9 | return ''; 10 | } 11 | $user_id = $attributes['user_id']; 12 | $is_new_contributor = ! empty( $attributes['is_new_contributor'] ); 13 | ob_start(); 14 | ?> 15 | 16 |
    17 | 18 | 19 | 20 | 21 | 22 |
    23 | 24 | gp_factory = $gp_factory; 13 | $this->set = $this->gp_factory->translation_set->create_with_project_and_locale(); 14 | } 15 | 16 | public function create( int $user_id, $date_added = null ) { 17 | $original = $this->gp_factory->original->create( 18 | array( 19 | 'project_id' => $this->set->project->id, 20 | 'status' => '+active', 21 | 'singular' => 'foo', 22 | ) 23 | ); 24 | if ( $date_added ) { 25 | $original->update( array( 'date_added' => $date_added->format( 'Y-m-d H:i:s' ) ) ); 26 | } 27 | 28 | $translation = $this->gp_factory->translation->create( 29 | array( 30 | 'user_id' => $user_id, 31 | 'translation_set_id' => $this->set->id, 32 | 'original_id' => $original->id, 33 | 'status' => 'waiting', 34 | ) 35 | ); 36 | if ( $date_added ) { 37 | $translation->update( array( 'date_added' => $date_added->format( 'Y-m-d H:i:s' ) ) ); 38 | } 39 | return $translation; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /themes/wporg-translate-events-2024/blocks/attendee-list/index.php: -------------------------------------------------------------------------------- 1 | function ( array $attributes ) { 11 | if ( ! isset( $attributes['id'] ) ) { 12 | return ''; 13 | } 14 | $event_id = $attributes['id']; 15 | $view_type = $attributes['view_type'] ?? 'list'; 16 | $user_id = get_current_user_id(); 17 | $attendees = Translation_Events::get_attendee_repository()->get_attendees( $event_id ); 18 | if ( empty( $attendees ) ) { 19 | return ''; 20 | } 21 | $attendees_not_contributing = array_filter( 22 | $attendees, 23 | function ( Attendee $attendee ) { 24 | return ! $attendee->is_contributor(); 25 | } 26 | ); 27 | ob_start(); 28 | if ( 'table' === $view_type ) { 29 | $event = Translation_Events::get_event_repository()->get_event( $event_id ); 30 | include_once 'render-table.php'; 31 | } else { 32 | include_once 'render-list.php'; 33 | } 34 | return ob_get_clean(); 35 | }, 36 | ) 37 | ); 38 | -------------------------------------------------------------------------------- /includes/routes/event/create.php: -------------------------------------------------------------------------------- 1 | request ) ) ); 21 | exit; 22 | } 23 | 24 | if ( ! current_user_can( 'create_translation_event' ) ) { 25 | $this->die_with_error( 'You do not have permission to create events.', 403 ); 26 | } 27 | 28 | $now = Translation_Events::now(); 29 | 30 | $event = new Event( 31 | get_current_user_id(), 32 | new Event_Start_Date( $now->format( 'Y-m-d H:i:s' ) ), 33 | new Event_End_Date( $now->modify( '+1 hour' )->format( 'Y-m-d H:i:s' ) ), 34 | new DateTimeZone( 'UTC' ), 35 | 'draft', 36 | '', 37 | '', 38 | ); 39 | 40 | $this->use_theme(); 41 | $this->tmpl( 42 | 'event-create', 43 | array( 44 | 'event' => $event, 45 | ), 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /includes/routes/event/edit.php: -------------------------------------------------------------------------------- 1 | event_repository = Translation_Events::get_event_repository(); 18 | } 19 | 20 | public function handle( int $event_id ): void { 21 | global $wp; 22 | if ( ! is_user_logged_in() ) { 23 | wp_safe_redirect( wp_login_url( home_url( $wp->request ) ) ); 24 | exit; 25 | } 26 | 27 | $event = $this->event_repository->get_event( $event_id ); 28 | if ( ! $event ) { 29 | $this->die_with_404(); 30 | } 31 | 32 | if ( ! current_user_can( 'edit_translation_event', $event->id() ) ) { 33 | $this->die_with_error( esc_html__( 'You do not have permission to edit this event.', 'gp-translation-events' ), 403 ); 34 | } 35 | 36 | include ABSPATH . 'wp-admin/includes/post.php'; 37 | 38 | $this->use_theme(); 39 | $this->tmpl( 40 | 'event-edit', 41 | array( 42 | 'event' => $event, 43 | 'event_id' => $event->id(), 44 | ), 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /themes/wporg-translate-events-2024/blocks/event-title/index.php: -------------------------------------------------------------------------------- 1 | function ( array $attributes, $content, $block ) { 11 | if ( ! isset( $block->context['postId'] ) ) { 12 | return ''; 13 | } 14 | $event_id = get_the_ID(); 15 | ob_start(); 16 | $event = Translation_Events::get_event_repository()->get_event( $event_id ); 17 | if ( ! $event ) { 18 | return ''; 19 | } 20 | $url = Urls::event_details( $event->id() ); 21 | ?> 22 | attendance_mode() ) { 24 | // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped 25 | echo do_blocks( sprintf( '', wp_json_encode( array( 'css_class' => 'video-icon-on-title' ) ) ) ); 26 | } 27 | ?> 28 |

    29 | 30 | title() ); ?> 31 | 32 |

    33 | 35 | { __( 'Example – hello from the editor!', 'example' ) } 36 |

    37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /tests/event/event-date.php: -------------------------------------------------------------------------------- 1 | assertEquals( 'UTC', $start->timezone()->getName() ); 12 | $this->assertEquals( 'UTC', $start->utc()->getTimezone()->getName() ); 13 | $this->assertEquals( '2024-03-07 12:00:00', $start->utc()->format( 'Y-m-d H:i:s' ) ); 14 | $this->assertEquals( '2024-03-07 12:00:00', $start->format( 'Y-m-d H:i:s' ) ); 15 | $this->assertEquals( '2024-03-07 12:00:00', strval( $start ) ); 16 | $this->assertEquals( 'Thursday, March 7, 2024', $start->utc()->format( 'l, F j, Y' ) ); 17 | $this->assertEquals( 'Thursday, March 7, 2024', $start->format( 'l, F j, Y' ) ); 18 | 19 | $start = new Event_Start_Date( '2024-03-07 12:00:00', new \DateTimeZone( 'Asia/Taipei' ) ); 20 | $this->assertEquals( 'Asia/Taipei', $start->timezone()->getName() ); 21 | $this->assertEquals( 'UTC', $start->utc()->getTimezone()->getName() ); 22 | $this->assertEquals( '2024-03-07 20:00:00', $start->format( 'Y-m-d H:i:s' ) ); 23 | $this->assertEquals( '2024-03-07 12:00:00', $start->utc()->format( 'Y-m-d H:i:s' ) ); 24 | $this->assertEquals( '2024-03-07 12:00:00', strval( $start ) ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /themes/wporg-translate-events-2024/blocks/event-date/index.php: -------------------------------------------------------------------------------- 1 | function ( array $attributes, $content, $block ) { 10 | if ( ! isset( $block->context['postId'] ) ) { 11 | return ''; 12 | } 13 | $event_id = get_the_ID(); 14 | $event = Translation_Events::get_event_repository()->get_event( $event_id ); 15 | if ( ! $event ) { 16 | return ''; 17 | } 18 | $start = $event->start()->format( 'F j, Y' ); 19 | return ''; 20 | }, 21 | ) 22 | ); 23 | 24 | register_block_type( 25 | 'wporg-translate-events-2024/event-end', 26 | array( 27 | // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found 28 | 'render_callback' => function ( array $attributes, $content, $block ) { 29 | if ( ! isset( $block->context['postId'] ) ) { 30 | return ''; 31 | } 32 | $event_id = $block->context['postId']; 33 | $event = Translation_Events::get_event_repository()->get_event( $event_id ); 34 | if ( ! $event ) { 35 | return ''; 36 | } 37 | $end = $event->end()->format( 'F j, Y' ); 38 | return '

    ' . esc_html( $end ) . '

    '; 39 | }, 40 | ) 41 | ); 42 | -------------------------------------------------------------------------------- /themes/wporg-translate-events-2024/blocks/event-template/index.php: -------------------------------------------------------------------------------- 1 | function ( array $attributes, $content, $block ) { 9 | if ( ! isset( $attributes['id'] ) ) { 10 | return ''; 11 | } 12 | 13 | $query = new \WP_Query( 14 | array( 15 | 'p' => intval( $attributes['id'] ), 16 | 'post_type' => Translation_Events::CPT, 17 | ) 18 | ); 19 | 20 | $block_content = ''; 21 | while ( $query->have_posts() ) { 22 | $query->the_post(); 23 | $block_instance = $block->parsed_block; 24 | $filter_block_context = static function ( $context ) use ( $attributes ) { 25 | $context['postId'] = $attributes['id']; 26 | $context['postType'] = Translation_Events::CPT; 27 | return $context; 28 | }; 29 | 30 | // Use an early priority to so that other 'render_block_context' filters have access to the values. 31 | add_filter( 'render_block_context', $filter_block_context, 1 ); 32 | $block_content = ( new \WP_Block( $block_instance ) )->render( array( 'dynamic' => false ) ); 33 | remove_filter( 'render_block_context', $filter_block_context, 1 ); 34 | } 35 | wp_reset_postdata(); 36 | return $block_content; 37 | }, 38 | ) 39 | ); 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wporg-gp-translation-events 2 | 3 | Here we are implementing Translation Events, as discussed in our Polyglots Make P2 Post: [Translation Events Prototype](https://make.wordpress.org/polyglots/2024/02/28/translation-events-prototype/). 4 | 5 | ## Development environment 6 | First follow [instructions to install `wp-env`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-env/#prerequisites). 7 | 8 | Then install dependencies: 9 | 10 | ```shell 11 | composer install 12 | ``` 13 | 14 | Then you can run a local WordPress instance with the plugin installed: 15 | 16 | ```shell 17 | composer dev:start 18 | ``` 19 | 20 | Once the environment is running, you must create the database tables needed by this plugin: 21 | 22 | ```shell 23 | composer dev:db:schema 24 | ``` 25 | 26 | WordPress is now running at http://localhost:8888, user: `admin`, password: `password`. 27 | 28 | ### Local environment 29 | 30 | If you are not using `wp-env`, you need to add the tables to the database of your local environment. To do this, you can run this command from the plugin folder: 31 | 32 | ```shell 33 | wp db query < schema.sql 34 | ``` 35 | 36 | ### Tests 37 | 38 | You can run tests in `wp-env` with the following command: 39 | 40 | > Note that `wp-env` must be running. 41 | 42 | ```shell 43 | composer dev:test 44 | ``` 45 | 46 | If you want to run only one test, you can use the following command: 47 | 48 | ```shell 49 | composer dev:test -- --filter methods_name 50 | ``` 51 | -------------------------------------------------------------------------------- /templates/my-events.php: -------------------------------------------------------------------------------- 1 | esc_html__( 'Translation Events', 'gp-translation-events' ) . ' - ' . esc_html__( 'My Events', 'gp-translation-events' ), 17 | 'page_title' => __( 'My Events', 'gp-translation-events' ), 18 | 'breadcrumbs' => array( esc_html__( 'My Events', 'gp-translation-events' ) ), 19 | ), 20 | ); 21 | 22 | ?> 23 | 24 |
    25 | events ) ) : 27 | esc_html_e( 'No events found.', 'gp-translation-events' ); 28 | else : 29 | ?> 30 | $events, 35 | 'pagination_query_param' => 'page', 36 | 'show_start' => true, 37 | 'show_end' => true, 38 | 'relative_time' => false, 39 | 'current_user_attendee_per_event' => $current_user_attendee_per_event, 40 | ), 41 | ); 42 | endif; 43 | ?> 44 |
    45 | 46 | 47 | -------------------------------------------------------------------------------- /themes/wporg-translate-events-2024/blocks/header/site-header.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
    11 | 12 |
    13 | 14 | 15 |
    16 | -------------------------------------------------------------------------------- /includes/routes/event/delete.php: -------------------------------------------------------------------------------- 1 | event_repository = Translation_Events::get_event_repository(); 19 | } 20 | 21 | public function handle( int $event_id ): void { 22 | if ( ! is_user_logged_in() ) { 23 | global $wp; 24 | wp_safe_redirect( wp_login_url( home_url( $wp->request ) ) ); 25 | exit; 26 | } 27 | 28 | $event = $this->event_repository->get_event( $event_id ); 29 | if ( ! $event ) { 30 | $this->die_with_404(); 31 | } 32 | 33 | if ( ! current_user_can( 'manage_translation_events', $event->id() ) ) { 34 | $this->die_with_error( esc_html__( 'You do not have permission to delete events.', 'gp-translation-events' ), 403 ); 35 | } 36 | 37 | if ( ! current_user_can( 'delete_translation_event', $event->id() ) ) { 38 | $this->die_with_error( esc_html__( 'You do not have permission to delete this event.', 'gp-translation-events' ), 403 ); 39 | } 40 | 41 | $this->event_repository->delete_event( $event ); 42 | 43 | wp_safe_redirect( Urls::events_home() ); 44 | exit; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /themes/wporg-translate-events-2024/blocks/event-flag/index.php: -------------------------------------------------------------------------------- 1 | function ( array $attributes, $content, $block ) { 10 | if ( ! isset( $block->context['postId'] ) ) { 11 | return ''; 12 | } 13 | $event_id = $block->context['postId']; 14 | $event = Translation_Events::get_event_repository()->get_event( $event_id ); 15 | if ( ! $event ) { 16 | return ''; 17 | } 18 | $current_user_attendee = Translation_Events::get_attendee_repository()->is_user_attending( $event_id, get_current_user_id() ); 19 | $event_flag = ''; 20 | 21 | if ( ! $current_user_attendee ) { 22 | return ''; 23 | } 24 | if ( $event->is_past() ) { 25 | $event_flag = $current_user_attendee->is_host() ? __( 'Hosted', 'wporg-translate-events-2024' ) : __( 'Attended', 'wporg-translate-events-2024' ); 26 | } else { 27 | $event_flag = $current_user_attendee->is_host() ? __( 'Hosting', 'wporg-translate-events-2024' ) : __( 'Attending', 'wporg-translate-events-2024' ); 28 | } 29 | 30 | if ( ! $event_flag ) { 31 | return ''; 32 | } 33 | 34 | ob_start(); 35 | ?> 36 | 37 | event_repository = Translation_Events::get_event_repository(); 18 | } 19 | 20 | public function handle(): void { 21 | if ( ! is_user_logged_in() ) { 22 | global $wp; 23 | wp_safe_redirect( wp_login_url( home_url( $wp->request ) ) ); 24 | exit; 25 | } 26 | 27 | if ( ! current_user_can( 'manage_translation_events' ) ) { 28 | $this->die_with_error( 'You do not have permission to manage events.', 403 ); 29 | } 30 | 31 | $current_page = 1; 32 | // phpcs:disable WordPress.Security.NonceVerification.Recommended 33 | if ( isset( $_GET['page'] ) ) { 34 | $value = sanitize_text_field( wp_unslash( $_GET['page'] ) ); 35 | if ( is_numeric( $value ) ) { 36 | $current_page = (int) $value; 37 | } 38 | } 39 | // phpcs:enable 40 | 41 | $trashed_events_query = $this->event_repository->get_trashed_events( $current_page, 10 ); 42 | 43 | $this->tmpl( 44 | 'trashed-events', 45 | array( 46 | 'trashed_events_query' => $trashed_events_query, 47 | ), 48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /themes/wporg-translate-events-2024/blocks/event-trash-link/index.php: -------------------------------------------------------------------------------- 1 | function ( array $attributes, $content, $block ) { 12 | if ( ! isset( $block->context['postId'] ) ) { 13 | return ''; 14 | } 15 | $event_id = $block->context['postId']; 16 | $event = Translation_Events::get_event_repository()->get_event( $event_id ); 17 | if ( ! $event ) { 18 | return ''; 19 | } 20 | 21 | ob_start(); 22 | if ( ! current_user_can( 'trash_translation_event', $event->id() ) ) { 23 | return ''; 24 | } 25 | if ( $event->is_trashed() ) : 26 | ?> 27 | 30 | 31 | 32 | 33 | 36 | 37 | 38 | - 21 | --health-cmd "mysqladmin ping" 22 | --health-interval 10s 23 | --health-timeout 5s 24 | --health-retries 3 25 | 26 | strategy: 27 | fail-fast: false 28 | matrix: 29 | php: [ '7.4', '8.3' ] 30 | 31 | steps: 32 | - name: Check out Git repository 33 | uses: actions/checkout@v4 34 | 35 | - name: Setup PHP 36 | uses: shivammathur/setup-php@v2 37 | with: 38 | php-version: ${{ matrix.php }} 39 | coverage: 'none' 40 | tools: composer 41 | 42 | - name: Install dependencies 43 | uses: ramsey/composer-install@v2 44 | with: 45 | composer-options: "--no-progress --no-ansi --no-interaction" 46 | 47 | - name: Install WordPress test setup 48 | run: bash bin/install-wp-tests.sh wordpress_test root password 127.0.0.1:${{ job.services.mysql.ports[3306] }} latest 49 | 50 | - name: Setup problem matchers for PHPUnit 51 | run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" 52 | 53 | - name: Run tests 54 | env: 55 | WP_TESTS_DIR: /tmp/wordpress-tests-lib/ 56 | run: vendor/bin/phpunit 57 | -------------------------------------------------------------------------------- /includes/routes/event/trash.php: -------------------------------------------------------------------------------- 1 | event_repository = Translation_Events::get_event_repository(); 21 | } 22 | 23 | public function handle( int $event_id ): void { 24 | if ( ! is_user_logged_in() ) { 25 | global $wp; 26 | wp_safe_redirect( wp_login_url( home_url( $wp->request ) ) ); 27 | exit; 28 | } 29 | 30 | $event = $this->event_repository->get_event( $event_id ); 31 | if ( ! $event ) { 32 | $this->die_with_404(); 33 | } 34 | 35 | if ( ! current_user_can( 'trash_translation_event', $event->id() ) ) { 36 | $this->die_with_error( esc_html__( 'You do not have permission to delete or restore this event.', 'gp-translation-events' ), 403 ); 37 | } 38 | 39 | if ( ! $event->is_trashed() ) { 40 | // Trash. 41 | $this->event_repository->trash_event( $event ); 42 | wp_safe_redirect( Urls::events_home() ); 43 | } else { 44 | // Restore. 45 | $event->set_status( 'draft' ); 46 | $this->event_repository->update_event( $event ); 47 | wp_safe_redirect( Urls::event_edit( $event->id() ) ); 48 | } 49 | 50 | exit; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /themes/wporg-translate-events-2024/blocks/attendee-list/render-list.php: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 |

    11 | 15 |

    16 | 17 | 18 | 19 |
    20 | 23 | 24 |
    25 | 35 | is_remote() ) : ?> 36 | 37 | 38 |
    39 | 40 | 41 | 44 |
    45 | 46 | -------------------------------------------------------------------------------- /includes/routes/user/my-events.php: -------------------------------------------------------------------------------- 1 | event_repository = Translation_Events::get_event_repository(); 21 | $this->attendee_repository = Translation_Events::get_attendee_repository(); 22 | } 23 | 24 | public function handle(): void { 25 | global $wp; 26 | if ( ! is_user_logged_in() ) { 27 | wp_safe_redirect( wp_login_url( home_url( $wp->request ) ) ); 28 | exit; 29 | } 30 | 31 | $user_id = get_current_user_id(); 32 | 33 | $page = 1; 34 | // phpcs:disable WordPress.Security.NonceVerification.Recommended 35 | if ( isset( $_GET['page'] ) ) { 36 | $value = sanitize_text_field( wp_unslash( $_GET['page'] ) ); 37 | if ( is_numeric( $value ) ) { 38 | $page = (int) $value; 39 | } 40 | } 41 | // phpcs:enable 42 | 43 | $events = $this->event_repository->get_events_for_user( get_current_user_id(), $page, 10 ); 44 | $event_ids = $events->event_ids; 45 | 46 | $current_user_attendee_per_event = $this->attendee_repository->get_attendees_for_user_for_events( $user_id, $event_ids ); 47 | 48 | $this->use_theme(); 49 | $this->tmpl( 50 | 'my-events', 51 | compact( 52 | 'events', 53 | 'event_ids', 54 | 'current_user_attendee_per_event' 55 | ), 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /includes/project/project-repository.php: -------------------------------------------------------------------------------- 1 | get_results( 20 | $wpdb->prepare( 21 | " 22 | select 23 | o.project_id as project, 24 | group_concat( distinct e.locale ) as locales, 25 | sum(action = 'create') as created, 26 | count(*) as total, 27 | count(distinct user_id) as users 28 | from {$gp_table_prefix}event_actions e, {$gp_table_prefix}originals o 29 | where e.event_id = %d and e.original_id = o.id 30 | group by o.project_id 31 | ", 32 | array( 33 | $event_id, 34 | ) 35 | ) 36 | ); 37 | // phpcs:enable 38 | 39 | $projects = array(); 40 | foreach ( $rows as $row ) { 41 | $row->project = GP::$project->get( $row->project ); 42 | $project_name = $row->project->name; 43 | $parent_project_id = $row->project->parent_project_id; 44 | while ( $parent_project_id ) { 45 | $parent_project = GP::$project->get( $parent_project_id ); 46 | $parent_project_id = $parent_project->parent_project_id; 47 | $project_name = substr( htmlspecialchars_decode( $parent_project->name ), 0, 35 ) . ' - ' . $project_name; 48 | } 49 | $projects[ $project_name ] = $row; 50 | } 51 | 52 | ksort( $projects ); 53 | 54 | return $projects; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /templates/translations/footer.php: -------------------------------------------------------------------------------- 1 | 6 | 7 |
    8 |
    9 | 40 | theme_loader = new Theme_Loader( 'wporg-translate-events-2024' ); 16 | } 17 | 18 | public function tmpl( $template, $args = array(), $honor_api = true ) { 19 | $this->set_notices_and_errors(); 20 | $this->header( 'Content-Type: text/html; charset=utf-8' ); 21 | 22 | if ( ! $this->use_theme ) { 23 | $this->enqueue_legacy_styles(); 24 | Templates::render( $template, $args ); 25 | return; 26 | } 27 | 28 | $json = wp_json_encode( $args ); 29 | // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped 30 | echo do_blocks( "" ); 31 | } 32 | 33 | protected function use_theme( bool $also_in_production = false ): void { 34 | if ( $also_in_production ) { 35 | $this->use_theme = true; 36 | } else { 37 | // Only enable if new design has been explicitly enabled. 38 | $this->use_theme = defined( 'TRANSLATION_EVENTS_NEW_DESIGN' ) && TRANSLATION_EVENTS_NEW_DESIGN; 39 | } 40 | 41 | if ( ! $this->use_theme ) { 42 | return; 43 | } 44 | 45 | $this->theme_loader->load(); 46 | } 47 | 48 | private function enqueue_legacy_styles(): void { 49 | wp_register_style( 50 | 'translation-events-css', 51 | plugins_url( '/assets/css/translation-events.css', realpath( __DIR__ . '/../' ) ), 52 | array( 'dashicons' ), 53 | filemtime( __DIR__ . '/../../assets/css/translation-events.css' ) 54 | ); 55 | wp_enqueue_style( 'translation-events-css' ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /themes/wporg-translate-events-2024/blocks/event-host-list/index.php: -------------------------------------------------------------------------------- 1 | function ( array $attributes ) { 11 | if ( ! isset( $attributes['id'] ) ) { 12 | return ''; 13 | } 14 | $event_id = $attributes['id']; 15 | $event = Translation_Events::get_event_repository()->get_event( $event_id ); 16 | 17 | $attendees = Translation_Events::get_attendee_repository()->get_attendees( $event_id ); 18 | $hosts = array_filter( 19 | $attendees, 20 | function ( Attendee $attendee ) { 21 | return $attendee->is_host(); 22 | } 23 | ); 24 | 25 | $has_hosts = count( $hosts ) > 0; 26 | 27 | if ( ! $has_hosts ) { 28 | $hosts = array( new Attendee( $event->id(), $event->author_id(), true ) ); 29 | } 30 | $hosts_list = array_map( 31 | function ( $host ) { 32 | $url = get_author_posts_url( $host->user_id() ); 33 | $name = get_the_author_meta( 'display_name', $host->user_id() ); 34 | return '' . esc_html( $name ) . ''; 35 | }, 36 | $hosts 37 | ); 38 | 39 | if ( ! $has_hosts ) { 40 | /* translators: %s: Display name of the user who created the event. */ 41 | $hosts_string = __( 'Created by: %s', 'gp-translation-events' ); 42 | } else { 43 | /* translators: %s is a comma-separated list of event hosts (=usernames) */ 44 | $hosts_string = _n( 'Host: %s', 'Hosts: %s', count( $hosts ), 'gp-translation-events' ); 45 | } 46 | return wp_kses( 47 | sprintf( $hosts_string, implode( ', ', $hosts_list ) ), 48 | array( 'a' => array( 'href' => array() ) ) 49 | ); 50 | }, 51 | ) 52 | ); 53 | -------------------------------------------------------------------------------- /tests/lib/stats-factory.php: -------------------------------------------------------------------------------- 1 | query( "delete from {$gp_table_prefix}event_actions" ); 15 | // phpcs:enable 16 | } 17 | 18 | public function create( int $event_id, $user_id, $original_id, $action, $locale = 'aa' ) { 19 | global $wpdb, $gp_table_prefix; 20 | // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery 21 | $wpdb->insert( 22 | // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared 23 | $gp_table_prefix . 'event_actions', 24 | array( 25 | 'event_id' => $event_id, 26 | 'user_id' => $user_id, 27 | 'original_id' => $original_id, 28 | 'action' => $action, 29 | 'locale' => $locale, 30 | ) 31 | ); 32 | } 33 | 34 | public function get_count(): int { 35 | global $wpdb, $gp_table_prefix; 36 | // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery 37 | // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching 38 | // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared 39 | return intval( $wpdb->get_var( "select count(*) from {$gp_table_prefix}event_actions" ) ); 40 | // phpcs:enable 41 | } 42 | 43 | public function get_by_event_id( $event_id ): array { 44 | global $wpdb, $gp_table_prefix; 45 | // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery 46 | // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching 47 | // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared 48 | return $wpdb->get_results( $wpdb->prepare( "select * from {$gp_table_prefix}event_actions where event_id = %s", $event_id ), ARRAY_A ); 49 | // phpcs:enable 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /themes/wporg-translate-events-2024/blocks/event-nav-links/index.php: -------------------------------------------------------------------------------- 1 | function ( $attributes ) { 11 | $page_block_name = esc_html( $attributes['page_block_name'] ); 12 | ob_start(); 13 | ?> 14 | 15 |
    16 |
    17 | 18 | 23 | Deleted Events 24 | 25 | 26 | 27 | 28 | | 29 | 30 | My Events 31 | 32 | 33 | 34 | 35 |
    36 | Create Event 37 |
    38 | 39 | 40 | 41 |
    42 |
    43 | 44 | 2 | 3 | Coding standards 4 | 5 | 6 | 7 | 8 | ./assets/ 9 | ./includes/ 10 | ./src/ 11 | ./templates/ 12 | ./tests/ 13 | ./wporg-gp-translation-events.php 14 | 15 | ./themes/ 16 | ^.*/themes/*/style.css 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | attendee_repository = new Attendee_Repository(); 23 | $this->event_repository = Translation_Events::get_event_repository(); 24 | } 25 | 26 | public function handle( string $event_slug ): void { 27 | global $wp; 28 | $user = wp_get_current_user(); 29 | $is_active_filter = false; 30 | if ( ! is_user_logged_in() ) { 31 | wp_safe_redirect( wp_login_url( home_url( $wp->request ) ) ); 32 | exit; 33 | } 34 | 35 | $event = get_page_by_path( $event_slug, OBJECT, Translation_Events::CPT ); 36 | if ( ! $event ) { 37 | $this->die_with_404(); 38 | } 39 | $event = $this->event_repository->get_event( $event->ID ); 40 | if ( ! $event ) { 41 | $this->die_with_404(); 42 | } 43 | if ( ! current_user_can( 'edit_translation_event_attendees', $event->id() ) ) { 44 | $this->die_with_error( esc_html__( 'You do not have permission to edit this event\'s attendees.', 'gp-translation-events' ), 403 ); 45 | } 46 | if ( gp_get( 'filter' ) && 'hosts' === gp_get( 'filter' ) ) { 47 | $is_active_filter = true; 48 | $attendees = $this->attendee_repository->get_hosts( $event->id() ); 49 | } else { 50 | $attendees = $this->attendee_repository->get_attendees( $event->id() ); 51 | } 52 | 53 | $this->use_theme(); 54 | $this->tmpl( 55 | 'event-attendees', 56 | array( 57 | 'event' => $event, 58 | 'attendees' => $attendees, 59 | 'is_active_filter' => $is_active_filter, 60 | 'event_id' => $event->id(), 61 | ), 62 | ); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /includes/theme-loader.php: -------------------------------------------------------------------------------- 1 | theme = $theme; 10 | } 11 | 12 | public function load(): void { 13 | if ( str_ends_with( get_stylesheet_directory(), $this->theme ) ) { 14 | // Our theme is already the active theme, there's nothing to do here. 15 | return; 16 | } 17 | 18 | if ( class_exists( 'WP_Theme_JSON_Resolver_Gutenberg' ) ) { 19 | // We must clean cached theme.json data to force a new parse of theme.json of the child and parent themes. 20 | \WP_Theme_JSON_Resolver_Gutenberg::clean_cached_data(); 21 | } 22 | 23 | add_filter( 24 | 'template', 25 | function (): string { 26 | // TODO: Calculate automatically. 27 | return 'wporg-parent-2021'; 28 | } 29 | ); 30 | add_filter( 31 | 'stylesheet', 32 | function (): string { 33 | return $this->theme; 34 | } 35 | ); 36 | 37 | global $wp_stylesheet_path, $wp_template_path; 38 | $wp_stylesheet_path = get_stylesheet_directory(); 39 | $wp_template_path = get_template_directory(); 40 | 41 | foreach ( wp_get_active_and_valid_themes() as $theme ) { 42 | if ( file_exists( $theme . '/functions.php' ) ) { 43 | include $theme . '/functions.php'; 44 | } 45 | } 46 | 47 | do_action( 'wporg_translate_events_theme_init' ); 48 | 49 | $this->dequeue_unwanted_assets(); 50 | } 51 | 52 | private function dequeue_unwanted_assets(): void { 53 | // Dequeue styles and scripts from glotpress and from the pub/wporg theme. 54 | // The WordPress.org theme enqueues styles in wp_enqueue_scripts, so we need to dequeue in both styles and scripts. 55 | foreach ( array( 'wp_enqueue_styles', 'wp_enqueue_scripts' ) as $action ) { 56 | add_action( 57 | $action, 58 | function (): void { 59 | wp_styles()->remove( 60 | array( 61 | 'wporg-style', 62 | ) 63 | ); 64 | wp_scripts()->remove( 65 | array( 66 | 'wporg-plugins-skip-link-focus-fix', 67 | ) 68 | ); 69 | }, 70 | 9999 // Run as late as possible to make sure the styles/scripts are not enqueued after we dequeue them. 71 | ); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /includes/routes/attendee/remove.php: -------------------------------------------------------------------------------- 1 | event_repository = Translation_Events::get_event_repository(); 25 | $this->attendee_repository = Translation_Events::get_attendee_repository(); 26 | } 27 | 28 | /** 29 | * Handle the request to remove an attendee from an event. 30 | * 31 | * @param int $event_id The event slug. 32 | * @param int $user_id The user ID. 33 | * @return void 34 | */ 35 | public function handle( int $event_id, int $user_id ): void { 36 | global $wp; 37 | if ( ! is_user_logged_in() ) { 38 | wp_safe_redirect( wp_login_url( home_url( $wp->request ) ) ); 39 | exit; 40 | } 41 | 42 | $event = $this->event_repository->get_event( $event_id ); 43 | if ( ! $event ) { 44 | $this->die_with_404(); 45 | } 46 | if ( ! current_user_can( 'edit_translation_event_attendees', $event->id() ) ) { 47 | $this->die_with_error( esc_html__( 'You do not have permission to edit this event.', 'gp-translation-events' ), 403 ); 48 | } 49 | 50 | $attendee = $this->attendee_repository->get_attendee_for_event_for_user( $event->id(), $user_id ); 51 | if ( $attendee instanceof Attendee ) { 52 | if ( ! current_user_can( 'edit_translation_event_attendees', $event->id() ) ) { 53 | $this->die_with_error( esc_html__( 'You do not have permission to remove this attendee.', 'gp-translation-events' ), 403 ); 54 | } 55 | $this->attendee_repository->remove_attendee( $event->id(), $user_id ); 56 | } 57 | 58 | wp_safe_redirect( Urls::event_attendees( $event->id() ) ); 59 | exit; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /themes/wporg-translate-events-2024/blocks/event-projects/index.php: -------------------------------------------------------------------------------- 1 | function ( array $attributes ) { 15 | if ( ! isset( $attributes['id'] ) ) { 16 | return ''; 17 | } 18 | $event_id = $attributes['id']; 19 | $projects = ( new Project_Repository() )->get_for_event( $event_id ); 20 | if ( empty( $projects ) ) { 21 | return ''; 22 | } 23 | 24 | ob_start(); 25 | ?> 26 | 27 |

    28 | 29 | 56 | event_id = $event_id; 31 | $this->user_id = $user_id; 32 | $this->is_host = $is_host; 33 | $this->is_new_contributor = $is_new_contributor; 34 | $this->is_remote = $is_remote; 35 | $this->contributed_locales = $contributed_locales; 36 | } 37 | 38 | public function event_id(): int { 39 | return $this->event_id; 40 | } 41 | 42 | public function user_id(): int { 43 | return $this->user_id; 44 | } 45 | 46 | public function is_host(): bool { 47 | return $this->is_host; 48 | } 49 | 50 | public function is_new_contributor(): bool { 51 | return $this->is_new_contributor; 52 | } 53 | 54 | public function is_contributor(): bool { 55 | return ! empty( $this->contributed_locales ); 56 | } 57 | 58 | public function is_remote(): bool { 59 | return $this->is_remote; 60 | } 61 | 62 | public function mark_as_host(): void { 63 | $this->is_host = true; 64 | } 65 | 66 | public function mark_as_non_host(): void { 67 | $this->is_host = false; 68 | } 69 | 70 | public function mark_as_new_contributor(): void { 71 | $this->is_new_contributor = true; 72 | } 73 | 74 | public function mark_as_active_contributor(): void { 75 | $this->is_new_contributor = false; 76 | } 77 | 78 | public function mark_as_remote_attendee(): void { 79 | $this->is_remote = true; 80 | } 81 | 82 | public function mark_as_in_person_attendee(): void { 83 | $this->is_remote = false; 84 | } 85 | 86 | /** 87 | * @return string[] 88 | */ 89 | public function contributed_locales(): array { 90 | return $this->contributed_locales; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /autoload.php: -------------------------------------------------------------------------------- 1 | 11 | 23 | is_past() ) : 25 | ?> 26 | 27 |
    28 |
    29 |
    30 |

    31 | 32 |

    33 |
    34 |
    35 | 36 | 37 | 38 | 39 |

    40 | 41 | 42 | Start Date: start()->print_time_html(); ?> 43 | End Date: end()->print_time_html(); ?> 44 |

    45 | 46 | 47 | 48 | 49 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /templates/translations/header.php: -------------------------------------------------------------------------------- 1 | title() ) ) ); 12 | 13 | $breadcrumbs = array( '' . esc_html( $event->title() ) . '', __( 'Translations', 'glotpress' ), $locale->english_name ); 14 | Templates::part( 'breadcrumbs', array( 'extra_items' => $breadcrumbs ) ); 15 | 16 | gp_enqueue_scripts( array( 'gp-editor', 'gp-translations-page' ) ); 17 | wp_localize_script( 18 | 'gp-translations-page', 19 | '$gp_translations_options', 20 | array( 21 | 'sort' => __( 'Sort', 'glotpress' ), 22 | 'filter' => __( 'Filter', 'glotpress' ), 23 | ) 24 | ); 25 | 26 | gp_tmpl_header(); 27 | ?> 28 | 29 |
    30 |

    31 | title() ); ?> 32 | status() ) : ?> 33 | status() ); ?> 34 | 35 |

    36 |
    37 |
    38 |

    39 | english_name 45 | ) 46 | ); 47 | ?> 48 |

    49 | 54 | 55 | 58 | 59 | 62 | 63 | 66 |
    67 | -------------------------------------------------------------------------------- /themes/wporg-translate-events-2024/blocks/contributor-list/index.php: -------------------------------------------------------------------------------- 1 | function ( array $attributes ) { 13 | if ( ! isset( $attributes['id'] ) ) { 14 | return ''; 15 | } 16 | $event_id = $attributes['id']; 17 | $attendees = Translation_Events::get_attendee_repository()->get_attendees( $event_id ); 18 | $contributors = array_filter( 19 | $attendees, 20 | function ( Attendee $attendee ) { 21 | return $attendee->is_contributor(); 22 | } 23 | ); 24 | if ( empty( $contributors ) ) { 25 | return ''; 26 | } 27 | 28 | ob_start(); 29 | ?> 30 | 31 |

    32 | 36 |

    37 | 38 | 39 | 40 |
    41 | 44 | 45 |
    46 | 56 | is_remote() ) : ?> 57 | 58 | 59 |
    60 | 61 | 62 | 65 |
    66 | 67 | event_repository = Translation_Events::get_event_repository(); 27 | $this->attendee_repository = Translation_Events::get_attendee_repository(); 28 | } 29 | 30 | /** 31 | * Handle the request to toggle whether the current user is attending an event onsite or remotely. 32 | * 33 | * @param int $event_id The event ID. 34 | * @param int $user_id The user ID. 35 | * @return void 36 | */ 37 | public function handle( int $event_id, int $user_id ): void { 38 | 39 | $current_user = wp_get_current_user(); 40 | if ( ! $current_user->exists() ) { 41 | $this->die_with_error( esc_html__( 'Only logged-in users can manage the attendance mode of an attendee', 'gp-translation-events' ), 403 ); 42 | } 43 | 44 | if ( ! current_user_can( 'edit_translation_event', $event_id ) ) { 45 | $this->die_with_error( esc_html__( 'You do not have permissions to manage the attendance mode of an attendee', 'gp-translation-events' ), 403 ); 46 | } 47 | $event = $this->event_repository->get_event( $event_id ); 48 | if ( ! $event ) { 49 | $this->die_with_404(); 50 | } 51 | 52 | $affected_attendee = $this->attendee_repository->get_attendee_for_event_for_user( $event_id, $user_id ); 53 | if ( $affected_attendee instanceof Attendee ) { 54 | if ( $affected_attendee->is_remote() ) { 55 | $affected_attendee->mark_as_in_person_attendee(); 56 | } else { 57 | $affected_attendee->mark_as_remote_attendee(); 58 | } 59 | $this->attendee_repository->update_attendee( $affected_attendee ); 60 | } 61 | wp_safe_redirect( Urls::event_attendees( $event->id() ) ); 62 | exit; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /includes/routes/user/host-event.php: -------------------------------------------------------------------------------- 1 | event_repository = Translation_Events::get_event_repository(); 27 | $this->attendee_repository = Translation_Events::get_attendee_repository(); 28 | } 29 | 30 | /** 31 | * Handle the request to toggle whether the current user is hosting an event. 32 | * 33 | * @param int $event_id The event ID. 34 | * @param int $user_id The user ID. 35 | * @return void 36 | */ 37 | public function handle( int $event_id, int $user_id ): void { 38 | $current_user = wp_get_current_user(); 39 | if ( ! $current_user->exists() ) { 40 | $this->die_with_error( esc_html__( "Only logged-in users can manage the event's hosts.", 'gp-translation-events' ), 403 ); 41 | } 42 | 43 | if ( ! current_user_can( 'edit_translation_event', $event_id ) ) { 44 | $this->die_with_error( esc_html__( "You do not have permissions to manage the event's hosts.", 'gp-translation-events' ), 403 ); 45 | } 46 | 47 | $event = $this->event_repository->get_event( $event_id ); 48 | if ( ! $event ) { 49 | $this->die_with_404(); 50 | } 51 | 52 | $affected_attendee = $this->attendee_repository->get_attendee_for_event_for_user( $event_id, $user_id ); 53 | // The user is attending to the event, so if I don't find the attendee, I won't create it. 54 | if ( $affected_attendee instanceof Attendee ) { 55 | if ( $affected_attendee->is_host() ) { 56 | $affected_attendee->mark_as_non_host(); 57 | } else { 58 | $affected_attendee->mark_as_host(); 59 | } 60 | 61 | $this->attendee_repository->update_attendee( $affected_attendee ); 62 | $this->event_repository->update_event( $event ); 63 | } 64 | 65 | wp_safe_redirect( Urls::event_attendees( $event->id() ) ); 66 | exit; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /themes/wporg-translate-events-2024/README.md: -------------------------------------------------------------------------------- 1 | # Theme for translate.wordpress.org/events 2 | 3 | This is the theme used by the [Translations Events](https://translate.wordpress.org/events) section of the `translate.wordpress.org` site (from now on referred to as `wporg-translate`). 4 | 5 | > Note that currently not all pages are using this theme yet, work is ongoing to rework pages so they use this theme. 6 | 7 | ## Context 8 | This section provides context useful to understand why this theme is structured the way it is, and how it integrates into the wider environment at `wporg-translate`. 9 | 10 | ### Themes at `wporg-translate` 11 | The `wporg-translate` site does not use WordPress themes in the traditional way. Instead, requests are handled by a `Route`, which then renders the template of the requested page. In a traditional WP site, WP itself would decide which page to render, and (for example) apply the header and footer of the currently-active theme. 12 | 13 | At `wporg-translate` this is not the case, the `Route` and the template of the page completely control the markup being rendered, and the styles being used. 14 | 15 | ### How it used to work 16 | 17 | > Note that currently most pages at `wporg-translate` still work this way. Work is ongoing to rework pages so they work as described in the [next section](#how-it-works-now-with-this-theme). 18 | 19 | As described above, a `Route` intercepts the request, then calls the PHP file of the template of the requested page. That PHP file registers whatever styles and scripts are needed, then renders the markup of the full page, including header and footer. 20 | 21 | The templates and styles are provided by the following plugins: 22 | 23 | - `GlotPress` 24 | - `wporg-gp-customizations` 25 | - `wporg-gp-translation-events` 26 | - Maybe other `wporg-gp-*` plugins 27 | 28 | ### How it works now, with this theme 29 | This new behaviour is enabled for a given page when the developer adds a call to `$this->use_theme()` in the `Route` of said page. This results in this theme being "faked" as the currently-active theme for only the ongoing request (see [`Theme_Loader` in `wporg-gp-translation-events`](https://github.com/WordPress/wporg-gp-translation-events/blob/trunk/includes/theme-loader.php)). 30 | 31 | In this case, when the `Route` intercepts the request, instead of calling the PHP template file, it instead renders a block provided by this theme, that is specific to the page to render ( e.g. `wporg-translate-events-2024/page-events-my-events`). This block renders: 32 | 33 | - The header that is common to all pages. 34 | - The content of the specific page being rendered. 35 | - The footer. 36 | -------------------------------------------------------------------------------- /includes/notifications/notifications-schedule.php: -------------------------------------------------------------------------------- 1 | now = $now; 20 | $this->event_repository = $event_repository; 21 | } 22 | 23 | /** 24 | * Schedule emails for events. 25 | * 26 | * @param int $post_id Post ID. 27 | * 28 | * @return void 29 | */ 30 | public function schedule_emails( int $post_id ) { 31 | $event = $this->event_repository->get_event( $post_id ); 32 | if ( ! $event ) { 33 | return; 34 | } 35 | 36 | $this->delete_scheduled_emails( $post_id ); 37 | if ( 'publish' === get_post_status( $post_id ) ) { 38 | $args = array( 39 | 'post_id' => $post_id, 40 | ); 41 | $new_next_1h_schedule = $event->start()->getTimestamp() - HOUR_IN_SECONDS; 42 | $new_next_24h_schedule = $event->start()->getTimestamp() - 24 * HOUR_IN_SECONDS; 43 | if ( $new_next_1h_schedule > $this->now->getTimestamp() ) { 44 | wp_schedule_single_event( $new_next_1h_schedule, 'wporg_gp_translation_events_email_notifications_1h', $args ); 45 | } 46 | if ( $new_next_24h_schedule > $this->now->getTimestamp() ) { 47 | wp_schedule_single_event( $new_next_24h_schedule, 'wporg_gp_translation_events_email_notifications_24h', $args ); 48 | } 49 | } 50 | } 51 | 52 | /** 53 | * Delete scheduled emails for events. 54 | * 55 | * @param int $post_id Post ID. 56 | * 57 | * @return void 58 | */ 59 | public function delete_scheduled_emails( int $post_id ): void { 60 | $args = array( 61 | 'post_id' => $post_id, 62 | ); 63 | 64 | $unscheduled_1h = false; 65 | $unscheduled_24h = false; 66 | $next_1h_schedule = wp_next_scheduled( 'wporg_gp_translation_events_email_notifications_1h', $args ); 67 | $next_24h_schedule = wp_next_scheduled( 'wporg_gp_translation_events_email_notifications_24h', $args ); 68 | if ( $next_1h_schedule ) { 69 | $unscheduled_1h = wp_unschedule_event( $next_1h_schedule, 'wporg_gp_translation_events_email_notifications_1h', $args ); 70 | } 71 | if ( $next_24h_schedule ) { 72 | $unscheduled_24h = wp_unschedule_event( $next_24h_schedule, 'wporg_gp_translation_events_email_notifications_24h', $args ); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /themes/wporg-translate-events-2024/blocks/event-stats/index.php: -------------------------------------------------------------------------------- 1 | function ( array $attributes ) { 13 | if ( ! isset( $attributes['id'] ) ) { 14 | return ''; 15 | } 16 | $event_id = $attributes['id']; 17 | $event_stats = ( new Stats_Calculator() )->for_event( $event_id ); 18 | ob_start(); 19 | ?> 20 | rows() ) ) : ?> 21 | 22 | 23 |

    24 | 25 | 26 | 27 |
    28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | rows() as $_locale => $row ) : ?> 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 |
    language->english_name ); ?>created ); ?>reviewed ); ?>users ); ?>
    totals()->created ); ?>totals()->reviewed ); ?>totals()->users ); ?>
    54 |
    55 | 56 | function ( array $attributes ) { 11 | $event_ids = $attributes['event_ids'] ?? array(); 12 | $event_filter = $attributes['filter_by'] ?? ''; 13 | if ( empty( $event_ids ) ) { 14 | return get_no_result_view(); 15 | } 16 | $show_flag = ! empty( $attributes['show_flag'] ) && true === $attributes['show_flag']; 17 | $next_page = ! empty( $attributes['next_page'] ) ? $attributes['next_page'] : 0; 18 | ob_start(); 19 | ?> 20 |
    21 | 47 |
    48 | 58 | '; 71 | $content .= '
    '; 72 | $content .= sprintf( 73 | '

    %s

    ', 74 | esc_attr__( 'No events found in this category.', 'wporg-translate-events-2024' ) 75 | ); 76 | $content .= '
    '; 77 | 78 | return do_blocks( $content ); 79 | } 80 | 81 | -------------------------------------------------------------------------------- /themes/wporg-translate-events-2024/blocks/pages/events/home/render.php: -------------------------------------------------------------------------------- 1 | $current_events_query['event_ids'] ?? array(), 11 | 'filter_by' => 'current_events_paged', 12 | 'next_page' => ( $current_events_query['page_count'] >= $current_events_query['current_page'] + 1 ) ? $current_events_query['current_page'] + 1 : 0, 13 | ); 14 | 15 | $upcoming_events_data = array( 16 | 'event_ids' => $upcoming_events_query['event_ids'] ?? array(), 17 | 'filter_by' => 'upcoming_events_paged', 18 | 'next_page' => ( $upcoming_events_query['page_count'] >= $upcoming_events_query['current_page'] + 1 ) ? $upcoming_events_query['current_page'] + 1 : 0, 19 | 20 | ); 21 | 22 | $past_events_data = array( 23 | 'event_ids' => $attributes['past_events_query']['event_ids'] ?? array(), 24 | 'filter_by' => 'past_events_paged', 25 | 'next_page' => ( $past_events_query['page_count'] >= $past_events_query['current_page'] + 1 ) ? $past_events_query['current_page'] + 1 : 0, 26 | 27 | ); 28 | 29 | ?> 30 | 31 | 32 |

    33 | 34 | 35 | 36 |

    37 | 38 | 39 | 40 |

    41 | 42 | 43 | -------------------------------------------------------------------------------- /includes/routes/user/attend-event.php: -------------------------------------------------------------------------------- 1 | event_repository = Translation_Events::get_event_repository(); 29 | $this->attendee_repository = Translation_Events::get_attendee_repository(); 30 | $this->attendee_adder = Translation_Events::get_attendee_adder(); 31 | } 32 | 33 | public function handle( int $event_id ): void { 34 | $nonce_name = '_attendee_nonce'; 35 | if ( isset( $_POST['_attendee_nonce'] ) ) { 36 | $nonce_value = sanitize_text_field( wp_unslash( $_POST['_attendee_nonce'] ) ); 37 | if ( ! wp_verify_nonce( $nonce_value, $nonce_name ) ) { 38 | $this->die_with_error( esc_html__( 'You are not authorized to change the attendance mode of this attendee', 'gp-translation-events' ), 403 ); 39 | } 40 | } 41 | $user = wp_get_current_user(); 42 | if ( ! $user ) { 43 | $this->die_with_error( esc_html__( 'Only logged-in users can attend events', 'gp-translation-events' ), 403 ); 44 | } 45 | $user_id = $user->ID; 46 | 47 | $event = $this->event_repository->get_event( $event_id ); 48 | if ( ! $event ) { 49 | $this->die_with_404(); 50 | } 51 | 52 | if ( $event->is_past() ) { 53 | $this->die_with_error( esc_html__( 'Cannot attend or un-attend a past event', 'gp-translation-events' ), 403 ); 54 | } 55 | 56 | $attendee = $this->attendee_repository->get_attendee_for_event_for_user( $event->id(), $user_id ); 57 | $is_remote_attendee = isset( $_POST['attend_remotely'] ); 58 | 59 | if ( $attendee instanceof Attendee ) { 60 | if ( $attendee->is_contributor() ) { 61 | $this->die_with_error( esc_html__( 'Contributors cannot un-attend the event', 'gp-translation-events' ), 403 ); 62 | } 63 | $this->attendee_repository->remove_attendee( $event->id(), $user_id ); 64 | } else { 65 | $attendee = new Attendee( $event->id(), $user_id, false, false, array(), $is_remote_attendee ); 66 | $this->attendee_adder->add_to_event( $event, $attendee ); 67 | } 68 | 69 | wp_safe_redirect( Urls::event_details( $event->id() ) ); 70 | exit; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /includes/attendee/attendee-adder.php: -------------------------------------------------------------------------------- 1 | attendee_repository = $attendee_repository; 14 | } 15 | 16 | /** 17 | * Add an attendee to an event. 18 | * 19 | * @param Event $event Event to which to add the attendee. 20 | * @param Attendee $attendee Attendee to add to the event. 21 | * 22 | * @throws Exception 23 | */ 24 | public function add_to_event( Event $event, Attendee $attendee ): void { 25 | if ( $this->check_is_new_contributor( $event, $attendee->user_id() ) ) { 26 | $attendee->mark_as_new_contributor(); 27 | } 28 | 29 | $this->attendee_repository->insert_attendee( $attendee ); 30 | 31 | // If the event is active right now, 32 | // import stats for translations the user created since the event started. 33 | if ( $event->is_active() ) { 34 | $this->import_stats( $event, $attendee ); 35 | } 36 | } 37 | 38 | private function import_stats( Event $event, Attendee $attendee ): void { 39 | global $wpdb, $gp_table_prefix; 40 | // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared 41 | // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery 42 | // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching 43 | $wpdb->query( 44 | $wpdb->prepare( 45 | " 46 | insert ignore into {$gp_table_prefix}event_actions 47 | (event_id, user_id, original_id, locale, action, happened_at) 48 | select %d, t.user_id, t.original_id, ts.locale, %s, t.date_added 49 | from {$gp_table_prefix}translations t, 50 | {$gp_table_prefix}translation_sets ts 51 | where t.user_id = %d 52 | and t.translation_set_id = ts.id 53 | and t.status in ( 'current', 'waiting', 'changesrequested', 'fuzzy' ) 54 | and date_added between %s and %s 55 | ", 56 | array( 57 | 'event_id' => $event->id(), 58 | 'action' => Stats_Listener::ACTION_CREATE, 59 | 'user_id' => $attendee->user_id(), 60 | 'date_added_after' => $event->start()->utc()->format( 'Y-m-d H:i:s' ), 61 | 'date_added_before' => $event->end()->utc()->format( 'Y-m-d H:i:s' ), 62 | ), 63 | ), 64 | ); 65 | // phpcs:enable 66 | } 67 | 68 | public function check_is_new_contributor( Event $event, int $user_id ): bool { 69 | global $wpdb, $gp_table_prefix; 70 | 71 | // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared 72 | // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery 73 | // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching 74 | $translation_count = $wpdb->get_var( 75 | $wpdb->prepare( 76 | " 77 | select count(*) as cnt 78 | from {$gp_table_prefix}translations 79 | where user_id = %d 80 | and date_added < %s 81 | ", 82 | array( 83 | $user_id, 84 | $event->start()->format( 'Y-m-d H:i:s' ), 85 | ), 86 | ) 87 | ); 88 | // phpcs:enable 89 | 90 | return intval( $translation_count ) <= 10; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /includes/urls.php: -------------------------------------------------------------------------------- 1 | 5 | 6 |
    7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 31 | 36 | 41 | 58 | 59 | 60 | 61 | 62 |
    20 | 30 | 32 | is_remote() ) : ?> 33 | 34 | 35 | 37 | is_host() ) : ?> 38 | 39 | 40 | 42 |
    43 |
    44 | is_host() ) : ?> 45 | 46 | 47 | 48 | 49 | is_hybrid() ) : ?> 50 | 51 | 52 | is_host() ) : ?> 53 |
    54 | 55 |
    56 |
    57 |
    63 |
    64 | 65 | -------------------------------------------------------------------------------- /tests/lib/event-factory.php: -------------------------------------------------------------------------------- 1 | default_generation_definitions = array( 17 | 'post_status' => 'publish', 18 | 'post_title' => new WP_UnitTest_Generator_Sequence( 'Event title %s' ), 19 | 'post_content' => new WP_UnitTest_Generator_Sequence( 'Event content %s' ), 20 | 'post_excerpt' => new WP_UnitTest_Generator_Sequence( 'Event excerpt %s' ), 21 | 'post_type' => Translation_Events::CPT, 22 | ); 23 | } 24 | 25 | public function create_draft( DateTimeImmutable $now ): int { 26 | $timezone = new DateTimeZone( 'Europe/Lisbon' ); 27 | 28 | $event_id = $this->create_event( 29 | $now->modify( '-1 hours' ), 30 | $now->modify( '+1 hours' ), 31 | $timezone, 32 | array(), 33 | ); 34 | 35 | $event = get_post( $event_id ); 36 | $event->post_status = 'draft'; 37 | wp_update_post( $event ); 38 | 39 | return $event_id; 40 | } 41 | 42 | public function create_active( DateTimeImmutable $now, array $attendee_ids = array() ): int { 43 | $timezone = new DateTimeZone( 'Europe/Lisbon' ); 44 | 45 | return $this->create_event( 46 | $now, 47 | $now->modify( '+1 hour' ), 48 | $timezone, 49 | $attendee_ids, 50 | ); 51 | } 52 | 53 | public function create_inactive_past( DateTimeImmutable $now, array $attendee_ids = array() ): int { 54 | $timezone = new DateTimeZone( 'Europe/Lisbon' ); 55 | 56 | return $this->create_event( 57 | $now->modify( '-2 month' ), 58 | $now->modify( '-1 month' ), 59 | $timezone, 60 | $attendee_ids, 61 | ); 62 | } 63 | 64 | public function create_inactive_future( DateTimeImmutable $now, array $attendee_ids = array() ): int { 65 | $timezone = new DateTimeZone( 'Europe/Lisbon' ); 66 | 67 | return $this->create_event( 68 | $now->modify( '+1 month' ), 69 | $now->modify( '+2 month' ), 70 | $timezone, 71 | $attendee_ids, 72 | ); 73 | } 74 | 75 | public function create_event( DateTimeImmutable $start, DateTimeImmutable $end, DateTimeZone $timezone, array $attendee_ids ): int { 76 | $attendee_repository = new Attendee_Repository(); 77 | $event_id = $this->create(); 78 | 79 | $user_id = get_current_user_id(); 80 | if ( ! in_array( $user_id, $attendee_ids, true ) ) { 81 | // The current user will have been added as attending the event, but it was not specified as an attendee by 82 | // the caller of this function. So we remove the current user as attendee. 83 | $attendee_repository->remove_attendee( $event_id, $user_id ); 84 | } 85 | 86 | update_post_meta( $event_id, '_event_start', $start->format( 'Y-m-d H:i:s' ) ); 87 | update_post_meta( $event_id, '_event_end', $end->format( 'Y-m-d H:i:s' ) ); 88 | update_post_meta( $event_id, '_event_timezone', $timezone->getName() ); 89 | 90 | foreach ( $attendee_ids as $attendee_id ) { 91 | $attendee_repository->insert_attendee( new Attendee( $event_id, $attendee_id ) ); 92 | } 93 | 94 | return $event_id; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /themes/wporg-translate-events-2024/blocks/event-attend-button/index.php: -------------------------------------------------------------------------------- 1 | function ( array $attributes ) { 12 | if ( ! isset( $attributes['id'] ) || ! isset( $attributes['user_is_attending'] ) || ! isset( $attributes['user_is_contributor'] ) ) { 13 | return ''; 14 | } 15 | $event_id = $attributes['id']; 16 | $user_is_attending = $attributes['user_is_attending']; 17 | $user_is_contributor = $attributes['user_is_contributor']; 18 | $event = Translation_Events::get_event_repository()->get_event( $event_id ); 19 | if ( ! $event ) { 20 | return ''; 21 | } 22 | 23 | ob_start(); 24 | if ( is_user_logged_in() ) : 25 | ?> 26 |
    27 | end()->is_in_the_past() ) : ?> 28 | 29 |

    30 | 31 | 32 | 33 | 34 |
    35 | 36 | 37 | " /> 38 | 39 | is_remote() ) : ?> 40 | 41 | is_hybrid() ) : ?> 42 |

    43 | Note: This is an onsite-only event. Please only click attend if you are at the event. The host might otherwise remove you.', 'gp-translation-events' ) ); ?> 44 |

    45 | 46 | 47 | is_remote() || $event->is_hybrid() ) : ?> 48 | 49 | 50 | 51 |
    52 | 53 |
    54 | 55 |
    56 |

    57 | end()->is_in_the_past() ) : ?> 58 | 59 | 60 | 61 | 62 |

    63 |
    64 | expectException( InvalidEnd::class ); 19 | new Event( 20 | 0, 21 | new Event_Start_Date( 'now' ), 22 | ( new Event_End_Date( 'now' ) )->modify( '-1 hour' ), 23 | $timezone, 24 | 'publish', 25 | 'Foo title', 26 | '', 27 | ); 28 | } 29 | 30 | public function test_validates_start_and_end_timezone() { 31 | $timezone = new DateTimeZone( 'Europe/Lisbon' ); 32 | 33 | $this->expectException( InvalidStart::class ); 34 | new Event( 35 | 0, 36 | new Event_Start_Date( 'now', $timezone ), 37 | ( new Event_End_Date( 'now', $timezone ) )->modify( '+1 hour' ), 38 | $timezone, 39 | 'publish', 40 | 'Foo title', 41 | '', 42 | ); 43 | } 44 | 45 | public function test_validates_status() { 46 | $timezone = new DateTimeZone( 'Europe/Lisbon' ); 47 | 48 | $this->expectException( InvalidStatus::class ); 49 | new Event( 50 | 0, 51 | new Event_Start_Date( 'now' ), 52 | ( new Event_End_Date( 'now' ) )->modify( '+1 hour' ), 53 | $timezone, 54 | '', 55 | 'Foo title', 56 | '', 57 | ); 58 | } 59 | 60 | public function test_is_active() { 61 | $timezone = new DateTimeZone( 'Europe/Lisbon' ); 62 | $start = new Event_Start_Date( 'now' ); 63 | $end = new Event_End_Date( 'now' ); 64 | 65 | $past_event = new Event( 66 | 0, 67 | $start->modify( '-1 hour' ), 68 | $end, 69 | $timezone, 70 | 'publish', 71 | 'Foo title', 72 | '', 73 | ); 74 | 75 | $active_event = new Event( 76 | 0, 77 | $start, 78 | $end->modify( '+1 hour' ), 79 | $timezone, 80 | 'publish', 81 | 'Foo title', 82 | '', 83 | ); 84 | 85 | $future_event = new Event( 86 | 0, 87 | $start->modify( '+1 hour' ), 88 | $end->modify( '+2 hours' ), 89 | $timezone, 90 | 'publish', 91 | 'Foo title', 92 | '', 93 | ); 94 | 95 | $this->assertFalse( $past_event->is_active() ); 96 | $this->assertTrue( $active_event->is_active() ); 97 | $this->assertFalse( $future_event->is_active() ); 98 | } 99 | 100 | public function test_is_past() { 101 | $timezone = new DateTimeZone( 'Europe/Lisbon' ); 102 | $start = new Event_Start_Date( 'now' ); 103 | $end = new Event_End_Date( 'now' ); 104 | 105 | $past_event = new Event( 106 | 0, 107 | $start->modify( '-1 hour' ), 108 | $end->modify( '-30 minutes' ), 109 | $timezone, 110 | 'publish', 111 | 'Foo title', 112 | '', 113 | ); 114 | 115 | $active_event = new Event( 116 | 0, 117 | $start, 118 | $end->modify( '+1 hour' ), 119 | $timezone, 120 | 'publish', 121 | 'Foo title', 122 | '', 123 | ); 124 | 125 | $future_event = new Event( 126 | 0, 127 | $start->modify( '+1 hour' ), 128 | $end->modify( '+2 hours' ), 129 | $timezone, 130 | 'publish', 131 | 'Foo title', 132 | '', 133 | ); 134 | 135 | $this->assertTrue( $past_event->is_past() ); 136 | $this->assertFalse( $active_event->is_past() ); 137 | $this->assertFalse( $future_event->is_past() ); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /templates/home.php: -------------------------------------------------------------------------------- 1 | __( 'Translation Events', 'gp-translation-events' ), 18 | 'page_title' => __( 'Translation Events', 'gp-translation-events' ), 19 | ), 20 | ); 21 | ?> 22 |
    23 |
    24 | Find more information here.', 'gp-translation-events' ), 29 | 'https://make.wordpress.org/polyglots/2024/05/29/translation-events-inviting-gtes-to-create-and-manage-events/' 30 | ), 31 | array( 'a' => array( 'href' => array() ) ) 32 | ); 33 | ?> 34 |
    35 |
    36 | events ) && empty( $upcoming_events_query->events ) && empty( $past_events_query->post_count ) ) : 38 | esc_html_e( 'No events found.', 'gp-translation-events' ); 39 | endif; 40 | 41 | if ( ! empty( $current_events_query->events ) ) : 42 | ?> 43 |

    44 | $current_events_query, 49 | 'pagination_query_param' => 'current_events_paged', 50 | 'show_end' => true, 51 | ), 52 | ); 53 | endif; 54 | 55 | if ( ! empty( $upcoming_events_query->events ) ) : 56 | ?> 57 |

    58 | $upcoming_events_query, 63 | 'pagination_query_param' => 'upcoming_events_paged', 64 | 'show_start' => true, 65 | ), 66 | ); 67 | endif; 68 | 69 | if ( ! empty( $past_events_query->events ) ) : 70 | ?> 71 |

    72 | $past_events_query, 77 | 'pagination_query_param' => 'past_events_paged', 78 | 'show_end' => true, 79 | ), 80 | ); 81 | endif; 82 | ?> 83 | 84 |
    85 | 86 |
    87 |

    Events I'm Attending

    88 | events ) ) : ?> 89 |

    You don't have any events to attend.

    90 | 91 | $user_attending_events_query, 96 | 'pagination_query_param' => 'user_attending_events_paged', 97 | 'show_start' => true, 98 | 'show_end' => true, 99 | 'show_excerpt' => false, 100 | 'date_format' => 'F j, Y H:i T', 101 | 'relative_time' => false, 102 | 'classes' => array( 'event-attending-list' ), 103 | ), 104 | ); 105 | endif; 106 | ?> 107 |
    108 | 109 |
    110 | 111 | 112 | -------------------------------------------------------------------------------- /includes/routes/event/image.php: -------------------------------------------------------------------------------- 1 | event_repository = Translation_Events::get_event_repository(); 25 | } 26 | 27 | /** 28 | * Handles the request. 29 | * 30 | * Generates an image with the event title. 31 | * 32 | * @param int $event_id The event ID. 33 | */ 34 | public function handle( int $event_id ): void { 35 | if ( ! extension_loaded( 'gd' ) ) { 36 | $this->die_with_error( esc_html__( 'The image cannot be generated because GD extension is not installed.', 'gp-translation-events' ) ); 37 | } 38 | 39 | $event = $this->event_repository->get_event( $event_id ); 40 | $text = ! $event ? esc_html__( 'Translation events', 'gp-translation-events' ) : $event->title(); 41 | $text = '' === $text ? esc_html__( 'Translation events', 'gp-translation-events' ) : $text; 42 | $text = substr( $text, 0, 44 ); // Limit the text to 44 characters. 43 | 44 | $lines = $this->split_text( $text, 22 ); // Limit each line to 22 characters. 45 | $text1 = $lines[0]; 46 | $text2 = $lines[1] ?? ''; 47 | 48 | $image = imagecreatetruecolor( 1200, 675 ); 49 | $bg_color = imagecolorallocate( $image, 35, 40, 45 ); 50 | imagefill( $image, 0, 0, $bg_color ); 51 | $text_color = imagecolorallocate( $image, 255, 255, 255 ); 52 | $font = trailingslashit( dirname( __DIR__, 3 ) ) . 'assets/fonts/eb-garamond/EBGaramond-Regular.ttf'; 53 | $text_size = 70; 54 | $text_angle = 0; 55 | 56 | $text_box1 = imagettfbbox( $text_size, $text_angle, $font, $text1 ); 57 | $text_width1 = $text_box1[4] - $text_box1[0]; 58 | $text_x1 = ( 1200 - $text_width1 ) / 2; 59 | $text_y1 = 350; 60 | if ( '' !== $text2 ) { 61 | $text_y1 -= 50; 62 | } 63 | 64 | if ( '' !== $text2 ) { 65 | $text_box2 = imagettfbbox( $text_size, $text_angle, $font, $text2 ); 66 | $text_width2 = $text_box2[4] - $text_box2[0]; 67 | $text_x2 = ( 1200 - $text_width2 ) / 2; 68 | $text_y2 = $text_y1 + 110; 69 | imagettftext( $image, $text_size, $text_angle, $text_x2, $text_y2, $text_color, $font, $text2 ); 70 | } 71 | 72 | imagettftext( $image, $text_size, $text_angle, $text_x1, $text_y1, $text_color, $font, $text1 ); 73 | 74 | header( 'Content-type: image/png' ); 75 | imagepng( $image ); 76 | imagedestroy( $image ); 77 | } 78 | 79 | /** 80 | * Splits a string into two lines based on the maximum line length. 81 | * 82 | * @param string $text The text to split. 83 | * @param int $max_length The maximum length of each line. 84 | * 85 | * @return string[] 86 | */ 87 | private function split_text( string $text, int $max_length ): array { 88 | if ( strlen( $text ) <= $max_length ) { 89 | return array( $text ); 90 | } 91 | 92 | $words = explode( ' ', $text ); 93 | 94 | $line1 = ''; 95 | $line2 = ''; 96 | 97 | foreach ( $words as $word ) { 98 | if ( strlen( $line1 . ' ' . $word ) <= $max_length ) { 99 | $line1 .= ( '' === $line1 ? '' : ' ' ) . $word; 100 | } else { 101 | $line2 .= ( '' === $line2 ? '' : ' ' ) . $word; 102 | } 103 | } 104 | 105 | return array( $line1, $line2 ); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /themes/wporg-translate-events-2024/patterns/front-cover.php: -------------------------------------------------------------------------------- 1 | 13 | 14 |

    15 | 16 | 17 |

    18 | 19 |
    20 |
    21 |
    22 | 23 | 24 |
    25 |
    26 | 27 | 28 | 29 |
    30 |
    31 | 32 | 33 |
    34 |
    35 |
    36 | 37 | __( 'Events Home Cover', 'wporg-translate-events-2024' ), 44 | 'categories' => array( 'featured' ), 45 | 'content' => $content, 46 | ) 47 | ); 48 | -------------------------------------------------------------------------------- /includes/routes/event/details.php: -------------------------------------------------------------------------------- 1 | event_repository = Translation_Events::get_event_repository(); 26 | $this->attendee_repository = Translation_Events::get_attendee_repository(); 27 | $this->project_repository = new Project_Repository(); 28 | $this->stats_calculator = new Stats_Calculator(); 29 | } 30 | 31 | public function handle( string $event_slug ): void { 32 | $user = wp_get_current_user(); 33 | $event = get_page_by_path( $event_slug, OBJECT, Translation_Events::CPT ); 34 | if ( ! $event ) { 35 | $this->die_with_404(); 36 | } 37 | $event = $this->event_repository->get_event( $event->ID ); 38 | if ( ! $event ) { 39 | $this->die_with_404(); 40 | } 41 | 42 | if ( ! current_user_can( 'view_translation_event', $event->id() ) ) { 43 | $this->die_with_error( esc_html__( 'You are not authorized to view this page.', 'gp-translation-events' ), 403 ); 44 | } 45 | 46 | $projects = $this->project_repository->get_for_event( $event->id() ); 47 | $attendees = $this->attendee_repository->get_attendees( $event->id() ); 48 | $current_user_attendee = $attendees[ $user->ID ] ?? null; 49 | $user_is_attending = $current_user_attendee instanceof Attendee; 50 | $user_is_contributor = $user_is_attending && $current_user_attendee->is_contributor(); 51 | 52 | $hosts = array_filter( 53 | $attendees, 54 | function ( Attendee $attendee ) { 55 | return $attendee->is_host(); 56 | } 57 | ); 58 | 59 | $contributors = array_filter( 60 | $attendees, 61 | function ( Attendee $attendee ) { 62 | return $attendee->is_contributor(); 63 | } 64 | ); 65 | 66 | $attendees_not_contributing = array_filter( 67 | $attendees, 68 | function ( Attendee $attendee ) { 69 | return ! $attendee->is_contributor(); 70 | } 71 | ); 72 | 73 | $new_contributor_ids = array_filter( 74 | $contributors, 75 | function ( Attendee $contributor ) { 76 | return $contributor->is_new_contributor(); 77 | } 78 | ); 79 | 80 | try { 81 | $event_stats = $this->stats_calculator->for_event( $event->id() ); 82 | } catch ( Exception $e ) { 83 | // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log 84 | error_log( $e ); 85 | $this->die_with_error( esc_html__( 'Failed to calculate event stats', 'gp-translation-events' ) ); 86 | } 87 | 88 | $this->use_theme(); 89 | $this->tmpl( 90 | 'event-details', 91 | array( 92 | 'event' => $event, 93 | 'user_is_attending' => $user_is_attending, 94 | 'user_is_contributor' => $user_is_contributor, 95 | 'hosts' => $hosts, 96 | 'attendees_not_contributing' => $attendees_not_contributing, 97 | 'contributors' => $contributors, 98 | 'new_contributor_ids' => $new_contributor_ids, 99 | 'event_stats' => $event_stats, 100 | 'projects' => $projects, 101 | 'user' => $user, 102 | 'event_id' => $event->id(), 103 | ), 104 | ); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /templates/parts/header.php: -------------------------------------------------------------------------------- 1 | ' . "\n"; 25 | echo '' . "\n"; 26 | echo '' . "\n"; 27 | echo '' . "\n"; 28 | echo '' . "\n"; 29 | echo '' . "\n"; 30 | echo '' . "\n"; 31 | echo '' . "\n"; 32 | 33 | echo '' . "\n"; 34 | echo '' . "\n"; 35 | echo '' . "\n"; 36 | echo '' . "\n"; 37 | echo '' . "\n"; 38 | echo '' . "\n"; 39 | echo '' . "\n"; 40 | echo '' . "\n"; 41 | echo '' . "\n"; 42 | echo '' . "\n"; 43 | echo '' . "\n"; 44 | } 45 | ); 46 | gp_title( $html_title ); 47 | Templates::part( 'breadcrumbs', array( 'extra_items' => $breadcrumbs ?? array() ) ); 48 | gp_tmpl_header(); 49 | ?> 50 | 51 |
    52 |

    53 | 54 | 55 | 56 | 57 | 58 |

    59 | 60 | 71 | 72 | 73 |

    74 | 75 |

    76 | 77 |
    78 | -------------------------------------------------------------------------------- /includes/stats/stats-listener.php: -------------------------------------------------------------------------------- 1 | event_repository = $event_repository; 22 | } 23 | 24 | public function start(): void { 25 | add_action( 26 | 'gp_translation_created', 27 | function ( $translation ) { 28 | $happened_at = DateTimeImmutable::createFromFormat( 'Y-m-d H:i:s', $translation->date_added, new DateTimeZone( 'UTC' ) ); 29 | if ( ! $translation->user_id ) { 30 | return; 31 | } 32 | $this->handle_action( $translation, $translation->user_id, self::ACTION_CREATE, $happened_at ); 33 | }, 34 | ); 35 | 36 | add_action( 37 | 'gp_translation_saved', 38 | function ( $translation, $translation_before ) { 39 | $user_id = $translation->user_id_last_modified; 40 | $status = $translation->status; 41 | $happened_at = DateTimeImmutable::createFromFormat( 'Y-m-d H:i:s', $translation->date_modified, new DateTimeZone( 'UTC' ) ); 42 | 43 | if ( $translation_before->status === $status ) { 44 | // Translation hasn't changed status, so there's nothing for us to track. 45 | return; 46 | } 47 | 48 | $action = null; 49 | switch ( $status ) { 50 | case 'current': 51 | $action = self::ACTION_APPROVE; 52 | break; 53 | case 'rejected': 54 | $action = self::ACTION_REJECT; 55 | break; 56 | case 'changesrequested': 57 | $action = self::ACTION_REQUEST_CHANGES; 58 | break; 59 | } 60 | 61 | if ( $action && $user_id ) { 62 | $this->handle_action( $translation, $user_id, $action, $happened_at ); 63 | } 64 | }, 65 | 10, 66 | 2, 67 | ); 68 | } 69 | 70 | private function handle_action( GP_Translation $translation, int $user_id, string $action, DateTimeImmutable $happened_at ): void { 71 | try { 72 | // Get events that are active now, for which the user is registered for. 73 | $events = $this->event_repository->get_current_events_for_user( $user_id )->events; 74 | 75 | // phpcs:ignore Generic.Commenting.DocComment.MissingShort 76 | /** @var GP_Translation_Set $translation_set Translation set */ 77 | $translation_set = ( new GP_Translation_Set() )->find_one( array( 'id' => $translation->translation_set_id ) ); 78 | global $wpdb, $gp_table_prefix; 79 | 80 | foreach ( $events as $event ) { 81 | // A given user can only do one action on a specific translation. 82 | // So we insert ignore, which will keep only the first action. 83 | // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared 84 | // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery 85 | // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching 86 | $wpdb->query( 87 | $wpdb->prepare( 88 | "insert ignore into {$gp_table_prefix}event_actions (event_id, locale, user_id, original_id, action, happened_at) values (%d, %s, %d, %d, %s, %s)", 89 | array( 90 | // Start unique key. 91 | 'event_id' => $event->id(), 92 | 'locale' => $translation_set->locale, 93 | 'user_id' => $user_id, 94 | 'original_id' => $translation->original_id, 95 | // End unique key. 96 | 'action' => $action, 97 | 'happened_at' => $happened_at->format( 'Y-m-d H:i:s' ), 98 | ), 99 | ), 100 | ); 101 | // phpcs:enable 102 | } 103 | } catch ( Exception $exception ) { 104 | // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log 105 | error_log( $exception ); 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /templates/event-attendees.php: -------------------------------------------------------------------------------- 1 | __( 'Translation Events', 'gp-translation-events' ), 17 | 'page_title' => __( 'Manage Attendees', 'gp-translation-events' ), 18 | ), 19 | ); 20 | ?> 21 | 22 |
    23 |
    24 | Go to event page 25 |
      26 |
    • 27 |
    • 28 |
    29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 49 | 54 | 59 | 74 | 75 | 76 | 77 |
    43 | user_id(), 48 ); ?> 44 | user_id() ) ); ?> 45 | is_new_contributor() ) : ?> 46 | 47 | 48 | 50 | is_remote() ) : ?> 51 | 52 | 53 | 55 | is_host() ) : ?> 56 | 57 | 58 | 60 |
    61 | is_host() ) : ?> 62 | 63 | 64 | 65 | 66 | is_hybrid() ) : ?> 67 | is_remote() ? esc_html_e( 'Set as on-site', 'gp-translation-events' ) : esc_html_e( 'Set as remote', 'gp-translation-events' ); ?> 68 | 69 | is_host() ) : ?> 70 | 71 | 72 |
    73 |
    78 | 79 |

    80 |
    81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /includes/routes/event/list.php: -------------------------------------------------------------------------------- 1 | event_repository = Translation_Events::get_event_repository(); 18 | } 19 | 20 | public function handle(): void { 21 | $_current_events_paged = 1; 22 | $_upcoming_events_paged = 1; 23 | $_past_events_paged = 1; 24 | $_user_attending_events_paged = 1; 25 | $filter_key = ''; 26 | 27 | // phpcs:disable WordPress.Security.NonceVerification.Recommended 28 | if ( isset( $_GET['current_events_paged'] ) ) { 29 | $value = sanitize_text_field( wp_unslash( $_GET['current_events_paged'] ) ); 30 | if ( is_numeric( $value ) ) { 31 | $_current_events_paged = (int) $value; 32 | $filter_key = 'current_events_query'; 33 | } 34 | } 35 | if ( isset( $_GET['upcoming_events_paged'] ) ) { 36 | $value = sanitize_text_field( wp_unslash( $_GET['upcoming_events_paged'] ) ); 37 | if ( is_numeric( $value ) ) { 38 | $_upcoming_events_paged = (int) $value; 39 | $filter_key = 'upcoming_events_query'; 40 | } 41 | } 42 | if ( isset( $_GET['past_events_paged'] ) ) { 43 | $value = sanitize_text_field( wp_unslash( $_GET['past_events_paged'] ) ); 44 | if ( is_numeric( $value ) ) { 45 | $_past_events_paged = (int) $value; 46 | $filter_key = 'past_events_query'; 47 | } 48 | } 49 | if ( isset( $_GET['user_attending_events_paged'] ) ) { 50 | $value = sanitize_text_field( wp_unslash( $_GET['user_attending_events_paged'] ) ); 51 | if ( is_numeric( $value ) ) { 52 | $_user_attending_events_paged = (int) $value; 53 | $filter_key = 'user_attending_events_query'; 54 | } 55 | } 56 | // phpcs:enable 57 | $tmpl_args = array( 58 | 'current_events_query' => $this->event_repository->get_current_events( $_current_events_paged, 10 ), 59 | 'upcoming_events_query' => $this->event_repository->get_upcoming_events( $_upcoming_events_paged, 10 ), 60 | 'past_events_query' => $this->event_repository->get_past_events( $_past_events_paged, 10 ), 61 | 'user_attending_events_query' => $this->event_repository->get_current_and_upcoming_events_for_user( get_current_user_id(), $_user_attending_events_paged, 10 ), 62 | ); 63 | 64 | $this->use_theme(); 65 | // phpcs:disable WordPress.Security.NonceVerification.Recommended 66 | if ( isset( $_GET['format'] ) ) { 67 | 68 | $format = sanitize_text_field( wp_unslash( $_GET['format'] ) ); 69 | 70 | if ( 'html' !== $format || empty( $filter_key ) ) { 71 | return; 72 | } 73 | 74 | if ( ! empty( $tmpl_args[ $filter_key ]->event_ids ) ) { 75 | $this->handle_ajax( $filter_key, $tmpl_args ); 76 | } 77 | } 78 | 79 | $this->tmpl( 80 | 'home', 81 | $tmpl_args, 82 | ); 83 | } 84 | 85 | public function handle_ajax( $filter_key, $tmpl_args ) { 86 | $event_ids = $tmpl_args[ $filter_key ]->event_ids; 87 | $current_page = $tmpl_args[ $filter_key ]->current_page; 88 | $page_count = $tmpl_args[ $filter_key ]->page_count; 89 | $next_page = ( ( $current_page + 1 ) <= $page_count ) ? $current_page + 1 : 0; 90 | 91 | $list_block_markup = ''; 98 | 99 | $rendered_html = ''; 100 | $parsed_blocks = parse_blocks( do_blocks( $list_block_markup ) ); 101 | foreach ( $parsed_blocks as $block ) { 102 | $rendered_html .= render_block( $block ); 103 | } 104 | 105 | wp_send_json_success( 106 | array( 107 | 'nextPage' => $next_page, 108 | 'html' => $rendered_html, 109 | ) 110 | ); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /themes/wporg-translate-events-2024/blocks/event-contribution-summary/index.php: -------------------------------------------------------------------------------- 1 | function ( array $attributes ) { 14 | if ( ! isset( $attributes['id'] ) ) { 15 | return ''; 16 | } 17 | $event_id = $attributes['id']; 18 | $event = Translation_Events::get_event_repository()->get_event( $event_id ); 19 | if ( ! $event ) { 20 | return ''; 21 | } 22 | $event_stats = ( new Stats_Calculator() )->for_event( $event_id ); 23 | if ( empty( $event_stats->rows() ) ) { 24 | return ''; 25 | } 26 | 27 | $attendees = Translation_Events::get_attendee_repository()->get_attendees( $event_id ); 28 | $contributors = array_filter( 29 | $attendees, 30 | function ( Attendee $attendee ) { 31 | return $attendee->is_contributor(); 32 | } 33 | ); 34 | $new_contributor_ids = array_filter( 35 | $contributors, 36 | function ( Attendee $contributor ) { 37 | return $contributor->is_new_contributor(); 38 | } 39 | ); 40 | ob_start(); 41 | ?> 42 | 43 |

    44 | 45 |

    46 | %1$s event, we had %2$d people %3$s who contributed in %4$d languages (%5$l), translated %6$d strings and reviewed %7$d strings.', 'wporg-translate-events-2024' ), 60 | esc_html( $event->title() ), 61 | esc_html( $event_stats->totals()->users ), 62 | $new_contributors_text, 63 | count( $event_stats->rows() ), 64 | array_map( 65 | function ( $row ) { 66 | return $row->language->english_name; 67 | }, 68 | $event_stats->rows() 69 | ), 70 | esc_html( $event_stats->totals()->created ), 71 | esc_html( $event_stats->totals()->reviewed ) 72 | ), 73 | array( 74 | 'strong' => array(), 75 | ) 76 | ); 77 | ?> 78 | is_new_contributor() ) { 92 | $append_tada = ' 🎉'; 93 | } 94 | return '@' . ( new WP_User( $contributor->user_id() ) )->user_login . $append_tada; 95 | }, 96 | $contributors 97 | ) 98 | ), 99 | array( 100 | 'span' => array( 101 | 'class' => array(), 102 | 'title' => array(), 103 | ), 104 | ) 105 | ); 106 | ?> 107 |

    108 | created = $created; 17 | $this->reviewed = $reviewed; 18 | $this->users = $users; 19 | $this->language = $language; 20 | } 21 | } 22 | 23 | class Event_Stats { 24 | /** 25 | * Associative array of rows, with the locale as key. 26 | * 27 | * @var Stats_Row[] 28 | */ 29 | private array $rows = array(); 30 | 31 | private Stats_Row $totals; 32 | 33 | /** 34 | * Add a stats row. 35 | * 36 | * @throws Exception When incorrect locale is passed. 37 | */ 38 | public function add_row( string $locale, Stats_Row $row ) { 39 | if ( ! $locale ) { 40 | throw new Exception( 'locale must not be empty' ); 41 | } 42 | $this->rows[ $locale ] = $row; 43 | } 44 | 45 | public function set_totals( Stats_Row $totals ) { 46 | $this->totals = $totals; 47 | } 48 | 49 | /** 50 | * Get an associative array of rows, with the locale as key. 51 | * 52 | * @return Stats_Row[] 53 | */ 54 | public function rows(): array { 55 | uasort( 56 | $this->rows, 57 | function ( $a, $b ) { 58 | if ( ! $a->language && ! $b->language ) { 59 | return 0; 60 | } 61 | if ( ! $a->language ) { 62 | return -1; 63 | } 64 | if ( ! $b->language ) { 65 | return 1; 66 | } 67 | 68 | return strcasecmp( $a->language->english_name, $b->language->english_name ); 69 | } 70 | ); 71 | 72 | return $this->rows; 73 | } 74 | 75 | public function totals(): Stats_Row { 76 | return $this->totals; 77 | } 78 | } 79 | 80 | class Stats_Calculator { 81 | /** 82 | * Get stats for an event. 83 | * 84 | * @throws Exception When stats calculation failed. 85 | */ 86 | public function for_event( int $event_id ): Event_Stats { 87 | $stats = new Event_Stats(); 88 | global $wpdb, $gp_table_prefix; 89 | 90 | // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared 91 | // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery 92 | // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching 93 | // phpcs thinks we're doing a schema change but we aren't. 94 | // phpcs:disable WordPress.DB.DirectDatabaseQuery.SchemaChange 95 | $rows = $wpdb->get_results( 96 | $wpdb->prepare( 97 | " 98 | SELECT 99 | ea.locale, 100 | SUM( ea.action = 'create' ) AS created, 101 | count( ea.translate_event_actions_id ) AS total, 102 | COUNT( DISTINCT ea.user_id ) AS users 103 | FROM {$gp_table_prefix}event_actions AS ea 104 | WHERE 105 | ea.event_id = %d 106 | GROUP BY 107 | ea.locale with rollup; 108 | ", 109 | array( 110 | $event_id, 111 | ) 112 | ) 113 | ); 114 | // phpcs:enable 115 | 116 | foreach ( $rows as $index => $row ) { 117 | $is_totals = null === $row->locale; 118 | if ( $is_totals && array_key_last( $rows ) !== $index ) { 119 | // If this is not the last row, something is wrong in the data in the database table 120 | // or there's a bug in the query above. 121 | throw new Exception( 122 | 'Only the last row should have no locale but we found a non-last row with no locale.' 123 | ); 124 | } 125 | 126 | $lang = GP_Locales::by_slug( $row->locale ); 127 | if ( ! $lang ) { 128 | $lang = null; 129 | } 130 | 131 | $stats_row = new Stats_Row( 132 | $row->created, 133 | $row->total - $row->created, 134 | $row->users, 135 | $lang 136 | ); 137 | 138 | if ( ! $is_totals ) { 139 | $stats->add_row( $row->locale, $stats_row ); 140 | } else { 141 | $stats->set_totals( $stats_row ); 142 | } 143 | } 144 | 145 | return $stats; 146 | } 147 | 148 | /** 149 | * Check if an event has stats. 150 | * 151 | * @param int $event_id The id of the event to check. 152 | * 153 | * @return bool True if the event has stats, false otherwise. 154 | */ 155 | public function event_has_stats( int $event_id ): bool { 156 | try { 157 | $stats = $this->for_event( $event_id ); 158 | } catch ( Exception $e ) { 159 | return false; 160 | } 161 | 162 | return ! empty( $stats->rows() ); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /assets/fonts/eb-garamond/license.txt: -------------------------------------------------------------------------------- 1 | Copyright 2017 The EB Garamond Project Authors (https://github.com/octaviopardo/EBGaramond12) 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | https://openfontlicense.org 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | -------------------------------------------------------------------------------- /tests/event/event-image.php: -------------------------------------------------------------------------------- 1 | $html_title, 27 | 'url' => $url, 28 | 'html_description' => $html_description, 29 | 'image_url' => $image_url, 30 | 'page_title' => $page_title, 31 | ), 32 | ); 33 | $output = ob_get_clean(); 34 | 35 | $this->assertStringContainsString( '', $output ); 36 | $this->assertStringContainsString( '', $output ); 37 | $this->assertStringContainsString( '', $output ); 38 | $this->assertStringContainsString( '', $output ); 39 | $this->assertStringContainsString( '', $output ); 40 | $this->assertStringContainsString( '', $output ); 41 | 42 | $this->assertStringContainsString( '', $output ); 43 | $this->assertStringContainsString( '', $output ); 44 | $this->assertStringContainsString( '', $output ); 45 | $this->assertStringContainsString( '', $output ); 46 | $this->assertStringContainsString( '', $output ); 47 | $this->assertStringContainsString( '', $output ); 48 | $this->assertStringContainsString( '', $output ); 49 | $this->assertStringContainsString( '', $output ); 50 | $this->assertStringContainsString( '', $output ); 51 | $this->assertStringContainsString( '', $output ); 52 | } 53 | 54 | /** 55 | * Test that the event image route generates a valid PNG image. 56 | * 57 | * @return void 58 | */ 59 | public function test_handle_generates_image() { 60 | // phpcs:disable 61 | error_reporting( 0 ); 62 | // phpcs:enable 63 | ob_start(); 64 | $this->event_factory = new Event_Factory(); 65 | $event_id = $this->event_factory->create_active( $this->now ); 66 | 67 | $image_route = new Image_Route(); 68 | $image_route->handle( $event_id ); 69 | 70 | $output = ob_get_clean(); 71 | 72 | // Verify the output is not empty and is a valid PNG image. 73 | $this->assertNotEmpty( $output ); 74 | $this->assertStringStartsWith( "\x89PNG", $output, 'The output is not a valid PNG image' ); 75 | 76 | // Create an image resource from the output. 77 | $image = imagecreatefromstring( $output ); 78 | $this->assertNotFalse( $image, 'Failed to create image from output' ); 79 | 80 | // Check the image dimensions. 81 | $width = imagesx( $image ); 82 | $height = imagesy( $image ); 83 | $this->assertEquals( 1200, $width, 'Image width is not 1200 pixels' ); 84 | $this->assertEquals( 675, $height, 'Image height is not 675 pixels' ); 85 | 86 | // Verify the background color (35, 40, 45). 87 | $bg_color = imagecolorat( $image, 0, 0 ); 88 | $bg_rgb = imagecolorsforindex( $image, $bg_color ); 89 | $this->assertEquals( 35, $bg_rgb['red'], 'Background red color is not 35' ); 90 | $this->assertEquals( 40, $bg_rgb['green'], 'Background green color is not 40' ); 91 | $this->assertEquals( 45, $bg_rgb['blue'], 'Background blue color is not 45' ); 92 | 93 | // Check if the text color exists in the image. 94 | $text_color = imagecolorallocate( $image, 255, 255, 255 ); 95 | $text_color_exists = false; 96 | for ( $x = 0; $x < $width; $x++ ) { 97 | for ( $y = 0; $y < $height; $y++ ) { 98 | if ( imagecolorat( $image, $x, $y ) === $text_color ) { 99 | $text_color_exists = true; 100 | break 2; 101 | } 102 | } 103 | } 104 | $this->assertTrue( $text_color_exists, 'Text color not found in the image' ); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /includes/upgrade.php: -------------------------------------------------------------------------------- 1 | Translation_Events::CPT, 82 | 'post_status' => 'publish', 83 | ) 84 | ); 85 | 86 | $events = $query->get_posts(); 87 | $event_repository = Translation_Events::get_event_repository(); 88 | $attendee_repository = Translation_Events::get_attendee_repository(); 89 | 90 | foreach ( $events as $post ) { 91 | $event = $event_repository->get_event( $post->ID ); 92 | if ( ! $event ) { 93 | continue; 94 | } 95 | 96 | $attendees = $attendee_repository->get_attendees( $event->id() ); 97 | 98 | foreach ( $attendees as $attendee ) { 99 | // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared 100 | // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery 101 | // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching 102 | $translation_count = $wpdb->get_var( 103 | $wpdb->prepare( 104 | " 105 | select count(*) as cnt 106 | from {$gp_table_prefix}translations 107 | where user_id = %d 108 | and date_added < %s 109 | ", 110 | array( 111 | $attendee->user_id(), 112 | $event->start()->format( 'Y-m-d H:i:s' ), 113 | ), 114 | ) 115 | ); 116 | 117 | if ( $translation_count > 10 ) { 118 | // Not a new contributor. 119 | continue; 120 | } 121 | 122 | $wpdb->update( 123 | "{$gp_table_prefix}event_attendees", 124 | array( 'is_new_contributor' => 1 ), 125 | array( 126 | 'event_id' => $attendee->event_id(), 127 | 'user_id' => $attendee->user_id(), 128 | ) 129 | ); 130 | // phpcs:enable 131 | } 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /templates/translations/table.php: -------------------------------------------------------------------------------- 1 | 9 |
    10 |
    11 |

    12 | locale, $translation_set->slug ), 19 | esc_html( 20 | gp_project_names_from_root( $project ) 21 | ) 22 | ), 23 | array( 24 | 'a' => array( 25 | 'href' => array(), 26 | 'title' => array(), 27 | ), 28 | ) 29 | ), 30 | esc_html( $locale->name ) 31 | ); 32 | ?> 33 |

    34 |
    35 |
    36 | 41 |
    42 | 43 | text_direction ? ' translation-sets-rtl' : ''; ?> 44 | 54 | 55 | 56 | 57 | 60 | 61 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | translation_set_id ) { 73 | $translation->translation_set_id = $translation_set->id; 74 | } 75 | 76 | $can_approve_translation = GP::$permission->current_user_can( 'approve', 'translation', $translation->id, array( 'translation' => $translation ) ); 77 | gp_tmpl_load( 'translation-row', get_defined_vars() ); 78 | } 79 | ?> 80 | 81 | 84 | 85 | 88 |
    89 | 99 | 100 |
    101 | 106 |
    107 |
    108 | get_static( 'statuses' ) as $legend_status ) : 110 | if ( ( 'changesrequested' === $legend_status ) && ( ! apply_filters( 'gp_enable_changesrequested_status', false ) ) ) { // todo: delete when we merge the gp-translation-helpers in GlotPress. 111 | continue; 112 | } 113 | ?> 114 |
    115 |
    116 | 144 |
    145 | 148 |
    149 |
    150 |
    151 |
    152 |
    153 |
    154 | -------------------------------------------------------------------------------- /tests/urls.php: -------------------------------------------------------------------------------- 1 | event_factory = new Event_Factory(); 17 | $this->event_repository = new Event_Repository( $this->now, new Attendee_Repository() ); 18 | 19 | $this->set_normal_user_as_current(); 20 | } 21 | 22 | public function test_events_home() { 23 | $expected = '/glotpress/events'; 24 | $this->assertEquals( $expected, Urls::events_home() ); 25 | } 26 | 27 | public function test_event_details() { 28 | $event_id = $this->event_factory->create_active( $this->now ); 29 | $event = $this->event_repository->get_event( $event_id ); 30 | 31 | $expected = "/glotpress/events/{$event->slug()}"; 32 | $this->assertEquals( $expected, Urls::event_details( $event_id ) ); 33 | } 34 | 35 | public function test_event_details_draft() { 36 | $event_id = $this->event_factory->create_active( $this->now ); 37 | $post = get_post( $event_id ); 38 | $post->post_status = 'draft'; 39 | wp_update_post( $post ); 40 | 41 | $event = $this->event_repository->get_event( $event_id ); 42 | 43 | $expected = "/glotpress/events/{$event->slug()}"; 44 | $this->assertEquals( $expected, Urls::event_details( $event_id ) ); 45 | } 46 | 47 | public function test_event_details_absolute() { 48 | $event_id = $this->event_factory->create_active( $this->now ); 49 | $event = $this->event_repository->get_event( $event_id ); 50 | 51 | $expected = site_url() . "/glotpress/events/{$event->slug()}"; 52 | $this->assertEquals( $expected, Urls::event_details_absolute( $event_id ) ); 53 | } 54 | 55 | public function test_event_translations() { 56 | $event_id = $this->event_factory->create_active( $this->now ); 57 | $event = $this->event_repository->get_event( $event_id ); 58 | 59 | $expected = "/glotpress/events/{$event->slug()}/translations/pt"; 60 | $this->assertEquals( $expected, Urls::event_translations( $event_id, 'pt' ) ); 61 | 62 | $expected = "/glotpress/events/{$event->slug()}/translations/pt/waiting"; 63 | $this->assertEquals( $expected, Urls::event_translations( $event_id, 'pt', 'waiting' ) ); 64 | } 65 | 66 | public function test_event_edit() { 67 | $event_id = 42; 68 | $expected = "/glotpress/events/edit/$event_id"; 69 | $this->assertEquals( $expected, Urls::event_edit( $event_id ) ); 70 | } 71 | 72 | public function test_event_trash() { 73 | $event_id = 42; 74 | $expected = "/glotpress/events/trash/$event_id"; 75 | $this->assertEquals( $expected, Urls::event_trash( $event_id ) ); 76 | } 77 | 78 | public function test_event_delete() { 79 | $event_id = 42; 80 | $expected = "/glotpress/events/delete/$event_id"; 81 | $this->assertEquals( $expected, Urls::event_delete( $event_id ) ); 82 | } 83 | 84 | public function test_event_toggle_attendee() { 85 | $event_id = 42; 86 | $expected = "/glotpress/events/attend/$event_id"; 87 | $this->assertEquals( $expected, Urls::event_toggle_attendee( $event_id ) ); 88 | } 89 | 90 | public function test_event_toggle_host() { 91 | $user_id = get_current_user_id(); 92 | $event_id = 42; 93 | $expected = "/glotpress/events/host/$event_id/$user_id"; 94 | $this->assertEquals( $expected, Urls::event_toggle_host( $event_id, $user_id ) ); 95 | } 96 | 97 | public function test_event_create() { 98 | $expected = '/glotpress/events/new'; 99 | $this->assertEquals( $expected, Urls::event_create() ); 100 | } 101 | 102 | public function test_my_events() { 103 | $expected = '/glotpress/events/my-events'; 104 | $this->assertEquals( $expected, Urls::my_events() ); 105 | } 106 | 107 | /** 108 | * This test must be last because once it runs, the GP_URL_BASE constant 109 | * will be changed from the default ('/glotpress') to '/'. 110 | */ 111 | public function test_custom_gp_url_base() { 112 | define( 'GP_URL_BASE', '/' ); 113 | $expected = '/events'; 114 | $this->assertEquals( $expected, Urls::events_home() ); 115 | } 116 | 117 | public function test_event_image() { 118 | $event_id = $this->event_factory->create_active( $this->now ); 119 | $expected = trailingslashit( gp_url_public_root() ) . "events/image/$event_id"; 120 | $this->assertEquals( $expected, Urls::event_image( $event_id ) ); 121 | } 122 | 123 | public function test_event_default_image() { 124 | $expected = trailingslashit( gp_url_public_root() ) . 'events/image/0'; 125 | $this->assertEquals( $expected, Urls::event_default_image() ); 126 | $this->assertTrue( $this->starts_with_http_or_https( Urls::event_default_image() ), 'URL does not start with http:// or https://' ); 127 | } 128 | 129 | /** 130 | * Check if a string starts with http:// or https:// 131 | * 132 | * @param string $url The string to check. 133 | * 134 | * @return bool 135 | */ 136 | private function starts_with_http_or_https( string $url ): bool { 137 | return 1 === preg_match( '/^https?:\/\//', strtolower( $url ) ); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /tests/stats/stats-calculator.php: -------------------------------------------------------------------------------- 1 | event_factory = new Event_Factory(); 18 | $this->stats_factory = new Stats_Factory(); 19 | $this->calculator = new Stats_Calculator(); 20 | 21 | $this->set_normal_user_as_current(); 22 | } 23 | 24 | public function test_tells_that_event_has_no_stats() { 25 | $user_id = get_current_user_id(); 26 | $event_id = $this->event_factory->create_active( $this->now, array( $user_id ) ); 27 | $this->assertFalse( $this->calculator->event_has_stats( $event_id ) ); 28 | } 29 | 30 | public function test_tells_that_event_has_stats() { 31 | $user_id = get_current_user_id(); 32 | 33 | $event_id = $this->event_factory->create_active( $this->now, array( $user_id ) ); 34 | $translation_set = $this->factory->translation_set->create_with_project_and_locale(); 35 | $original = $this->create_original_and_translation( $translation_set ); 36 | $this->stats_factory->create( $event_id, $user_id, $original->id, 'create', $translation_set->locale ); 37 | 38 | $this->assertTrue( $this->calculator->event_has_stats( $event_id ) ); 39 | } 40 | 41 | public function test_calculates_stats_for_event() { 42 | $user1_id = 42; 43 | $user2_id = 43; 44 | $user3_id = 44; 45 | 46 | $event1_id = $this->event_factory->create_active( $this->now, array( $user1_id ) ); 47 | $event2_id = $this->event_factory->create_active( $this->now, array( $user1_id ) ); 48 | 49 | // For event1, aa locale, multiple users. 50 | $translation_set_1 = $this->factory->translation_set->create_with_project_and_locale(); 51 | $original_11 = $this->create_original_and_translation( $translation_set_1 ); 52 | $original_12 = $this->create_original_and_translation( $translation_set_1 ); 53 | $original_13 = $this->create_original_and_translation( $translation_set_1 ); 54 | $this->stats_factory->create( $event1_id, $user1_id, $original_11->id, 'create', $translation_set_1->locale ); 55 | $this->stats_factory->create( $event1_id, $user1_id, $original_12->id, 'create', $translation_set_1->locale ); 56 | $this->stats_factory->create( $event1_id, $user2_id, $original_13->id, 'create', $translation_set_1->locale ); 57 | $this->stats_factory->create( $event1_id, $user2_id, $original_11->id, 'approve', $translation_set_1->locale ); 58 | $this->stats_factory->create( $event1_id, $user2_id, $original_12->id, 'reject', $translation_set_1->locale ); 59 | $this->stats_factory->create( $event1_id, $user3_id, $original_13->id, 'request_changes', $translation_set_1->locale ); 60 | 61 | // For event1, bb locale, multiple users. 62 | $translation_set_2 = $this->factory->translation_set->create_with_project_and_locale(); 63 | $original_21 = $this->create_original_and_translation( $translation_set_2 ); 64 | $original_22 = $this->create_original_and_translation( $translation_set_2 ); 65 | $this->stats_factory->create( $event1_id, $user1_id, $original_21->id, 'create', $translation_set_2->locale ); 66 | $this->stats_factory->create( $event1_id, $user2_id, $original_22->id, 'create', $translation_set_2->locale ); 67 | $this->stats_factory->create( $event1_id, $user3_id, $original_21->id, 'approve', $translation_set_2->locale ); 68 | $this->stats_factory->create( $event1_id, $user3_id, $original_22->id, 'request_changes', $translation_set_2->locale ); 69 | 70 | // For event2, which should not be included in the stats. 71 | 72 | $this->stats_factory->create( $event2_id, $user1_id, 31, 'create', $translation_set_1->locale ); 73 | $this->stats_factory->create( $event2_id, $user1_id, 32, 'create', $translation_set_1->locale ); 74 | $this->stats_factory->create( $event2_id, $user2_id, 31, 'approve', $translation_set_1->locale ); 75 | $this->stats_factory->create( $event2_id, $user2_id, 32, 'reject', $translation_set_1->locale ); 76 | 77 | $event1 = get_post( $event1_id ); 78 | $stats = $this->calculator->for_event( $event1->ID ); 79 | $this->assertCount( 2, $stats->rows() ); 80 | 81 | // $translation_set_1 Locale. 82 | $this->assertEquals( 3, $stats->rows()[ $translation_set_1->locale ]->created ); 83 | $this->assertEquals( 3, $stats->rows()[ $translation_set_1->locale ]->reviewed ); 84 | $this->assertEquals( 3, $stats->rows()[ $translation_set_1->locale ]->users ); 85 | 86 | // $translation_set_2 Locale. 87 | $this->assertEquals( 2, $stats->rows()[ $translation_set_2->locale ]->created ); 88 | $this->assertEquals( 2, $stats->rows()[ $translation_set_2->locale ]->reviewed ); 89 | $this->assertEquals( 3, $stats->rows()[ $translation_set_2->locale ]->users ); 90 | 91 | // Totals. 92 | $this->assertEquals( 5, $stats->totals()->created ); 93 | $this->assertEquals( 5, $stats->totals()->reviewed ); 94 | $this->assertEquals( 3, $stats->totals()->users ); 95 | } 96 | 97 | private function create_original_and_translation( $translation_set, $status = 'current' ) { 98 | $original = $this->factory->original->create( array( 'project_id' => $translation_set->project_id ) ); 99 | $this->factory->translation->create( 100 | array( 101 | 'original_id' => $original->id, 102 | 'translation_set_id' => $translation_set->id, 103 | 'status' => $status, 104 | ) 105 | ); 106 | return $original; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /includes/event/event.php: -------------------------------------------------------------------------------- 1 | author_id = $author_id; 65 | $this->validate_times( $start, $end ); 66 | $this->set_start( $start ); 67 | $this->set_end( $end ); 68 | $this->set_timezone( $timezone ); 69 | $this->set_status( $status ); 70 | $this->set_title( $title ); 71 | $this->set_description( $description ); 72 | $this->set_updated_at( $updated_at ); 73 | $this->set_attendance_mode( $attendance_mode ); 74 | } 75 | 76 | public function id(): int { 77 | return $this->id; 78 | } 79 | 80 | public function author_id(): int { 81 | return $this->author_id; 82 | } 83 | 84 | public function start(): Event_Start_Date { 85 | return $this->start; 86 | } 87 | 88 | public function end(): Event_End_Date { 89 | return $this->end; 90 | } 91 | 92 | public function is_published(): bool { 93 | return 'publish' === $this->status; 94 | } 95 | 96 | public function is_draft(): bool { 97 | return 'draft' === $this->status; 98 | } 99 | 100 | public function is_trashed(): bool { 101 | return 'trash' === $this->status; 102 | } 103 | 104 | public function is_active(): bool { 105 | $now = Translation_Events::now(); 106 | return $now >= $this->start->utc() && $now < $this->end->utc(); 107 | } 108 | 109 | public function is_past(): bool { 110 | return $this->end->is_in_the_past(); 111 | } 112 | 113 | public function is_remote(): bool { 114 | return 'remote' === $this->attendance_mode; 115 | } 116 | 117 | public function is_hybrid(): bool { 118 | return 'hybrid' === $this->attendance_mode; 119 | } 120 | 121 | public function timezone(): DateTimeZone { 122 | return $this->timezone; 123 | } 124 | 125 | public function slug(): string { 126 | return $this->slug; 127 | } 128 | 129 | public function status(): string { 130 | return $this->status; 131 | } 132 | 133 | public function title(): string { 134 | return $this->title; 135 | } 136 | 137 | public function description(): string { 138 | return $this->description; 139 | } 140 | 141 | public function updated_at(): DateTimeImmutable { 142 | return $this->updated_at; 143 | } 144 | 145 | public function set_id( int $id ): void { 146 | $this->id = $id; 147 | } 148 | 149 | public function set_slug( string $slug ): void { 150 | $this->slug = $slug; 151 | } 152 | 153 | public function set_start( Event_Start_Date $start ): void { 154 | $this->start = $start; 155 | } 156 | 157 | public function set_end( Event_End_Date $end ): void { 158 | $this->end = $end; 159 | } 160 | 161 | public function set_timezone( DateTimeZone $timezone ): void { 162 | $this->timezone = $timezone; 163 | } 164 | 165 | public function attendance_mode(): string { 166 | return $this->attendance_mode; 167 | } 168 | 169 | /** 170 | * @throws InvalidStatus 171 | */ 172 | public function set_status( string $status ): void { 173 | if ( ! in_array( $status, array( 'draft', 'publish', 'trash' ), true ) ) { 174 | throw new InvalidStatus(); 175 | } 176 | $this->status = $status; 177 | } 178 | 179 | public function set_title( string $title ): void { 180 | $this->title = $title; 181 | } 182 | 183 | public function set_description( string $description ): void { 184 | $this->description = $description; 185 | } 186 | 187 | public function set_updated_at( DateTimeImmutable $updated_at = null ): void { 188 | $this->updated_at = $updated_at ?? Translation_Events::now(); 189 | } 190 | 191 | public function set_attendance_mode( string $attendance_mode ): void { 192 | $this->attendance_mode = $attendance_mode; 193 | } 194 | 195 | /** 196 | * @throws InvalidStart 197 | * @throws InvalidEnd 198 | */ 199 | public function validate_times( Event_Start_Date $start, Event_End_Date $end ) { 200 | if ( $end <= $start ) { 201 | throw new InvalidEnd(); 202 | } 203 | if ( ! $start->getTimezone() || 'UTC' !== $start->getTimezone()->getName() ) { 204 | throw new InvalidStart(); 205 | } 206 | if ( ! $end->getTimezone() || 'UTC' !== $end->getTimezone()->getName() ) { 207 | throw new InvalidEnd(); 208 | } 209 | } 210 | } 211 | --------------------------------------------------------------------------------