Making WordPress.org

Ticket #2733: 2733.diff

File 2733.diff, 36.0 KB (added by egmanekki, 6 years ago)

Initial solution patch

  • wordcamp.org/public_html/wp-content/plugins/wc-post-types/css/shortcodes.css

    diff --git wordcamp.org/public_html/wp-content/plugins/wc-post-types/css/shortcodes.css wordcamp.org/public_html/wp-content/plugins/wc-post-types/css/shortcodes.css
    index 54540de4..c6000cdf 100644
     
    11/*
    22 * [schedule]
    33 */
     4 .wcb-favourite-session {
     5        background: #e0f8ff;
     6}
     7
     8.wcpt-schedule td {
     9        vertical-align: top;
     10}
     11
     12.wcpt-schedule div.wcb-session-favourite-icon {
     13        float: right;
     14        width: 35px;
     15        text-align: center;
     16}
     17
     18.wcpt-schedule .dashicons {
     19        position: relative;
     20        box-sizing: content-box;
     21        width: 25px;
     22        height: 25px;
     23        overflow: hidden;
     24        white-space: nowrap;
     25        font-size: 16px;
     26        line-height: 1;
     27        cursor: pointer;
     28}
     29
     30.wcpt-schedule .dashicons:before {
     31        margin-right: 0px;
     32}
     33
     34.wcpt-schedule .dashicons:after {
     35        display: block;
     36        font-size: 9px;
     37        color: #999;
     38        text-align: right;
     39}
     40
     41
     42div.wcb-session-favourite-icon a.fav-session-button {
     43        color: #e8e8e8;
     44}
     45
     46
     47div.wcb-session-favourite-icon a.fav-session-button:hover {
     48        text-decoration: none;
     49        color: #fff689;
     50}
     51
     52
     53td.wcb-favourite-session a.fav-session-button {
     54        color: #fff689;
     55}
     56
     57.fav-session-email-wait-spinner {
     58        display: none;
     59        border: 2px solid #f3f3f3;
     60        border-radius: 50%;
     61        border-top: 2px solid #777;
     62        width: 16px;
     63        height: 16px;
     64        margin: 10px auto;
     65        -webkit-animation: spin 2s linear infinite;
     66        animation: spin 2s linear infinite;
     67}
     68
     69@-webkit-keyframes spin {
     70        0% { -webkit-transform: rotate(0deg); }
     71        100% { -webkit-transform: rotate(360deg); }
     72}
     73
     74@keyframes spin {
     75        0% { transform: rotate(0deg); }
     76        100% { transform: rotate(360deg); }
     77}
     78
     79
     80/*
     81 * CSS slide for email form for favourite sessions
     82 */
     83
     84.fav-session-email-form-hide {
     85        overflow: hidden;
     86        max-height: 0;
     87        padding-top: 0;
     88        padding-bottom: 0;
     89        margin-top: 0;
     90        margin-bottom: 0;
     91        -moz-transition-duration: 0.5s;
     92        -webkit-transition-duration: 0.5s;
     93        -o-transition-duration: 0.5s;
     94        transition-duration: 0.5s;
     95        -moz-transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
     96        -webkit-transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
     97        -o-transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
     98        transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
     99}
     100.fav-session-email-form-show {
     101        -moz-transition-duration: 0.5s;
     102        -webkit-transition-duration: 0.5s;
     103        -o-transition-duration: 0.5s;
     104        transition-duration: 0.5s;
     105        -moz-transition-timing-function: ease-in;
     106        -webkit-transition-timing-function: ease-in;
     107        -o-transition-timing-function: ease-in;
     108        transition-timing-function: ease-in;
     109        max-height: 1000px;
     110        overflow: hidden;
     111}
     112
     113
     114.show-email-form {
     115        display: none;
     116        position: fixed;
     117        bottom: 20px;
     118        right: 100px;
     119        padding: 5px 8px 1px 7px;
     120        border: 1px solid #a1a1a1;
     121        background: #dddddd;
     122        border-radius: 6px;
     123        color: #a1a1a1;
     124}
     125
     126.email-form {
     127        position: fixed;
     128        bottom: 50px;
     129        right: 100px;
     130        width: 200px;
     131        background: #dcdcdc;
     132        font-size: 12px;
     133}
     134
     135#fav-session-email-form {
     136        margin: 10px;
     137}
     138
     139.fav-session-email-result {
     140        display: none;
     141        margin: 10px;
     142}
     143
     144.fav-sessions-form {
     145        display: none;
     146}
     147
     148.entry-content a.show-email-form {
     149        color: #a1a1a1;
     150}
     151
     152.entry-content a.show-email-form:hover {
     153        color: #555;
     154        text-decoration: none;
     155}
     156
     157.show-email-form .dashicons-star-filled {
     158        font-size: 14px;
     159        padding-top: 2px;
     160        width: 14px;
     161}
     162
    4163@media screen and ( max-width: 700px ) {
    5164        .wcpt-schedule {
    6165                border: none;
     
    72231        span.wcpt-session-speakers a {
    73232                color: #21759b;
    74233        }
     234
     235        .show-email-form,
     236        .email-form {
     237                right: 20px;
     238        }
     239
    75240}
    76241
     242
     243
    77244/*
    78245 * [sessions]
    79246 * [speakers]
  • new file wordcamp.org/public_html/wp-content/plugins/wc-post-types/inc/favorite-schedule-shortcode.php

    diff --git wordcamp.org/public_html/wp-content/plugins/wc-post-types/inc/favorite-schedule-shortcode.php wordcamp.org/public_html/wp-content/plugins/wc-post-types/inc/favorite-schedule-shortcode.php
    new file mode 100644
    index 00000000..5ab69277
    - +  
     1<?php
     2/**
     3 * [schedule] shortcode building blocks and favourite session picker support.
     4 *
     5 * @package    wc-post-types
     6 * @subpackage wc-post-types/inc
     7 */
     8
     9// If this file is called directly, abort.
     10defined( 'WPINC' ) || die();
     11
     12/**
     13 * Retrun an associative array of term_id -> term object mapping for all selected tracks.
     14 *
     15 * In case of 'all' is used as a value for $selected_tracks, information for all available tracks
     16 * gets returned.
     17 *
     18 * @param string $selected_tracks Comma-separated list of tracks to display or 'all'.
     19 * @return array                  Associative array of terms with term_id as the key.
     20 */
     21function get_schedule_tracks( $selected_tracks ) {
     22        $tracks = array();
     23        if ( 'all' === $selected_tracks ) {
     24                // Include all tracks.
     25                $tracks = get_terms( 'wcb_track' );
     26
     27        } else {
     28                // Loop through given tracks and look for terms.
     29                $terms = array_map( 'trim', explode( ',', $selected_tracks ) );
     30                foreach ( $terms as $term_slug ) {
     31                        $term = get_term_by( 'slug', $term_slug, 'wcb_track' );
     32                        if ( $term ) {
     33                                $tracks[ $term->term_id ] = $term;
     34                        }
     35                }
     36        }
     37        return $tracks;
     38}
     39
     40/**
     41 * Return a time-sorted associative array mapping timestamp -> track_id -> session id.
     42 *
     43 * @param string $schedule_date               Date for which the sessions should be retrieved.
     44 * @param bool   $tracks_explicitly_specified True if tracks were explicitly specified in the shortcode,
     45 *                                            false otherwise.
     46 * @param array  $tracks                      Array of terms for tracks from get_schedule_tracks().
     47 * @return array                              Associative array of session ids by time and track.
     48 */
     49function get_schedule_sessions( $schedule_date, $tracks_explicitly_specified, $tracks ) {
     50        $query_args = array(
     51                'post_type'      => 'wcb_session',
     52                'posts_per_page' => -1,
     53                'meta_query'     => array(
     54                        'relation'   => 'AND',
     55                        array(
     56                                'key'     => '_wcpt_session_time',
     57                                'compare' => 'EXISTS',
     58                        ),
     59                ),
     60        );
     61
     62        if ( $schedule_date && strtotime( $schedule_date ) ) {
     63                $query_args['meta_query'][] = array(
     64                        'key'   => '_wcpt_session_time',
     65                        'value' => array(
     66                                strtotime( $schedule_date ),
     67                                strtotime( $schedule_date . ' +1 day' ),
     68                        ),
     69                        'compare' => 'BETWEEN',
     70                        'type'    => 'NUMERIC',
     71                );
     72        }
     73
     74        if ( $tracks_explicitly_specified ) {
     75                // If tracks were provided, restrict the lookup in WP_Query.
     76                if ( ! empty( $tracks ) ) {
     77                        $query_args['tax_query'][] = array(
     78                                'taxonomy' => 'wcb_track',
     79                                'field'    => 'id',
     80                                'terms'    => array_values( wp_list_pluck( $tracks, 'term_id' ) ),
     81                        );
     82                }
     83        }
     84
     85        // Loop through all sessions and assign them into the formatted
     86        // $sessions array: $sessions[ $time ][ $track ] = $session_id
     87        // Use 0 as the track ID if no tracks exist.
     88        $sessions = array();
     89        $sessions_query = new WP_Query( $query_args );
     90        foreach ( $sessions_query->posts as $session ) {
     91                $time = absint( get_post_meta( $session->ID, '_wcpt_session_time', true ) );
     92                $terms = get_the_terms( $session->ID, 'wcb_track' );
     93
     94                if ( ! isset( $sessions[ $time ] ) ) {
     95                        $sessions[ $time ] = array();
     96                }
     97
     98                if ( empty( $terms ) ) {
     99                        $sessions[ $time ][0] = $session->ID;
     100                } else {
     101                        foreach ( $terms as $track ) {
     102                                $sessions[ $time ][ $track->term_id ] = $session->ID;
     103                        }
     104                }
     105        }
     106
     107        // Sort all sessions by their key (timestamp).
     108        ksort( $sessions );
     109        return $sessions;
     110}
     111
     112/**
     113 * Return an array of columns identified by term ids to be used for schedule table.
     114 *
     115 * @param array $tracks                      Array of terms for tracks from get_schedule_tracks().
     116 * @param array $sessions                    Array of sessions from get_schedule_sessions().
     117 * @param array $tracks_explicitly_specified True if tracks were explicitly specified in the shortcode,
     118 *                                           false otherwise.
     119 * @return array                             Array of columns identified by term ids.
     120 */
     121function get_schedule_columns( $tracks, $sessions, $tracks_explicitly_specified ) {
     122        $columns = array();
     123
     124        // Use tracks to form the columns.
     125        if ( $tracks ) {
     126                foreach ( $tracks as $track ) {
     127                        $columns[ $track->term_id ] = $track->term_id;
     128                }
     129        } else {
     130                $columns[0] = 0;
     131        }
     132
     133        // Remove empty columns unless tracks have been explicitly specified.
     134        if ( ! $tracks_explicitly_specified ) {
     135                $used_terms = array();
     136
     137                foreach ( $sessions as $time => $entry ) {
     138                        if ( is_array( $entry ) ) {
     139                                foreach ( $entry as $term_id => $session_id ) {
     140                                        $used_terms[ $term_id ] = $term_id;
     141                                }
     142                        }
     143                }
     144                $columns = array_intersect( $columns, $used_terms );
     145                unset( $used_terms );
     146        }
     147        return $columns;
     148}
     149
     150/**
     151 * Update and preprocess input attributes for [schedule] shortcode.
     152 *
     153 * @param array $attr Array of attributes from shortcode.
     154 * @return array      Array of attributes, after preprocessing.
     155 */
     156function preprocess_schedule_attributes( $attr ) {
     157        $attr = shortcode_atts(
     158                array(
     159                        'date'         => null,
     160                        'tracks'       => 'all',
     161                        'speaker_link' => 'anchor',    // anchor|wporg|permalink|none
     162                        'session_link' => 'permalink', // permalink|anchor|none
     163                ), $attr
     164        );
     165
     166        foreach ( array( 'tracks', 'speaker_link', 'session_link' ) as $key_for_case_sensitive_value ) {
     167                $attr[ $key_for_case_sensitive_value ] = strtolower( $attr[ $key_for_case_sensitive_value ] );
     168        }
     169
     170        if ( ! in_array( $attr['speaker_link'], array( 'anchor', 'wporg', 'permalink', 'none' ), true ) ) {
     171                $attr['speaker_link'] = 'anchor';
     172        }
     173
     174        if ( ! in_array( $attr['session_link'], array( 'permalink', 'anchor', 'none' ), true ) ) {
     175                $attr['session_link'] = 'permalink';
     176        }
     177
     178        return $attr;
     179}
     180
     181/**
     182 * Return plain text list of sessions marked as favourite sessions.
     183 *
     184 * Format of each list item:
     185 * Time of session | Session title [by Speaker] | Track name(s).
     186 *
     187 * @param array $sessions_rev        Array of sessions with reversed subarray track_id->session_id.
     188 * @param array $fav_sessions_lookup Mapping session _id -> 1 for favourite sessions.
     189 * @return string                    List of sessions.
     190 */
     191function generate_plaintext_fav_sessions( $sessions_rev, $fav_sessions_lookup ) {
     192
     193        $sessions_text = '';
     194
     195        // timestamp -> session_id -> track_id.
     196        foreach ( $sessions_rev as $timestamp => $sessions_at_time ) {
     197                foreach ( $sessions_at_time as $session_id => $track_ids ) {
     198                        // Skip sessions which are not marked favourite.
     199                        if ( ! isset( $fav_sessions_lookup[ $session_id ] ) ) {
     200                                continue;
     201                        }
     202
     203                        $session              = get_post( $session_id );
     204                        $session_title        = apply_filters( 'the_title', $session->post_title );
     205                        $session_tracks       = get_the_terms( $session_id, 'wcb_track' );
     206                        $session_track_titles = is_array( $session_tracks ) ? implode( ', ', wp_list_pluck( $session_tracks, 'name' ) ) : '';
     207
     208                        $speakers = array();
     209                        $speakers_ids = array_map( 'absint', (array) get_post_meta( $session_id, '_wcpt_speaker_id' ) );
     210                        if ( ! empty( $speakers_ids ) ) {
     211                                $speakers = get_posts( array(
     212                                        'post_type'      => 'wcb_speaker',
     213                                        'posts_per_page' => -1,
     214                                        'post__in'       => $speakers_ids,
     215                                ) );
     216                        }
     217
     218                        $speakers_names = array();
     219                        foreach ( $speakers as $speaker ) {
     220                                $speaker_name = apply_filters( 'the_title', $speaker->post_title );
     221                                $speakers_names[] = $speaker_name;
     222                        }
     223
     224                        // Line format: Time of session | Session title [by Speaker] | Track name(s).
     225                        $sessions_text .= date( get_option( 'time_format' ), $timestamp );
     226                        $sessions_text .= ' | ';
     227                        $sessions_text .= $session_title;
     228                        if ( count( $speakers_names ) > 0 ) {
     229                                $sessions_text .= _x( ' by ', 'Speaker for the session', 'wordcamporg' ) . implode( ', ', $speakers_names );
     230                        }
     231                        $sessions_text .= ' | ';
     232                        $sessions_text .= $session_track_titles;
     233                        $sessions_text .= "\n";
     234                }
     235        }
     236        return $sessions_text;
     237
     238}
     239
     240/**
     241 * Return array of dates for which there are sessions in the provided array.
     242 *
     243 * @param array  $sessions    Array of sessions from get_schedule_sessions().
     244 * @param string $date_format Date format string (same format as php).
     245 * @return array              Array of dates for WordCamp formatted according to date_format string.
     246 */
     247function get_sessions_dates( $sessions, $date_format ) {
     248        $session_timestamps = array_keys( $sessions );
     249
     250        $session_dates = array_map(
     251                function( $timestamp ) use ( $date_format ) {
     252                        return date( $date_format, $timestamp );
     253                },
     254                $session_timestamps
     255        );
     256
     257        return array_unique( $session_dates );
     258}
     259
     260/**
     261 * Return true if any of the sessions from $session_rev is in $fav_session_ids,
     262 * false otherwise.
     263 *
     264 * @param array  $fav_session_ids Array with favourite sessions as keys.
     265 * @param string $sessions_rev    Array of sessions from flip_sessions_subarrays().
     266 * @return bool                   true if there is any intersection, false otherwise.
     267 */
     268function includes_fav_session( $fav_session_ids, $sessions_rev ) {
     269        foreach ( $sessions_rev as $timestamp => $session_id_subarray ) {
     270                foreach ( $session_id_subarray as $session_id => $_ ) {
     271                        if ( isset( $fav_session_ids[ $session_id ] ) ) {
     272                                return true;
     273                        }
     274                }
     275        }
     276        return false;
     277}
     278
     279/**
     280 * Return array of sessions with reverted subarrays, i.e. transformed from
     281 * timestamp -> track_id -> session_id into
     282 * timestamp -> session_id -> [track_id1, track_id2, ...]
     283 *
     284 * @param array $sessions An array of sessions from get_schedule_sessions().
     285 * @return array          Array with format timestamp -> session_id -> [track_id1, track_id2, ...].
     286 */
     287function flip_sessions_subarrays( $sessions ) {
     288        $sessions_reversed = array();
     289        foreach ( $sessions as $timestamp => $sessions_at_time ) {
     290                foreach ( $sessions_at_time as $track_id => $session_id ) {
     291                        $sessions_reversed[ $timestamp ][ $session_id ][] = $track_id;
     292                }
     293        }
     294        return $sessions_reversed;
     295}
     296
     297/**
     298 * Return plain text email message body for sharing favourite sessions email.
     299 *
     300 * @param string $wordcamp_name       WordCamp name to be used in the email.
     301 * @param array  $fav_sessions_lookup Mapping session _id -> 1 for favourite sessions.
     302 * @return string                     Plain text body of the email.
     303 */
     304function generate_email_body( $wordcamp_name, $fav_sessions_lookup ) {
     305        $date_format = get_option( 'date_format' );
     306        $tracks                      = get_schedule_tracks( 'all' );
     307        $tracks_explicitly_specified = false; // include all tracks in the email.
     308        $sessions                    = get_schedule_sessions( null, $tracks_explicitly_specified, $tracks );
     309        $sessions_dates              = get_sessions_dates( $sessions, $date_format );
     310
     311        // Convert timestamp -> track_id -> session_id to timestamp -> session_id -> [track_id1, ...].
     312        $sessions_reversed = flip_sessions_subarrays( $sessions );
     313
     314        $email_message = $wordcamp_name . "\n\n";
     315
     316        // Create list of sessions for each day.
     317        foreach ( $sessions_dates as $current_day ) {
     318                // Filter only the sessions for the 'current' day.
     319                $sessions_for_current_day = array_filter(
     320                        $sessions_reversed,
     321                        function( $date_ ) use ( $current_day, $date_format ) {
     322                                return date( $date_format, $date_ ) === $current_day;
     323                        },
     324                        ARRAY_FILTER_USE_KEY
     325                );
     326                $email_message .= $current_day . "\n";
     327                // Skip days when there's no session marked as favourite.
     328                if ( ! includes_fav_session( $fav_sessions_lookup, $sessions_for_current_day ) ) {
     329                        $email_message .= "\n";
     330                        continue;
     331                }
     332
     333                $email_message .= generate_plaintext_fav_sessions( $sessions_for_current_day, $fav_sessions_lookup );
     334                $email_message .= "\n\n";
     335        }
     336        return $email_message;
     337}
     338
     339/**
     340 * Return true if the email favourite sessions feature should be disabled,
     341 * false otherwise.
     342 *
     343 * Kill switch for sharing schedule over email -- both for REST API endpoint and UI
     344 * in [schedule] shortcode.
     345 * Could be linked to an option in the admin GUI later?
     346 *
     347 * @return bool true if email functionality should be disabled, false otherwise.
     348 */
     349function email_fav_sessions_disabled() {
     350        return false;
     351}
     352
     353/**
     354 * Send favourite sessions email to address specified in the REST request.
     355 *
     356 * REST API handler for 'wc-post-types/v1/email-fav-sessions' endpoint.
     357 *
     358 * @param WP_REST_Request $request REST API Request object.
     359 * @return WP_REST_Response        REST Response object.
     360 */
     361function send_favourite_sessions_email( WP_REST_Request $request ) {
     362        if ( email_fav_sessions_disabled() ) {
     363                return new WP_REST_Response(
     364                        array(
     365                                'message' => esc_html__( 'Email functionality disabled.', 'wordcamporg' ),
     366                        ), 200
     367                );
     368        }
     369
     370        $params = $request->get_params();
     371        // Input sanitized by REST controller.
     372        $email_address = $params['email-address'];
     373        $fav_sessions = $params['session-list'];
     374
     375        // Don't send the email if no sessions were marked as favourite.
     376        if ( count( explode( ',', $fav_sessions ) ) === 0 ) {
     377                return new WP_Error(
     378                        'fav_sessions_no_sessions',
     379                        esc_html__( 'No sessions selected.', 'wordcamporg' ),
     380                        array(
     381                                'status' => 400,
     382                        )
     383                );
     384        }
     385
     386        $fav_sessions_lookup = array_fill_keys( explode( ',', $fav_sessions ), 1 );
     387
     388        $wordcamp_name = get_wordcamp_name();
     389
     390        $headers[] = 'From: ' . $wordcamp_name . ' <dontreply@wordcamp.org>';
     391        $headers[] = 'Content-Type: text/plain; charset=' . get_bloginfo( 'charset' );
     392
     393        $subject = sprintf( __( 'My favourite sessions for %s' , 'wordcamporg' ), $wordcamp_name );
     394        $message = generate_email_body( $wordcamp_name, $fav_sessions_lookup );
     395
     396        if ( wp_mail( $email_address, $subject, $message, $headers ) ) {
     397                return new WP_REST_Response(
     398                        array(
     399                                'message' => esc_html__( 'Email sent successfully to ', 'wordcamporg' ) . " $email_address.",
     400                        ), 200
     401                );
     402        }
     403
     404        // Email was not sent successfully.
     405        return new WP_Error(
     406                'fav_sessions_email_failed',
     407                esc_html__( 'Favourite sessions email failed.', 'wordcamporg' ),
     408                array(
     409                        'status' => 500,
     410                )
     411        );
     412}
  • wordcamp.org/public_html/wp-content/plugins/wc-post-types/inc/rest-api.php

    diff --git wordcamp.org/public_html/wp-content/plugins/wc-post-types/inc/rest-api.php wordcamp.org/public_html/wp-content/plugins/wc-post-types/inc/rest-api.php
    index 107c776a..bd59b9aa 100644
     
    66 */
    77
    88namespace WordCamp\Post_Types\REST_API;
     9use WP_Rest_Server;
     10
    911defined( 'WPINC' ) || die();
    1012
     13require_once( 'favorite-schedule-shortcode.php' );
     14
    1115/**
    1216 * Add non-sensitive meta fields to the speaker/session REST API endpoints
    1317 *
    function register_additional_rest_fields() { 
    111115        } // End if().
    112116}
    113117
     118
    114119add_action( 'rest_api_init', __NAMESPACE__ . '\register_additional_rest_fields' );
    115120
     121
     122/**
     123 * Register route for sending schedule of favourite sessions via e-mail.
     124 *
     125 * This can be disabled in email_fav_sessions_disabled() from favorite-schedule-shortcode.php.
     126 *
     127 * @return void
     128 */
     129function register_fav_sessions_email(){
     130        register_rest_route(
     131                'wc-post-types/v1',     // REST namespace + API version
     132                '/email-fav-sessions/', // URL slug
     133                array(
     134                        'methods'   => WP_REST_Server::CREATABLE,
     135                        'callback'  => 'send_favourite_sessions_email',
     136                        'args'      => array(
     137                                'email-address' => array(
     138                                        'required'          => true,
     139                                        'validate_callback' => function( $value, $request, $param ) {
     140                                                return is_email( $value );
     141                                        },
     142                                        'sanitize_callback' => function( $value, $request, $param ) {
     143                                                return sanitize_email( $value );
     144                                        },
     145                                ),
     146                                'session-list' => array(
     147                                        'required'          => true,
     148                                        'validate_callback' => function( $value, $request, $param ) {
     149                                                $session_ids = explode( ',', $value );
     150                                                $session_count = count( $session_ids );
     151                                                for ( $i = 0; $i < $session_count; $i++ ) {
     152                                                        if ( ! is_numeric( $session_ids[ $i ] ) ) {
     153                                                                return false;
     154                                                        }
     155                                                }
     156                                                return true;
     157                                        },
     158                                        'sanitize_callback' => function( $value, $request, $param ) {
     159                                                $session_ids = explode( ',', $value );
     160                                                return implode( ',', array_filter( $session_ids, 'is_numeric' ) );
     161                                        },
     162                                ),
     163                        )
     164                )
     165        );
     166}
     167add_action( 'rest_api_init', __NAMESPACE__ . '\register_fav_sessions_email' );
     168
    116169/**
    117170 * Link all sessions to the speaker in the `speakers` API endpoint
    118171 *
  • new file wordcamp.org/public_html/wp-content/plugins/wc-post-types/js/favourite-sessions.js

    diff --git wordcamp.org/public_html/wp-content/plugins/wc-post-types/js/favourite-sessions.js wordcamp.org/public_html/wp-content/plugins/wc-post-types/js/favourite-sessions.js
    new file mode 100644
    index 00000000..c763c746
    - +  
     1jQuery( document ).ready( function ($) {
     2        var FavSessions = {
     3                favSessKey: 'favourite_sessions',
     4
     5                get: function () {
     6                        var favSessions = JSON.parse( localStorage.getItem( this.favSessKey ) );
     7                        if ( ! favSessions ) {
     8                                favSessions = {};
     9                        }
     10                        return favSessions;
     11                },
     12
     13                toggleSession: function ( sessionId ) {
     14                        var favSessions = this.get();
     15                        if ( favSessions.hasOwnProperty( sessionId ) ) {
     16                                delete favSessions[sessionId];
     17                        } else {
     18                                favSessions[sessionId] = true;
     19                        }
     20                        localStorage.setItem( this.favSessKey, JSON.stringify( favSessions ) );
     21                },
     22        };
     23
     24        function switchCellAppearance( sessionId ) {
     25                // (Un)highlight schedule table cell in case a session is (un)marked as favourite.
     26                var sessionSelector = "[data-session-id='" + sessionId + "']";
     27                var tdElements = document.querySelectorAll( sessionSelector );
     28                for ( var i = 0; i < tdElements.length; i++ ) {
     29                        tdElements[i].classList.toggle( 'wcb-favourite-session' );
     30                }
     31        }
     32
     33        function switchEmailFavButton() {
     34                var favSessions = FavSessions.get();
     35                // Display email form only if there are any selected sessions.
     36                if ( Object.keys( favSessions ).length > 0 ) {
     37                        $( '.show-email-form' ).show();
     38                } else {
     39                        $( '.show-email-form' ).hide();
     40                }
     41
     42        }
     43
     44        function switchSessionFavourite( sessionId ) {
     45                // Update localStorage.
     46                FavSessions.toggleSession( sessionId );
     47                // Change table cell appearance.
     48                switchCellAppearance( sessionId );
     49                // Display/hide email button if necessary.
     50                switchEmailFavButton();
     51        }
     52
     53        function initFavouriteSessions() {
     54                var favSessions = FavSessions.get();
     55                // No favourite session--nothing to do.
     56                if (favSessions === {}) {
     57                        return;
     58                }
     59
     60                // Highlight favourite sessions in table.
     61                var sessionIds = Object.keys( favSessions );
     62                for ( var i = 0; i < sessionIds.length; i++ ) {
     63                        var sessionId = sessionIds[i];
     64                        if (favSessions[sessionId] === true) {
     65                                switchCellAppearance( sessionId );
     66                        }
     67                }
     68                // Display/hide email button if necessary.
     69                switchEmailFavButton();
     70        }
     71
     72        function hideSpinnerShowResult( message ) {
     73                var fadeInDelay = 300;
     74                $( '.fav-session-email-wait-spinner' ).fadeOut( fadeInDelay );
     75
     76                setTimeout(function () {
     77                        $( '.fav-session-email-result' ).html( message );
     78                        $( '.fav-session-email-result' ).fadeIn();
     79                }, fadeInDelay);
     80        }
     81       
     82        function hideFormShowSpinner() {
     83                var fadeInDelay = 300;
     84                $( '#fav-session-email-form' ).fadeOut( fadeInDelay );
     85                setTimeout( function () {
     86                        $( '.fav-session-email-wait-spinner' ).fadeIn();
     87                }, fadeInDelay);
     88        }
     89
     90        $( '.show-email-form' ).click( function ( event ) {
     91                event.preventDefault();
     92                // Slide the slider.
     93                $( '.email-form' ).toggleClass( 'fav-session-email-form-hide' ).toggleClass( 'fav-session-email-form-show' );
     94
     95                // After the animnation finishes, activate the form again and hide the previous result.
     96                setTimeout( function () {
     97                        // Clear previous email result.
     98                        $( '.fav-session-email-result' ).html( '' );
     99                        // Show form div & clear email address.
     100                        $( '#fav-session-email-form' ).show();
     101                        $( '#fav-sessions-email-address' ).val( '' );
     102
     103                }, 500);
     104
     105                return false;
     106        });
     107
     108        $( '.fav-session-button' ).click( function ( event ) {
     109                event.preventDefault();
     110                var elem = $( this );
     111                var sessionId = elem.parent().parent().data( 'session-id' );
     112                switchSessionFavourite( sessionId );
     113                return false;
     114        });
     115
     116        $( '#fav-sessions-form' ).on( 'submit', function ( event ) {
     117                event.preventDefault();
     118                hideFormShowSpinner();
     119                var favSessions = FavSessions.get();
     120                favSessions = Object.keys( favSessions ).toString();
     121
     122                // Get email from the input.
     123                var emailAddress = '';
     124                if ( $( '#fav-sessions-email-address' ) ) {
     125                        emailAddress = $( '#fav-sessions-email-address' ).val();
     126                } else {
     127                        return;
     128                }
     129
     130                // Compile data object.
     131                // The nonce is sent in a request header - see beforeSend in $.ajax.
     132                var data = {
     133                        'email-address': emailAddress,
     134                        'session-list': favSessions,
     135                };
     136
     137                $.ajax({
     138                        method: 'POST',
     139                        url: favSessionsPhpObject.root + 'wc-post-types/v1/email-fav-sessions',
     140                        data: data,
     141                        beforeSend: function ( xhr ) {
     142                                // Set the nonce header provided by php script.
     143                                xhr.setRequestHeader( 'X-WP-Nonce', favSessionsPhpObject.api_nonce );
     144                        },
     145                        success: function ( response ) {
     146                                hideSpinnerShowResult( response.message );
     147                        },
     148                        fail: function ( response ) {
     149                                hideSpinnerShowResult( response.message );
     150                        },
     151                        error: function(jqXHR, textStatus, errorThrown) {
     152                                if ( textStatus === 'timeout' ) {
     153                                        hideSpinnerShowResult( favSessionsPhpObject.i18n.reqTimeOut );
     154                                } else {
     155                                        hideSpinnerShowResult( favSessionsPhpObject.i18n.otherError );
     156                                }
     157                        },
     158                        timeout: 5000,
     159                });
     160        });
     161
     162        initFavouriteSessions();
     163});
  • wordcamp.org/public_html/wp-content/plugins/wc-post-types/wc-post-types.php

    diff --git wordcamp.org/public_html/wp-content/plugins/wc-post-types/wc-post-types.php wordcamp.org/public_html/wp-content/plugins/wc-post-types/wc-post-types.php
    index e99df783..bbe3601e 100644
     
    55 */
    66
    77require( 'inc/back-compat.php' );
     8require_once('inc/favorite-schedule-shortcode.php');
    89
    910class WordCamp_Post_Types_Plugin {
    1011        protected $wcpt_permalinks;
    class WordCamp_Post_Types_Plugin { 
    267268
    268269        function wp_enqueue_scripts() {
    269270                wp_enqueue_style( 'wcb_shortcodes', plugins_url( 'css/shortcodes.css', __FILE__ ), array(), 2 );
     271               
     272                // Style and script for [schedule] shortcode.
     273                wp_register_script(
     274                        'favourite-sessions',
     275                        plugin_dir_url( __FILE__ ) . 'js/favourite-sessions.js',
     276                        array( 'jquery' ),
     277                        filemtime( plugin_dir_path( __FILE__ ) . 'js/favourite-sessions.js' ),
     278                        true
     279                );
    270280        }
    271281
    272282        /**
    class WordCamp_Post_Types_Plugin { 
    502512        }
    503513
    504514        /**
    505          * The [schedule] shortcode callback (experimental)
     515         * Return HTML code for email form used to send/share favourite sessions over email.
    506516         *
    507          * @todo implement date arg
    508          * @todo implement anchor for session_link
    509          * @todo maybe simplify $attr['custom']
    510          * @todo cleanup
     517         * Both form and button/link to show/hide the form can be styled using classes email-form
     518         * and show-email-form, respectively.
     519         *
     520         * @return string HTML code that represents the form to send emails and a link to show and hide it.
    511521         */
    512         function shortcode_schedule( $attr, $content ) {
    513                 $attr = shortcode_atts( array(
    514                         'date'         => null,
    515                         'tracks'       => 'all',
    516                         'speaker_link' => 'anchor', // anchor|wporg|permalink|none
    517                         'session_link' => 'permalink', // permalink|anchor|none
    518                 ), $attr );
    519 
    520                 foreach ( array( 'tracks', 'speaker_link', 'session_link' ) as $key_for_case_sensitive_value ) {
    521                         $attr[ $key_for_case_sensitive_value ] = strtolower( $attr[ $key_for_case_sensitive_value ] );
     522        function fav_session_email_form() {
     523                static $email_form_count = 0;
     524                // Skip email form if it is disabled or it was already added to document.
     525                if ( email_fav_sessions_disabled() || $email_form_count !== 0 ) {
     526                        return '';
    522527                }
     528                $send_email_label  = esc_html__( 'Send me my favourite sessions:', 'wordcamporg' );
     529                $send_email_button = esc_attr__( 'Send', 'wordcamporg' );
     530                $email_form = <<<EOS
     531                <div class="email-form fav-session-email-form-hide" style="">
     532                        <div id="fav-session-email-form">
     533                                $send_email_label
     534                                <form id="fav-sessions-form">
     535                                        <input type="text" name="email_address" id="fav-sessions-email-address" placeholder="my@email.com" />
     536                                        <input type="submit" value="$send_email_button" />
     537                                </form>
     538                        </div>
     539                        <div class="fav-session-email-wait-spinner"></div>
     540                        <div class="fav-session-email-result"></div>
     541                </div>
     542                <a class="show-email-form" href="javascript:">
     543                        <span class="dashicons dashicons-star-filled"></span>
     544                        <span class="dashicons dashicons-email-alt"></span>
     545                </a>
     546EOS;
     547
     548                $email_form_count++;
     549                return $email_form;
     550        }
    523551
    524                 if ( ! in_array( $attr['speaker_link'], array( 'anchor', 'wporg', 'permalink', 'none' ) ) )
    525                         $attr['speaker_link'] = 'anchor';
    526 
    527                 if ( ! in_array( $attr['session_link'], array( 'permalink', 'anchor', 'none' ) ) )
    528                         $attr['session_link'] = 'permalink';
    529 
    530                 $columns = array();
    531                 $tracks = array();
    532 
    533                 $query_args = array(
    534                         'post_type'      => 'wcb_session',
    535                         'posts_per_page' => -1,
    536                         'meta_query'     => array(
    537                                 'relation'   => 'AND',
    538                                 array(
    539                                         'key'     => '_wcpt_session_time',
    540                                         'compare' => 'EXISTS',
    541                                 ),
    542                         ),
     552        /**
     553         * Enqueue style and scripts needed for [schedule] shortcode.
     554         *
     555         * @return void.
     556         */
     557        function enqueue_schedule_shortcode_dependencies() {
     558                wp_enqueue_style( 'dashicons' );
     559                wp_enqueue_script(
     560                        'favourite-sessions',
     561                        plugin_dir_url( __FILE__ ) . 'js/favourite-sessions.js',
     562                        array( 'jquery' ),
     563                        filemtime( plugin_dir_path( __FILE__ ) . 'js/favourite-sessions.js' ),
     564                        true
    543565                );
    544566
    545                 if ( 'all' == $attr['tracks'] ) {
    546                         // Include all tracks.
    547                         $tracks = get_terms( 'wcb_track' );
    548                 } else {
    549                         // Loop through given tracks and look for terms.
    550                         $terms = array_map( 'trim', explode( ',', $attr['tracks'] ) );
    551                         foreach ( $terms as $term_slug ) {
    552                                 $term = get_term_by( 'slug', $term_slug, 'wcb_track' );
    553                                 if ( $term )
    554                                         $tracks[ $term->term_id ] = $term;
    555                         }
    556 
    557                         // If tracks were provided, restrict the lookup in WP_Query.
    558                         if ( ! empty( $tracks ) ) {
    559                                 $query_args['tax_query'][] = array(
    560                                         'taxonomy' => 'wcb_track',
    561                                         'field'    => 'id',
    562                                         'terms'    => array_values( wp_list_pluck( $tracks, 'term_id' ) ),
    563                                 );
    564                         }
    565                 }
    566 
    567                 if ( $attr['date'] && strtotime( $attr['date'] ) ) {
    568                         $query_args['meta_query'][] = array(
    569                                 'key'   => '_wcpt_session_time',
    570                                 'value' => array(
    571                                         strtotime( $attr['date'] ),
    572                                         strtotime( $attr['date'] . ' +1 day' ),
     567                wp_localize_script(
     568                        'favourite-sessions',
     569                        'favSessionsPhpObject',
     570                        array(
     571                                'api_nonce' => wp_create_nonce( 'wp_rest' ),
     572                                'root' => esc_url_raw( rest_url() ),
     573                                'i18n' => array(
     574                                        'reqTimeOut' => esc_html__( 'Sorry, the email request timed out.', 'wordcamporg' ),
     575                                        'otherError' => esc_html__( 'Sorry, the email request failed.', 'wordcamporg' ),
    573576                                ),
    574                                 'compare' => 'BETWEEN',
    575                                 'type'    => 'NUMERIC',
    576                         );
    577                 }
    578 
    579                 // Use tracks to form the columns.
    580                 if ( $tracks ) {
    581                         foreach ( $tracks as $track )
    582                                 $columns[ $track->term_id ] = $track->term_id;
    583                 } else {
    584                         $columns[ 0 ] = 0;
    585                 }
    586 
    587                 unset( $tracks );
    588 
    589                 // Loop through all sessions and assign them into the formatted
    590                 // $sessions array: $sessions[ $time ][ $track ] = $session_id
    591                 // Use 0 as the track ID if no tracks exist
    592 
    593                 $sessions = array();
    594                 $sessions_query = new WP_Query( $query_args );
    595                 foreach ( $sessions_query->posts as $session ) {
    596                         $time = absint( get_post_meta( $session->ID, '_wcpt_session_time', true ) );
    597                         $tracks = get_the_terms( $session->ID, 'wcb_track' );
    598 
    599                         if ( ! isset( $sessions[ $time ] ) )
    600                                 $sessions[ $time ] = array();
    601 
    602                         if ( empty( $tracks ) ) {
    603                                 $sessions[ $time ][ 0 ] = $session->ID;
    604                         } else {
    605                                 foreach ( $tracks as $track )
    606                                         $sessions[ $time ][ $track->term_id ] = $session->ID;
    607                         }
    608                 }
    609 
    610                 // Sort all sessions by their key (timestamp).
    611                 ksort( $sessions );
    612 
    613                 // Remove empty columns unless tracks have been explicitly specified
    614                 if ( 'all' == $attr['tracks'] ) {
    615                         $used_terms = array();
     577                        )
     578                );
     579        }
    616580
    617                         foreach ( $sessions as $time => $entry )
    618                                 if ( is_array( $entry ) )
    619                                         foreach ( $entry as $term_id => $session_id )
    620                                                 $used_terms[ $term_id ] = $term_id;
     581        /**
     582         * The [schedule] shortcode callback (experimental)
     583         *
     584         * @todo implement date arg
     585         * @todo implement anchor for session_link
     586         * @todo maybe simplify $attr['custom']
     587         * @todo cleanup
     588         */
     589        function shortcode_schedule( $attr, $content ) {
     590                $this->enqueue_schedule_shortcode_dependencies();
    621591
    622                         $columns = array_intersect( $columns, $used_terms );
    623                         unset( $used_terms );
    624                 }
     592                $attr                        = preprocess_schedule_attributes( $attr );
     593                $tracks                      = get_schedule_tracks( $attr['tracks'] );
     594                $tracks_explicitly_specified = 'all' !== $attr['tracks'];
     595                $sessions                    = get_schedule_sessions( $attr['date'], $tracks_explicitly_specified, $tracks );
     596                $columns                     = get_schedule_columns( $tracks, $sessions, $tracks_explicitly_specified );
    625597
    626                 $html = '<table class="wcpt-schedule" border="0">';
     598                $html  = '<table class="wcpt-schedule" border="0">';
    627599                $html .= '<thead>';
    628600                $html .= '<tr>';
    629601
    class WordCamp_Post_Types_Plugin { 
    707679                                $classes[] = 'wcpt-session-type-' . $session_type;
    708680                                $classes[] = 'wcb-session-' . $session->post_name;
    709681
     682                                // Favourite session star-icon.
     683                                $content = '<div class="wcb-session-favourite-icon">';
     684                                $content .= '<a class="fav-session-button"><span class="dashicons dashicons-star-filled"></span></a></div>';
     685                                $content .= '<div class="wcb-session-cell-content">';
     686
     687
    710688                                // Determine the session title
    711689                                if ( 'permalink' == $attr['session_link'] && 'session' == $session_type )
    712690                                        $session_title_html = sprintf( '<a class="wcpt-session-title" href="%s">%s</a>', esc_url( get_permalink( $session->ID ) ), $session_title );
    class WordCamp_Post_Types_Plugin { 
    715693                                else
    716694                                        $session_title_html = sprintf( '<span class="wcpt-session-title">%s</span>', $session_title );
    717695
    718                                 $content = $session_title_html;
     696                                $content .= $session_title_html;
    719697
    720698                                $speakers_names = array();
    721699                                foreach ( $speakers as $speaker ) {
    class WordCamp_Post_Types_Plugin { 
    738716                                if ( count( $speakers_names ) )
    739717                                        $content .= sprintf( ' <span class="wcpt-session-speakers">%s</span>', implode( ', ', $speakers_names ) );
    740718
     719                                // End of cell-content.
     720                                $content .= '</div>';
     721
    741722                                $columns_clone = $columns;
    742723
    743724                                // If the next element in the table is the same as the current one, use colspan
    class WordCamp_Post_Types_Plugin { 
    755736                                        }
    756737                                }
    757738
    758                                 $columns_html .= sprintf( '<td colspan="%d" class="%s" data-track-title="%s">%s</td>', $colspan, esc_attr( implode( ' ', $classes ) ), $session_track_titles, $content );
     739                                $columns_html .= sprintf( '<td colspan="%d" class="%s" data-track-title="%s" data-session-id="%s">%s</td>', $colspan, esc_attr( implode( ' ', $classes ) ), $session_track_titles, esc_attr( $session->ID ), $content );
    759740                        }
    760741
    761742                        $global_session      = $colspan == count( $columns ) ? ' global-session' : '';
    class WordCamp_Post_Types_Plugin { 
    769750
    770751                $html .= '</tbody>';
    771752                $html .= '</table>';
     753                $html .= $this->fav_session_email_form();
    772754                return $html;
    773755        }
    774756