<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[40607] trunk: Dashboard: Update the existing WordPress News dashboard widget to also include upcoming meetup events and WordCamps near the current user?\226?\128?\153s location.</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta" style="font-size: 105%">
<dt style="float: left; width: 6em; font-weight: bold">Revision</dt> <dd><a style="font-weight: bold" href="https://core.trac.wordpress.org/changeset/40607">40607</a><script type="application/ld+json">{"@context":"http://schema.org","@type":"EmailMessage","description":"Review this Commit","action":{"@type":"ViewAction","url":"https://core.trac.wordpress.org/changeset/40607","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>azaozz</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2017-05-10 20:03:01 +0000 (Wed, 10 May 2017)</dd>
</dl>

<pre style='padding-left: 1em; margin: 2em 0; border-left: 2px solid #ccc; line-height: 1.25; font-size: 105%; font-family: sans-serif'>Dashboard: Update the existing WordPress News dashboard widget to also include upcoming meetup events and WordCamps near the current user?\226?\128?\153s location.

Props @afercia, @andreamiddleton, @azaozz, @camikaos, @coreymckrill, @chanthaboune, @courtneypk, @dd32, @iandunn, @iseulde, @mapk, @mayukojpn, @melchoyce, @nao, @obenland, @pento, @samuelsidler, @stephdau, @tellyworth.
See <a href="https://core.trac.wordpress.org/ticket/40702">#40702</a>.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunksrcwpadminadminajaxphp">trunk/src/wp-admin/admin-ajax.php</a></li>
<li><a href="#trunksrcwpadmincssdashboardcss">trunk/src/wp-admin/css/dashboard.css</a></li>
<li><a href="#trunksrcwpadminincludesajaxactionsphp">trunk/src/wp-admin/includes/ajax-actions.php</a></li>
<li><a href="#trunksrcwpadminincludesdashboardphp">trunk/src/wp-admin/includes/dashboard.php</a></li>
<li><a href="#trunksrcwpadminincludesdeprecatedphp">trunk/src/wp-admin/includes/deprecated.php</a></li>
<li><a href="#trunksrcwpadminincludesupgradephp">trunk/src/wp-admin/includes/upgrade.php</a></li>
<li><a href="#trunksrcwpadminindexphp">trunk/src/wp-admin/index.php</a></li>
<li><a href="#trunksrcwpadminjsdashboardjs">trunk/src/wp-admin/js/dashboard.js</a></li>
<li><a href="#trunksrcwpadminnetworkindexphp">trunk/src/wp-admin/network/index.php</a></li>
<li><a href="#trunksrcwpincludesscriptloaderphp">trunk/src/wp-includes/script-loader.php</a></li>
</ul>

<h3>Added Paths</h3>
<ul>
<li><a href="#trunksrcwpadminincludesclasswpcommunityeventsphp">trunk/src/wp-admin/includes/class-wp-community-events.php</a></li>
<li><a href="#trunktestsphpunittestsadminincludesCommunityEventsphp">trunk/tests/phpunit/tests/admin/includesCommunityEvents.php</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunksrcwpadminadminajaxphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-admin/admin-ajax.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-admin/admin-ajax.php 2017-05-10 19:40:56 UTC (rev 40606)
+++ trunk/src/wp-admin/admin-ajax.php   2017-05-10 20:03:01 UTC (rev 40607)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -64,7 +64,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">        'parse-media-shortcode', 'destroy-sessions', 'install-plugin', 'update-plugin', 'press-this-save-post',
</span><span class="cx" style="display: block; padding: 0 10px">        'press-this-add-category', 'crop-image', 'generate-password', 'save-wporg-username', 'delete-plugin',
</span><span class="cx" style="display: block; padding: 0 10px">        'search-plugins', 'search-install-plugins', 'activate-plugin', 'update-theme', 'delete-theme',
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        'install-theme', 'get-post-thumbnail-html',
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ 'install-theme', 'get-post-thumbnail-html', 'get-community-events',
</ins><span class="cx" style="display: block; padding: 0 10px"> );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> // Deprecated
</span></span></pre></div>
<a id="trunksrcwpadmincssdashboardcss"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-admin/css/dashboard.css</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-admin/css/dashboard.css      2017-05-10 19:40:56 UTC (rev 40606)
+++ trunk/src/wp-admin/css/dashboard.css        2017-05-10 20:03:01 UTC (rev 40607)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -301,6 +301,145 @@
</span><span class="cx" style="display: block; padding: 0 10px">        content: "\f153";
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+/* Dashboard WordPress events */
+
+.community-events-errors {
+       margin: 0;
+}
+
+.community-events-loading {
+       padding: 10px 12px 8px;
+}
+
+.community-events {
+       margin-bottom: 6px;
+       padding: 0 12px;
+}
+
+.community-events .spinner {
+       float: none;
+       margin: 0;
+       padding-bottom: 3px;
+}
+
+.community-events-errors[aria-hidden="true"],
+.community-events-errors *[aria-hidden="true"],
+.community-events-loading[aria-hidden="true"],
+.community-events[aria-hidden="true"],
+.community-events *[aria-hidden="true"] {
+       display: none;
+}
+
+.community-events .activity-block:first-child,
+.community-events h2 {
+       padding-top: 12px;
+       padding-bottom: 10px;
+}
+
+.community-events-form {
+       margin: 15px 0 5px;
+}
+
+.community-events-form .regular-text {
+       width: 40%;
+       height: 28px;
+}
+
+.community-events li.event-none {
+       border-left: 4px solid #0070AE;
+}
+
+.community-events-form label {
+       display: inline-block;
+       padding-bottom: 3px;
+}
+
+.community-events .activity-block > p {
+       margin-bottom: 0;
+       display: inline;
+}
+
+#community-events-submit {
+       margin-left: 2px;
+}
+
+.community-events .button-link:hover,
+.community-events .button-link:active {
+       color: #00a0d2;
+}
+
+.community-events-cancel.button.button-link {
+       color: #0073aa;
+       text-decoration: underline;
+       margin-left: 2px;
+}
+
+.community-events ul {
+       background-color: #fafafa;
+       padding-left: 0;
+       padding-right: 0;
+       padding-bottom: 0;
+}
+
+.community-events li {
+       margin: 0;
+       padding: 8px 12px;
+       color: #72777c;
+}
+.community-events li:first-child {
+       border-top: 1px solid #eee;
+}
+
+.community-events li ~ li {
+       border-top: 1px solid #eee;
+}
+
+.community-events .activity-block.last {
+       border-bottom: 1px solid #eee;
+       padding-top: 0;
+       margin-top: -1px;
+}
+
+.community-events .event-info {
+       display: block;
+}
+
+.event-icon {
+       height: 18px;
+       padding-right: 10px;
+       width: 18px;
+       display: none; /* Hide on smaller screens */
+}
+
+.event-icon:before {
+       color: #82878C;
+       font-size: 18px;
+}
+.event-meetup .event-icon:before {
+       content: "\f484";
+}
+.event-wordcamp .event-icon:before {
+       content: "\f486";
+}
+
+.community-events .event-title {
+       font-weight: 600;
+       display: block;
+}
+
+.community-events .event-date,
+.community-events .event-time {
+       display: block;
+}
+
+.community-events-footer {
+       margin-top: 0;
+       margin-bottom: 0;
+       padding: 12px;
+       border-top: 1px solid #eee;
+       color: #ddd;
+}
+
</ins><span class="cx" style="display: block; padding: 0 10px"> /* Dashboard WordPress news */
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> #dashboard_primary .inside {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -333,9 +472,8 @@
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> #dashboard_primary .rss-widget {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        border-bottom: 1px solid #eee;
</del><span class="cx" style="display: block; padding: 0 10px">         font-size: 13px;
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        padding: 8px 12px 10px;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ padding: 0 12px 0;
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> #dashboard_primary .rss-widget:last-child {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -357,7 +495,8 @@
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> #dashboard_primary .rss-widget ul li {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        margin-bottom: 8px;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ padding: 4px 0;
+       margin: 0;
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> /* Dashboard right now */
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -874,9 +1013,9 @@
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> a.rsswidget {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        font-size: 14px;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ font-size: 13px;
</ins><span class="cx" style="display: block; padding: 0 10px">         font-weight: 600;
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        line-height: 1.7em;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ line-height: 1.4em;
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> .rss-widget ul li {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1087,6 +1226,14 @@
</span><span class="cx" style="display: block; padding: 0 10px">                width: 30px;
</span><span class="cx" style="display: block; padding: 0 10px">                margin: 4px 10px 5px 0;
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+       .community-events-toggle-location {
+               height: 38px;
+       }
+
+       .community-events-form .regular-text {
+               height: 31px;
+       }
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> /* Smartphone */
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1110,3 +1257,30 @@
</span><span class="cx" style="display: block; padding: 0 10px">                left: -35px;
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+@media screen and (min-width: 355px) {
+       .community-events .event-info {
+               display: table-row;
+               float: left;
+               max-width: 59%;
+       }
+
+       .event-icon,
+       .event-icon[aria-hidden="true"] {
+               display: table-cell;
+       }
+
+       .event-info-inner {
+               display: table-cell;
+       }
+
+       .community-events .event-date-time {
+               float: right;
+               max-width: 39%;
+       }
+
+       .community-events .event-date,
+       .community-events .event-time {
+               text-align: right;
+       }
+}
</ins></span></pre></div>
<a id="trunksrcwpadminincludesajaxactionsphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-admin/includes/ajax-actions.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-admin/includes/ajax-actions.php      2017-05-10 19:40:56 UTC (rev 40606)
+++ trunk/src/wp-admin/includes/ajax-actions.php        2017-05-10 20:03:01 UTC (rev 40607)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -297,6 +297,40 @@
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * Handles AJAX requests for community events
+ *
+ * @since 4.8.0
+ */
+function wp_ajax_get_community_events() {
+       require_once( ABSPATH . 'wp-admin/includes/class-wp-community-events.php' );
+
+       check_ajax_referer( 'community_events' );
+
+       $search         = isset( $_POST['location'] ) ? wp_unslash( $_POST['location'] ) : '';
+       $timezone       = isset( $_POST['timezone'] ) ? wp_unslash( $_POST['timezone'] ) : '';
+       $user_id        = get_current_user_id();
+       $saved_location = get_user_option( 'community-events-location', $user_id );
+       $events_client  = new WP_Community_Events( $user_id, $saved_location );
+       $events         = $events_client->get_events( $search, $timezone );
+
+       if ( is_wp_error( $events ) ) {
+               wp_send_json_error( array(
+                       'error' => $events->get_error_message(),
+               ) );
+       } else {
+               if ( isset( $events['location'] ) ) {
+                       // Send only the data that the client will use.
+                       $events['location'] = $events['location']['description'];
+
+                       // Store the location network-wide, so the user doesn't have to set it on each site.
+                       update_user_option( $user_id, 'community-events-location', $events['location'], true );
+               }
+
+               wp_send_json_success( $events );
+       }
+}
+
+/**
</ins><span class="cx" style="display: block; padding: 0 10px">  * Ajax handler for dashboard widgets.
</span><span class="cx" style="display: block; padding: 0 10px">  *
</span><span class="cx" style="display: block; padding: 0 10px">  * @since 3.4.0
</span></span></pre></div>
<a id="trunksrcwpadminincludesclasswpcommunityeventsphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/src/wp-admin/includes/class-wp-community-events.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-admin/includes/class-wp-community-events.php                         (rev 0)
+++ trunk/src/wp-admin/includes/class-wp-community-events.php   2017-05-10 20:03:01 UTC (rev 40607)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,419 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Administration: Community Events class.
+ *
+ * @package WordPress
+ * @subpackage Administration
+ * @since 4.8.0
+ */
+
+/**
+ * Class WP_Community_Events.
+ *
+ * A client for api.wordpress.org/events.
+ *
+ * @since 4.8.0
+ */
+class WP_Community_Events {
+       /**
+        * ID for a WordPress user account.
+        *
+        * @access protected
+        * @since 4.8.0
+        *
+        * @var int
+        */
+       protected $user_id = 0;
+
+       /**
+        * Stores location data for the user.
+        *
+        * @access protected
+        * @since 4.8.0
+        *
+        * @var bool|array
+        */
+       protected $user_location = false;
+
+       /**
+        * Constructor for WP_Community_Events.
+        *
+        * @since 4.8.0
+        *
+        * @param int        $user_id       WP user ID.
+        * @param bool|array $user_location Stored location data for the user.
+        *                                  false to pass no location;
+        *                                  array to pass a location {
+        *     @type string $description The name of the location
+        *     @type string $latitude    The latitude in decimal degrees notation, without the degree
+        *                               symbol. e.g.: 47.615200.
+        *     @type string $longitude   The longitude in decimal degrees notation, without the degree
+        *                               symbol. e.g.: -122.341100.
+        *     @type string $country     The ISO 3166-1 alpha-2 country code. e.g.: BR
+        * }
+        */
+       public function __construct( $user_id, $user_location = false ) {
+               $this->user_id       = absint( $user_id );
+               $this->user_location = $user_location;
+       }
+
+       /**
+        * Gets data about events near a particular location.
+        *
+        * Cached events will be immediately returned if the `user_location` property
+        * is set for the current user, and cached events exist for that location.
+        *
+        * Otherwise, this method sends a request to the w.org Events API with location
+        * data. The API will send back a recognized location based on the data, along
+        * with nearby events.
+        *
+        * @since 4.8.0
+        *
+        * @param string $location_search Optional city name to help determine the location.
+        *                                e.g., "Seattle". Default empty string.
+        * @param string $timezone        Optional timezone to help determine the location.
+        *                                Default empty string.
+        * @return array|WP_Error A WP_Error on failure; an array with location and events on
+        *                        success.
+        */
+       public function get_events( $location_search = '', $timezone = '' ) {
+               $cached_events = $this->get_cached_events();
+
+               if ( ! $location_search && $cached_events ) {
+                       return $cached_events;
+               }
+
+               $request_url    = $this->get_request_url( $location_search, $timezone );
+               $response       = wp_remote_get( $request_url );
+               $response_code  = wp_remote_retrieve_response_code( $response );
+               $response_body  = json_decode( wp_remote_retrieve_body( $response ), true );
+               $response_error = null;
+               $debugging_info = compact( 'request_url', 'response_code', 'response_body' );
+
+               if ( is_wp_error( $response ) ) {
+                       $response_error = $response;
+               } elseif ( 200 !== $response_code ) {
+                       $response_error = new WP_Error(
+                               'api-error',
+                               /* translators: %s is a numeric HTTP status code; e.g., 400, 403, 500, 504, etc. */
+                               sprintf( __( 'Invalid API response code (%d)' ), $response_code )
+                       );
+               } elseif ( ! isset( $response_body['location'], $response_body['events'] ) ) {
+                       $response_error = new WP_Error(
+                               'api-invalid-response',
+                               isset( $response_body['error'] ) ? $response_body['error'] : __( 'Unknown API error.' )
+                       );
+               }
+
+               if ( is_wp_error( $response_error ) ) {
+                       $this->maybe_log_events_response( $response_error->get_error_message(), $debugging_info );
+
+                       return $response_error;
+               } else {
+                       $expiration = false;
+
+                       if ( isset( $response_body['ttl'] ) ) {
+                               $expiration = $response_body['ttl'];
+                               unset( $response_body['ttl'] );
+                       }
+
+                       $this->cache_events( $response_body, $expiration );
+
+                       $response_body = $this->trim_events( $response_body );
+                       $response_body = $this->format_event_data_time( $response_body );
+
+                       // Avoid bloating the log with all the event data, but keep the count.
+                       $debugging_info['response_body']['events'] = count( $debugging_info['response_body']['events'] ) . ' events trimmed.';
+
+                       $this->maybe_log_events_response( 'Valid response received', $debugging_info );
+
+                       return $response_body;
+               }
+       }
+
+       /**
+        * Builds a URL for requests to the w.org Events API.
+        *
+        * @access protected
+        * @since 4.8.0
+        *
+        * @param  string $search   City search string. Default empty string.
+        * @param  string $timezone Timezone string. Default empty string.
+        * @return string The request URL.
+        */
+       protected function get_request_url( $search = '', $timezone = '' ) {
+               $api_url = 'https://api.wordpress.org/events/1.0/';
+               $args    = array( 'number' => 5 ); // Get more than three in case some get trimmed out.
+
+               /*
+                * Send the minimal set of necessary arguments, in order to increase the
+                * chances of a cache-hit on the API side.
+                */
+               if ( empty( $search ) && isset( $this->user_location['latitude'], $this->user_location['longitude'] ) ) {
+                       $args['latitude']  = $this->user_location['latitude'];
+                       $args['longitude'] = $this->user_location['longitude'];
+               } else {
+                       $args['locale'] = get_user_locale( $this->user_id );
+
+                       if ( $timezone ) {
+                               $args['timezone'] = $timezone;
+                       }
+
+                       if ( $search ) {
+                               $args['location'] = $search;
+                       } else {
+                               /*
+                                * Protect the user's privacy by anonymizing their IP before sending
+                                * it to w.org, and only send it when necessary.
+                                *
+                                * The w.org API endpoint only uses the IP address when a location
+                                * query is not provided, so we can safely avoid sending it when
+                                * there is a query.
+                                */
+                               $args['ip'] = $this->maybe_anonymize_ip_address( $this->get_unsafe_client_ip() );
+                       }
+               }
+
+               return add_query_arg( $args, $api_url );
+       }
+
+       /**
+        * Determines the user's actual IP address, if possible.
+        *
+        * $_SERVER['REMOTE_ADDR'] cannot be used in all cases, such as when the user
+        * is making their request through a proxy, or when the web server is behind
+        * a proxy. In those cases, $_SERVER['REMOTE_ADDR'] is set to the proxy address rather
+        * than the user's actual address.
+        *
+        * Modified from http://stackoverflow.com/a/2031935/450127, MIT license.
+        *
+        * SECURITY WARNING: This function is _NOT_ intended to be used in
+        * circumstances where the authenticity of the IP address matters. This does
+        * _NOT_ guarantee that the returned address is valid or accurate, and it can
+        * be easily spoofed.
+        *
+        * @access protected
+        * @since 4.8.0
+        *
+        * @return false|string false on failure, the string address on success.
+        */
+       protected function get_unsafe_client_ip() {
+               $client_ip = false;
+
+               // In order of preference, with the best ones for this purpose first.
+               $address_headers = array(
+                       'HTTP_CLIENT_IP',
+                       'HTTP_X_FORWARDED_FOR',
+                       'HTTP_X_FORWARDED',
+                       'HTTP_X_CLUSTER_CLIENT_IP',
+                       'HTTP_FORWARDED_FOR',
+                       'HTTP_FORWARDED',
+                       'REMOTE_ADDR',
+               );
+
+               foreach ( $address_headers as $header ) {
+                       if ( array_key_exists( $header, $_SERVER ) ) {
+                               /*
+                                * HTTP_X_FORWARDED_FOR can contain a chain of comma-separated
+                                * addresses. The first one is the original client. It can't be
+                                * trusted for authenticity, but we don't need to for this purpose.
+                                */
+                               $address_chain = explode( ',', $_SERVER[ $header ] );
+                               $client_ip     = trim( $address_chain[0] );
+
+                               break;
+                       }
+               }
+
+               return $client_ip;
+       }
+
+       /**
+        * Attempts to partially anonymize an IP address by converting it to a network ID.
+        *
+        * Geolocating the network ID usually returns a similar location as the
+        * actual IP, but provides some privacy for the user.
+        *
+        * Modified from https://github.com/geertw/php-ip-anonymizer, MIT license.
+        *
+        * @access protected
+        * @since 4.8.0
+        *
+        * @param  string      $address The IP address that should be anonymized.
+        * @return bool|string The anonymized address on success; the given address
+        *                     or false on failure.
+        */
+       protected function maybe_anonymize_ip_address( $address ) {
+               // These functions are not available on Windows until PHP 5.3.
+               if ( ! function_exists( 'inet_pton' ) || ! function_exists( 'inet_ntop' ) ) {
+                       return $address;
+               }
+
+               if ( 4 === strlen( inet_pton( $address ) ) ) {
+                       $netmask = '255.255.255.0'; // ipv4.
+               } else {
+                       $netmask = 'ffff:ffff:ffff:ffff:0000:0000:0000:0000'; // ipv6.
+               }
+
+               return inet_ntop( inet_pton( $address ) & inet_pton( $netmask ) );
+       }
+
+       /**
+        * Generates a transient key based on user location.
+        *
+        * This could be reduced to a one-liner in the calling functions, but it's
+        * intentionally a separate function because it's called from multiple
+        * functions, and having it abstracted keeps the logic consistent and DRY,
+        * which is less prone to errors.
+        *
+        * @access protected
+        * @since 4.8.0
+        *
+        * @param  array       $location Should contain 'latitude' and 'longitude' indexes.
+        * @return bool|string false on failure, or a string on success.
+        */
+       protected function get_events_transient_key( $location ) {
+               $key = false;
+
+               if ( isset( $location['latitude'], $location['longitude'] ) ) {
+                       $key = 'community-events-' . md5( $location['latitude'] . $location['longitude'] );
+               }
+
+               return $key;
+       }
+
+       /**
+        * Caches an array of events data from the Events API.
+        *
+        * @access protected
+        * @since 4.8.0
+        *
+        * @param array    $events               Response body from the API request.
+        * @param int|bool $expiration Optional. Amount of time to cache the events. Defaults to false.
+        * @return bool true if events were cached; false if not.
+        */
+       protected function cache_events( $events, $expiration = false ) {
+               $set              = false;
+               $transient_key    = $this->get_events_transient_key( $events['location'] );
+               $cache_expiration = $expiration ? absint( $expiration ) : HOUR_IN_SECONDS * 12;
+
+               if ( $transient_key ) {
+                       $set = set_site_transient( $transient_key, $events, $cache_expiration );
+               }
+
+               return $set;
+       }
+
+       /**
+        * Gets cached events.
+        *
+        * @since 4.8.0
+        *
+        * @return false|array false on failure; an array containing `location`
+        *                     and `events` items on success.
+        */
+       public function get_cached_events() {
+               $cached_response = get_site_transient( $this->get_events_transient_key( $this->user_location ) );
+               $cached_response = $this->trim_events( $cached_response );
+
+               return $this->format_event_data_time( $cached_response );
+       }
+
+       /**
+        * Adds formatted date and time items for each event in an API response.
+        *
+        * This has to be called after the data is pulled from the cache, because
+        * the cached events are shared by all users. If it was called before storing
+        * the cache, then all users would see the events in the localized data/time
+        * of the user who triggered the cache refresh, rather than their own.
+        *
+        * @access protected
+        * @since 4.8.0
+        *
+        * @param  array $response_body The response which contains the events.
+        * @return array The response with dates and times formatted.
+        */
+       protected function format_event_data_time( $response_body ) {
+               if ( isset( $response_body['events'] ) ) {
+                       foreach ( $response_body['events'] as $key => $event ) {
+                               $timestamp = strtotime( $event['date'] );
+
+                               /*
+                                * The `date_format` option is not used because it's important
+                                * in this context to keep the day of the week in the formatted date,
+                                * so that users can tell at a glance if the event is on a day they
+                                * are available, without having to open the link.
+                                */
+                               /* translators: Date format for upcoming events on the dashboard. Include the day of the week. See https://secure.php.net/date. */
+                               $response_body['events'][ $key ]['formatted_date'] = date_i18n( __( 'l, M j, Y' ), $timestamp );
+                               $response_body['events'][ $key ]['formatted_time'] = date_i18n( get_option( 'time_format' ), $timestamp );
+                       }
+               }
+
+               return $response_body;
+       }
+
+       /**
+        * Discards expired events, and reduces the remaining list.
+        *
+        * @access protected
+        * @since 4.8.0
+        *
+        * @param  array $response_body The response body which contains the events.
+        * @return array The response body with events trimmed.
+        */
+       protected function trim_events( $response_body ) {
+               if ( isset( $response_body['events'] ) ) {
+                       $current_timestamp = current_time('timestamp' );
+
+                       foreach ( $response_body['events'] as $key => $event ) {
+                               // Skip WordCamps, because they might be multi-day events.
+                               if ( 'meetup' !== $event['type'] ) {
+                                       continue;
+                               }
+
+                               $event_timestamp = strtotime( $event['date'] );
+
+                               if ( $current_timestamp > $event_timestamp && ( $current_timestamp - $event_timestamp ) > DAY_IN_SECONDS ) {
+                                       unset( $response_body['events'][ $key ] );
+                               }
+                       }
+
+                       $response_body['events'] = array_slice( $response_body['events'], 0, 3 );
+               }
+
+               return $response_body;
+       }
+
+       /**
+        * Logs responses to Events API requests.
+        *
+        * All responses are logged when debugging, even if they're not WP_Errors.
+        * Debugging info is still needed for "successful" responses, because
+        * the API might have returned a different location than the one the user
+        * intended to receive. In those cases, knowing the exact `request_url` is
+        * critical.
+        *
+        * Errors are logged instead of being triggered, to avoid breaking the JSON
+        * response when called from AJAX handlers and `display_errors` is enabled.
+        *
+        * @access protected
+        * @since 4.8.0
+        *
+        * @param string $message        A description of what occurred.
+        * @param array  $debugging_info Details that provide more context for the
+        *                               log entry.
+        */
+       protected function maybe_log_events_response( $message, $details ) {
+               if ( ! WP_DEBUG_LOG ) {
+                       return;
+               }
+
+               error_log( sprintf(
+                       '%s: %s. Details: %s',
+                       __METHOD__,
+                       trim( $message, '.' ),
+                       wp_json_encode( $details )
+               ) );
+       }
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/src/wp-admin/includes/class-wp-community-events.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="trunksrcwpadminincludesdashboardphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-admin/includes/dashboard.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-admin/includes/dashboard.php 2017-05-10 19:40:56 UTC (rev 40606)
+++ trunk/src/wp-admin/includes/dashboard.php   2017-05-10 20:03:01 UTC (rev 40607)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -52,8 +52,8 @@
</span><span class="cx" style="display: block; padding: 0 10px">                wp_add_dashboard_widget( 'dashboard_quick_press', $quick_draft_title, 'wp_dashboard_quick_press' );
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        // WordPress News
-       wp_add_dashboard_widget( 'dashboard_primary', __( 'WordPress News' ), 'wp_dashboard_primary' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ // WordPress Events and News
+       wp_add_dashboard_widget( 'dashboard_primary', __( 'WordPress Events and News' ), 'wp_dashboard_events_news' );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        if ( is_network_admin() ) {
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -130,6 +130,46 @@
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * Gets the community events data that needs to be passed to dashboard.js.
+ *
+ * @since 4.8.0
+ *
+ * @return array The script data.
+ */
+function wp_get_community_events_script_data() {
+       require_once( ABSPATH . 'wp-admin/includes/class-wp-community-events.php' );
+
+       $user_id       = get_current_user_id();
+       $user_location = get_user_option( 'community-events-location', $user_id );
+       $events_client = new WP_Community_Events( $user_id, $user_location );
+
+       $script_data = array(
+               'nonce' => wp_create_nonce( 'community_events' ),
+               'cache' => $events_client->get_cached_events(),
+
+               'l10n' => array(
+                       'enter_closest_city' => __( 'Enter your closest city to find nearby events.' ),
+                       'error_occurred_please_try_again' => __( 'An error occured. Please try again.' ),
+
+                       /*
+                        * These specific examples were chosen to highlight the fact that a
+                        * state is not needed, even for cities whose name is not unique.
+                        * It would be too cumbersome to include that in the instructions
+                        * to the user, so it's left as an implication.
+                        */
+                       /* translators: %s is the name of the city we couldn't locate. Replace the examples with cities in your locale, but test that they match the expected location before including them. Use endonyms (native locale names) whenever possible. */
+                       'could_not_locate_city' => __( "We couldn't locate %s. Please try another nearby city. For example: Kansas City; Springfield; Portland." ),
+
+                       // This one is only used with wp.a11y.speak(), so it can/should be more brief.
+                       /* translators: %s is the name of a city. */
+                       'city_updated' => __( 'City updated. Listing events near %s.' ),
+               )
+       );
+
+       return $script_data;
+}
+
+/**
</ins><span class="cx" style="display: block; padding: 0 10px">  * Adds a new dashboard widget.
</span><span class="cx" style="display: block; padding: 0 10px">  *
</span><span class="cx" style="display: block; padding: 0 10px">  * @since 2.7.0
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1069,10 +1109,173 @@
</span><span class="cx" style="display: block; padding: 0 10px">        wp_widget_rss_form( $widget_options[$widget_id], $form_inputs );
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
</ins><span class="cx" style="display: block; padding: 0 10px"> /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * Renders the Events and News dashboard widget.
+ *
+ * @since 4.8.0
+ */
+function wp_dashboard_events_news() {
+       wp_print_community_events_markup();
+
+       ?>
+
+       <div class="wordpress-news hide-if-no-js">
+               <?php wp_dashboard_primary(); ?>
+       </div>
+
+       <p class="community-events-footer">
+               <a href="https://make.wordpress.org/community/meetups-landing-page" target="_blank">
+                       <?php _e( 'Meetups' ); ?> <span class="dashicons dashicons-external"></span>
+               </a>
+
+               |
+
+               <a href="https://central.wordcamp.org/schedule/" target="_blank">
+                       <?php _e( 'WordCamps' ); ?> <span class="dashicons dashicons-external"></span>
+               </a>
+
+               |
+
+               <?php // translators: If a Rosetta site exists (e.g. https://es.wordpress.org/news/), then use that. Otherwise, leave untranslated. ?>
+               <a href="<?php _e( 'https://wordpress.org/news/' ); ?>" target="_blank">
+                       <?php _e( 'News' ); ?> <span class="dashicons dashicons-external"></span>
+               </a>
+       </p>
+
+       <?php
+}
+
+/**
+ * Prints the markup for the Community Events section of the Events and News Dashboard widget.
+ *
+ * @since 4.8.0
+ */
+function wp_print_community_events_markup() {
+       $script_data = wp_get_community_events_script_data();
+
+       ?>
+
+       <div class="community-events-errors notice notice-error inline hide-if-js">
+               <p class="hide-if-js">
+                       <?php _e( 'This widget requires JavaScript.'); ?>
+               </p>
+
+               <p class="community-events-error-occurred" aria-hidden="true">
+                       <?php echo $script_data['l10n']['error_occurred_please_try_again']; ?>
+               </p>
+
+               <p class="community-events-could-not-locate" aria-hidden="true"></p>
+       </div>
+
+       <div class="community-events-loading hide-if-no-js">
+               <?php _e( 'Loading&hellip;'); ?>
+       </div>
+
+       <?php
+       /*
+        * Hide the main element when the page first loads, because the content
+        * won't be ready until wp.communityEvents.renderEventsTemplate() has run.
+        */
+       ?>
+       <div id="community-events" class="community-events" aria-hidden="true">
+               <div class="activity-block">
+                       <p>
+                               <span id="community-events-location-message"></span>
+
+                               <button class="button-link community-events-toggle-location" aria-label="<?php _e( 'Edit city'); ?>" aria-expanded="false">
+                                       <span class="dashicons dashicons-edit"></span>
+                               </button>
+                       </p>
+
+                       <form class="community-events-form" aria-hidden="true" action="<?php echo esc_url( admin_url( 'admin-ajax.php' ) ); ?>" method="post">
+                               <label for="community-events-location">
+                                       <?php _e( 'City:' ); ?>
+                               </label>
+                               <?php /* translators: Replace with the name of a city in your locale that shows events. Use only the city name itself, without any region or country. Use the endonym instead of the English name. */ ?>
+                               <input id="community-events-location" class="regular-text" type="text" name="community-events-location" placeholder="<?php _e( 'Cincinnati' ); ?>" />
+
+                               <?php submit_button( __( 'Submit' ), 'secondary', 'community-events-submit', false ); ?>
+
+                               <button class="community-events-cancel button button-link" type="button" aria-expanded="false">
+                                       <?php _e( 'Cancel' ); ?>
+                               </button>
+
+                               <span class="spinner"></span>
+                       </form>
+               </div>
+
+               <ul class="community-events-results activity-block last"></ul>
+       </div>
+
+       <?php
+}
+
+/**
+ * Renders the events templates for the Event and News widget.
+ *
+ * @since 4.8.0
+ */
+function wp_print_community_events_templates() {
+       $script_data = wp_get_community_events_script_data();
+
+       ?>
+
+       <script id="tmpl-community-events-attend-event-near" type="text/template">
+               <?php printf(
+                       /* translators: %s is a placeholder for the name of a city. */
+                       __( 'Attend an upcoming event near %s.' ),
+                       '<strong>{{ data.location }}</strong>'
+               ); ?>
+       </script>
+
+       <script id="tmpl-community-events-could-not-locate" type="text/template">
+               <?php printf(
+                       $script_data['l10n']['could_not_locate_city'],
+                       '<em>{{data.unknownCity}}</em>'
+               ); ?>
+       </script>
+
+       <script id="tmpl-community-events-event-list" type="text/template">
+               <# _.each( data.events, function( event ) { #>
+                       <li class="event event-{{ event.type }} wp-clearfix">
+                               <div class="event-info">
+                                       <div class="dashicons event-icon" aria-hidden="true"></div>
+                                       <div class="event-info-inner">
+                                               <a class="event-title" href="{{ event.url }}">{{ event.title }}</a>
+                                               <span class="event-city">{{ event.location.location }}</span>
+                                       </div>
+                               </div>
+
+                               <div class="event-date-time">
+                                       <span class="event-date">{{ event.formatted_date }}</span>
+                                       <# if ( 'meetup' === event.type ) { #>
+                                               <span class="event-time">{{ event.formatted_time }}</span>
+                                       <# } #>
+                               </div>
+                       </li>
+               <# } ) #>
+       </script>
+
+       <script id="tmpl-community-events-no-upcoming-events" type="text/template">
+               <li class="event-none">
+                       <?php printf(
+                               /* translators: 1: the city the user searched for, 2: meetup organization documentation URL */
+                               __( 'There aren&#8217;t any events scheduled near %1$s at the moment. Would you like to <a href="%2$s">organize one</a>?' ),
+                               '{{data.location}}',
+                               __( 'https://make.wordpress.org/community/handbook/meetup-organizer/welcome/' )
+                       ); ?>
+               </li>
+       </script>
+
+       <?php
+}
+
+/**
</ins><span class="cx" style="display: block; padding: 0 10px">  * WordPress News dashboard widget.
</span><span class="cx" style="display: block; padding: 0 10px">  *
</span><span class="cx" style="display: block; padding: 0 10px">  * @since 2.7.0
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * @since 4.8.0 Removed popular plugins feed.
</ins><span class="cx" style="display: block; padding: 0 10px">  */
</span><span class="cx" style="display: block; padding: 0 10px"> function wp_dashboard_primary() {
</span><span class="cx" style="display: block; padding: 0 10px">        $feeds = array(
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1105,9 +1308,9 @@
</span><span class="cx" style="display: block; padding: 0 10px">                         */
</span><span class="cx" style="display: block; padding: 0 10px">                        'title'        => apply_filters( 'dashboard_primary_title', __( 'WordPress Blog' ) ),
</span><span class="cx" style="display: block; padding: 0 10px">                        'items'        => 1,
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        'show_summary' => 1,
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 'show_summary' => 0,
</ins><span class="cx" style="display: block; padding: 0 10px">                         'show_author'  => 0,
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        'show_date'    => 1,
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 'show_date'    => 0,
</ins><span class="cx" style="display: block; padding: 0 10px">                 ),
</span><span class="cx" style="display: block; padding: 0 10px">                'planet' => array(
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1152,20 +1355,6 @@
</span><span class="cx" style="display: block; padding: 0 10px">                )
</span><span class="cx" style="display: block; padding: 0 10px">        );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        if ( ( ! wp_disallow_file_mods( 'dashboard_widget' ) ) && ( ! is_multisite() && is_blog_admin() && current_user_can( 'install_plugins' ) ) || ( is_network_admin() && current_user_can( 'manage_network_plugins' ) && current_user_can( 'install_plugins' ) ) ) {
-               $feeds['plugins'] = array(
-                       'link'         => '',
-                       'url'          => array(
-                               'popular' => 'http://wordpress.org/plugins/rss/browse/popular/',
-                       ),
-                       'title'        => '',
-                       'items'        => 1,
-                       'show_summary' => 0,
-                       'show_author'  => 0,
-                       'show_date'    => 0,
-               );
-       }
-
</del><span class="cx" style="display: block; padding: 0 10px">         wp_dashboard_cached_rss_widget( 'dashboard_primary', 'wp_dashboard_primary_output', $feeds );
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1173,6 +1362,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">  * Display the WordPress news feeds.
</span><span class="cx" style="display: block; padding: 0 10px">  *
</span><span class="cx" style="display: block; padding: 0 10px">  * @since 3.8.0
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * @since 4.8.0 Removed popular plugins feed.
</ins><span class="cx" style="display: block; padding: 0 10px">  *
</span><span class="cx" style="display: block; padding: 0 10px">  * @param string $widget_id Widget ID.
</span><span class="cx" style="display: block; padding: 0 10px">  * @param array  $feeds     Array of RSS feeds.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1181,95 +1371,12 @@
</span><span class="cx" style="display: block; padding: 0 10px">        foreach ( $feeds as $type => $args ) {
</span><span class="cx" style="display: block; padding: 0 10px">                $args['type'] = $type;
</span><span class="cx" style="display: block; padding: 0 10px">                echo '<div class="rss-widget">';
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                if ( $type === 'plugins' ) {
-                       wp_dashboard_plugins_output( $args['url'], $args );
-               } else {
</del><span class="cx" style="display: block; padding: 0 10px">                         wp_widget_rss_output( $args['url'], $args );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                }
</del><span class="cx" style="display: block; padding: 0 10px">                 echo "</div>";
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> /**
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- * Display plugins text for the WordPress news widget.
- *
- * @since 2.5.0
- *
- * @param string $rss  The RSS feed URL.
- * @param array  $args Array of arguments for this RSS feed.
- */
-function wp_dashboard_plugins_output( $rss, $args = array() ) {
-       // Plugin feeds plus link to install them
-       $popular = fetch_feed( $args['url']['popular'] );
-
-       if ( false === $plugin_slugs = get_transient( 'plugin_slugs' ) ) {
-               $plugin_slugs = array_keys( get_plugins() );
-               set_transient( 'plugin_slugs', $plugin_slugs, DAY_IN_SECONDS );
-       }
-
-       echo '<ul>';
-
-       foreach ( array( $popular ) as $feed ) {
-               if ( is_wp_error( $feed ) || ! $feed->get_item_quantity() )
-                       continue;
-
-               $items = $feed->get_items(0, 5);
-
-               // Pick a random, non-installed plugin
-               while ( true ) {
-                       // Abort this foreach loop iteration if there's no plugins left of this type
-                       if ( 0 == count($items) )
-                               continue 2;
-
-                       $item_key = array_rand($items);
-                       $item = $items[$item_key];
-
-                       list($link, $frag) = explode( '#', $item->get_link() );
-
-                       $link = esc_url($link);
-                       if ( preg_match( '|/([^/]+?)/?$|', $link, $matches ) )
-                               $slug = $matches[1];
-                       else {
-                               unset( $items[$item_key] );
-                               continue;
-                       }
-
-                       // Is this random plugin's slug already installed? If so, try again.
-                       reset( $plugin_slugs );
-                       foreach ( $plugin_slugs as $plugin_slug ) {
-                               if ( $slug == substr( $plugin_slug, 0, strlen( $slug ) ) ) {
-                                       unset( $items[$item_key] );
-                                       continue 2;
-                               }
-                       }
-
-                       // If we get to this point, then the random plugin isn't installed and we can stop the while().
-                       break;
-               }
-
-               // Eliminate some common badly formed plugin descriptions
-               while ( ( null !== $item_key = array_rand($items) ) && false !== strpos( $items[$item_key]->get_description(), 'Plugin Name:' ) )
-                       unset($items[$item_key]);
-
-               if ( !isset($items[$item_key]) )
-                       continue;
-
-               $raw_title = $item->get_title();
-
-               $ilink = wp_nonce_url('plugin-install.php?tab=plugin-information&plugin=' . $slug, 'install-plugin_' . $slug) . '&amp;TB_iframe=true&amp;width=600&amp;height=800';
-               echo '<li class="dashboard-news-plugin"><span>' . __( 'Popular Plugin' ) . ':</span> ' . esc_html( $raw_title ) .
-                       '&nbsp;<a href="' . $ilink . '" class="thickbox open-plugin-details-modal" aria-label="' .
-                       /* translators: %s: plugin name */
-                       esc_attr( sprintf( __( 'Install %s' ), $raw_title ) ) . '">(' . __( 'Install' ) . ')</a></li>';
-
-               $feed->__destruct();
-               unset( $feed );
-       }
-
-       echo '</ul>';
-}
-
-/**
</del><span class="cx" style="display: block; padding: 0 10px">  * Display file upload quota on dashboard.
</span><span class="cx" style="display: block; padding: 0 10px">  *
</span><span class="cx" style="display: block; padding: 0 10px">  * Runs on the {@see 'activity_box_end'} hook in wp_dashboard_right_now().
</span></span></pre></div>
<a id="trunksrcwpadminincludesdeprecatedphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-admin/includes/deprecated.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-admin/includes/deprecated.php        2017-05-10 19:40:56 UTC (rev 40606)
+++ trunk/src/wp-admin/includes/deprecated.php  2017-05-10 20:03:01 UTC (rev 40607)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1295,6 +1295,88 @@
</span><span class="cx" style="display: block; padding: 0 10px"> function wp_dashboard_secondary_control() {}
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * Display plugins text for the WordPress news widget.
+ *
+ * @since 2.5.0
+ * @deprecated 4.8.0
+ *
+ * @param string $rss  The RSS feed URL.
+ * @param array  $args Array of arguments for this RSS feed.
+ */
+function wp_dashboard_plugins_output( $rss, $args = array() ) {
+       _deprecated_function( __FUNCTION__, '4.8.0' );
+
+       // Plugin feeds plus link to install them
+       $popular = fetch_feed( $args['url']['popular'] );
+
+       if ( false === $plugin_slugs = get_transient( 'plugin_slugs' ) ) {
+               $plugin_slugs = array_keys( get_plugins() );
+               set_transient( 'plugin_slugs', $plugin_slugs, DAY_IN_SECONDS );
+       }
+
+       echo '<ul>';
+
+       foreach ( array( $popular ) as $feed ) {
+               if ( is_wp_error( $feed ) || ! $feed->get_item_quantity() )
+                       continue;
+
+               $items = $feed->get_items(0, 5);
+
+               // Pick a random, non-installed plugin
+               while ( true ) {
+                       // Abort this foreach loop iteration if there's no plugins left of this type
+                       if ( 0 == count($items) )
+                               continue 2;
+
+                       $item_key = array_rand($items);
+                       $item = $items[$item_key];
+
+                       list($link, $frag) = explode( '#', $item->get_link() );
+
+                       $link = esc_url($link);
+                       if ( preg_match( '|/([^/]+?)/?$|', $link, $matches ) )
+                               $slug = $matches[1];
+                       else {
+                               unset( $items[$item_key] );
+                               continue;
+                       }
+
+                       // Is this random plugin's slug already installed? If so, try again.
+                       reset( $plugin_slugs );
+                       foreach ( $plugin_slugs as $plugin_slug ) {
+                               if ( $slug == substr( $plugin_slug, 0, strlen( $slug ) ) ) {
+                                       unset( $items[$item_key] );
+                                       continue 2;
+                               }
+                       }
+
+                       // If we get to this point, then the random plugin isn't installed and we can stop the while().
+                       break;
+               }
+
+               // Eliminate some common badly formed plugin descriptions
+               while ( ( null !== $item_key = array_rand($items) ) && false !== strpos( $items[$item_key]->get_description(), 'Plugin Name:' ) )
+                       unset($items[$item_key]);
+
+               if ( !isset($items[$item_key]) )
+                       continue;
+
+               $raw_title = $item->get_title();
+
+               $ilink = wp_nonce_url('plugin-install.php?tab=plugin-information&plugin=' . $slug, 'install-plugin_' . $slug) . '&amp;TB_iframe=true&amp;width=600&amp;height=800';
+               echo '<li class="dashboard-news-plugin"><span>' . __( 'Popular Plugin' ) . ':</span> ' . esc_html( $raw_title ) .
+                       '&nbsp;<a href="' . $ilink . '" class="thickbox open-plugin-details-modal" aria-label="' .
+                       /* translators: %s: plugin name */
+                       esc_attr( sprintf( __( 'Install %s' ), $raw_title ) ) . '">(' . __( 'Install' ) . ')</a></li>';
+
+               $feed->__destruct();
+               unset( $feed );
+       }
+
+       echo '</ul>';
+}
+
+/**
</ins><span class="cx" style="display: block; padding: 0 10px">  * This was once used to move child posts to a new parent.
</span><span class="cx" style="display: block; padding: 0 10px">  *
</span><span class="cx" style="display: block; padding: 0 10px">  * @since 2.3.0
</span></span></pre></div>
<a id="trunksrcwpadminincludesupgradephp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-admin/includes/upgrade.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-admin/includes/upgrade.php   2017-05-10 19:40:56 UTC (rev 40606)
+++ trunk/src/wp-admin/includes/upgrade.php     2017-05-10 20:03:01 UTC (rev 40607)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -565,6 +565,10 @@
</span><span class="cx" style="display: block; padding: 0 10px">        if ( $wp_current_db_version < 37965 )
</span><span class="cx" style="display: block; padding: 0 10px">                upgrade_460();
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        if ( $wp_current_db_version < 40500 ) { //todo update to commit for #40702
+               upgrade_480();
+       }
+
</ins><span class="cx" style="display: block; padding: 0 10px">         maybe_disable_link_manager();
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        maybe_disable_automattic_widgets();
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1733,6 +1737,26 @@
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * Executes changes made in WordPress 4.8.0.
+ *
+ * @ignore
+ * @since 4.8.0
+ *
+ * @global int $wp_current_db_version Current database version.
+ */
+function upgrade_480() {
+       global $wp_current_db_version;
+
+       if ( $wp_current_db_version < 40500 ) { // todo update to commit for #40702
+               // This feature plugin was merged for #40702, so the plugin itself is no longer needed
+               deactivate_plugins( array( 'nearby-wp-events/nearby-wordpress-events.php' ), true );
+
+               // The markup stored in this transient changed for #40702
+               delete_transient( 'dash_' . md5( 'dashboard_primary' . '_' . get_locale() ) );
+       }
+}
+
+/**
</ins><span class="cx" style="display: block; padding: 0 10px">  * Executes network-level upgrade routines.
</span><span class="cx" style="display: block; padding: 0 10px">  *
</span><span class="cx" style="display: block; padding: 0 10px">  * @since 3.0.0
</span></span></pre></div>
<a id="trunksrcwpadminindexphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-admin/index.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-admin/index.php      2017-05-10 19:40:56 UTC (rev 40606)
+++ trunk/src/wp-admin/index.php        2017-05-10 20:03:01 UTC (rev 40607)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -15,6 +15,8 @@
</span><span class="cx" style="display: block; padding: 0 10px"> wp_dashboard_setup();
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> wp_enqueue_script( 'dashboard' );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+wp_localize_script( 'dashboard', 'communityEventsData', wp_get_community_events_script_data() );
+
</ins><span class="cx" style="display: block; padding: 0 10px"> if ( current_user_can( 'edit_theme_options' ) )
</span><span class="cx" style="display: block; padding: 0 10px">        wp_enqueue_script( 'customize-loader' );
</span><span class="cx" style="display: block; padding: 0 10px"> if ( current_user_can( 'install_plugins' ) ) {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -138,4 +140,6 @@
</span><span class="cx" style="display: block; padding: 0 10px"> </div><!-- wrap -->
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> <?php
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+wp_print_community_events_templates();
+
</ins><span class="cx" style="display: block; padding: 0 10px"> require( ABSPATH . 'wp-admin/admin-footer.php' );
</span></span></pre></div>
<a id="trunksrcwpadminjsdashboardjs"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-admin/js/dashboard.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-admin/js/dashboard.js        2017-05-10 19:40:56 UTC (rev 40606)
+++ trunk/src/wp-admin/js/dashboard.js  2017-05-10 20:03:01 UTC (rev 40607)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1,5 +1,6 @@
</span><span class="cx" style="display: block; padding: 0 10px"> /* global pagenow, ajaxurl, postboxes, wpActiveEditor:true */
</span><span class="cx" style="display: block; padding: 0 10px"> var ajaxWidgets, ajaxPopulateWidgets, quickPressLoad;
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+window.wp = window.wp || {};
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> jQuery(document).ready( function($) {
</span><span class="cx" style="display: block; padding: 0 10px">        var welcomePanel = $( '#welcome-panel' ),
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -187,3 +188,262 @@
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> } );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+jQuery( function( $ ) {
+       'use strict';
+       
+       var communityEventsData = window.communityEventsData || {};
+
+       var app = window.wp.communityEvents = {
+               initialized: false,
+               model: null,
+
+               /**
+                * Initializes the wp.communityEvents object.
+                *
+                * @since 4.8.0
+                */
+               init: function() {
+                       if ( app.initialized ) {
+                               return;
+                       }
+
+                       var $container = $( '#community-events' );
+
+                       /*
+                        * When JavaScript is disabled, the errors container is shown, so
+                        * that "This widget requires Javascript" message can be seen.
+                        *
+                        * When JS is enabled, the container is hidden at first, and then
+                        * revealed during the template rendering, if there actually are
+                        * errors to show.
+                        *
+                        * The display indicator switches from `hide-if-js` to `aria-hidden`
+                        * here in order to maintain consistency with all the other fields
+                        * that key off of `aria-hidden` to determine their visibility.
+                        * `aria-hidden` can't be used initially, because there would be no
+                        * way to set it to false when JavaScript is disabled, which would
+                        * prevent people from seeing the "This widget requires JavaScript"
+                        * message.
+                        */
+                       $( '.community-events-errors' )
+                               .attr( 'aria-hidden', true )
+                               .removeClass( 'hide-if-js' );
+
+                       $container.on( 'click', '.community-events-toggle-location, .community-events-cancel', app.toggleLocationForm );
+
+                       $container.on( 'submit', '.community-events-form', function( event ) {
+                               event.preventDefault();
+
+                               app.getEvents( {
+                                       location: $( '#community-events-location' ).val()
+                               });
+                       });
+
+                       if ( communityEventsData && communityEventsData.cache && communityEventsData.cache.location && communityEventsData.cache.events ) {
+                               app.renderEventsTemplate( communityEventsData.cache, 'app' );
+                       } else {
+                               app.getEvents();
+                       }
+
+                       app.initialized = true;
+               },
+
+               /**
+                * Toggles the visibility of the Edit Location form.
+                *
+                * @since 4.8.0
+                *
+                * @param {event|string} action 'show' or 'hide' to specify a state;
+                *                              Or an event object to flip between states
+                */
+               toggleLocationForm: function( action ) {
+                       var $toggleButton = $( '.community-events-toggle-location' ),
+                           $cancelButton = $( '.community-events-cancel' ),
+                           $form         = $( '.community-events-form' );
+
+                       if ( 'object' === typeof action ) {
+                               // Strict comparison doesn't work in this case.
+                               action = 'true' == $toggleButton.attr( 'aria-expanded' ) ? 'hide' : 'show';
+                       }
+
+                       if ( 'hide' === action ) {
+                               $toggleButton.attr( 'aria-expanded', false );
+                               $cancelButton.attr( 'aria-expanded', false );
+                               $form.attr( 'aria-hidden', true );
+                       } else {
+                               $toggleButton.attr( 'aria-expanded', true );
+                               $cancelButton.attr( 'aria-expanded', true );
+                               $form.attr( 'aria-hidden', false );
+                       }
+               },
+
+               /**
+                * Sends REST API requests to fetch events for the widget.
+                *
+                * @since 4.8.0
+                *
+                * @param {object} requestParams
+                */
+               getEvents: function( requestParams ) {
+                       var initiatedBy,
+                           app = this,
+                           $spinner = $( '.community-events-form' ).children( '.spinner' );
+
+                       requestParams          = requestParams || {};
+                       requestParams._wpnonce = communityEventsData.nonce;
+                       requestParams.timezone = window.Intl ? window.Intl.DateTimeFormat().resolvedOptions().timeZone : '';
+
+                       initiatedBy = requestParams.location ? 'user' : 'app';
+
+                       $spinner.addClass( 'is-active' );
+
+                       wp.ajax.post( 'get-community-events', requestParams )
+                               .always( function() {
+                                       $spinner.removeClass( 'is-active' );
+                               })
+
+                               .done( function( response ) {
+                                       if ( 'no_location_available' === response.error ) {
+                                               if ( requestParams.location ) {
+                                                       response.unknownCity = requestParams.location;
+                                               } else {
+                                                       /*
+                                                        * No location was passed, which means that this was an automatic query
+                                                        * based on IP, locale, and timezone. Since the user didn't initiate it,
+                                                        * it should fail silently. Otherwise, the error could confuse and/or
+                                                        * annoy them.
+                                                        */
+
+                                                       delete response.error;
+                                               }
+                                       }
+                                       app.renderEventsTemplate( response, initiatedBy );
+                               })
+
+                               .fail( function() {
+                                       app.renderEventsTemplate( {
+                                               'location' : false,
+                                               'error'    : true
+                                       }, initiatedBy );
+                               });
+               },
+
+               /**
+                * Renders the template for the Events section of the Events & News widget.
+                *
+                * @since 4.8.0
+                *
+                * @param {Object} templateParams The various parameters that will get passed to wp.template
+                * @param {string} initiatedBy    'user' to indicate that this was triggered manually by the user;
+                *                                'app' to indicate it was triggered automatically by the app itself.
+                */
+               renderEventsTemplate: function( templateParams, initiatedBy ) {
+                       var template,
+                           elementVisibility,
+                           l10nPlaceholder  = /%(?:\d\$)?s/g, // Match `%s`, `%1$s`, `%2$s`, etc.
+                           $locationMessage = $( '#community-events-location-message' ),
+                           $results         = $( '.community-events-results' );
+
+                       /*
+                        * Hide all toggleable elements by default, to keep the logic simple.
+                        * Otherwise, each block below would have to turn hide everything that
+                        * could have been shown at an earlier point.
+                        *
+                        * The exception to that is that the .community-events container. It's hidden
+                        * when the page is first loaded, because the content isn't ready yet,
+                        * but once we've reached this point, it should always be shown.
+                        */
+                       elementVisibility = {
+                               '.community-events'                  : true,
+                               '.community-events-loading'          : false,
+                               '.community-events-errors'           : false,
+                               '.community-events-error-occurred'   : false,
+                               '.community-events-could-not-locate' : false,
+                               '#community-events-location-message' : false,
+                               '.community-events-toggle-location'  : false,
+                               '.community-events-results'          : false
+                       };
+
+                       /*
+                        * Determine which templates should be rendered and which elements
+                        * should be displayed.
+                        */
+                       if ( templateParams.location ) {
+                               template = wp.template( 'community-events-attend-event-near' );
+                               $locationMessage.html( template( templateParams ) );
+
+                               if ( templateParams.events.length ) {
+                                       template = wp.template( 'community-events-event-list' );
+                                       $results.html( template( templateParams ) );
+                               } else {
+                                       template = wp.template( 'community-events-no-upcoming-events' );
+                                       $results.html( template( templateParams ) );
+                               }
+                               wp.a11y.speak( communityEventsData.l10n.city_updated.replace( l10nPlaceholder, templateParams.location ) );
+
+                               elementVisibility['#community-events-location-message'] = true;
+                               elementVisibility['.community-events-toggle-location']  = true;
+                               elementVisibility['.community-events-results']          = true;
+
+                       } else if ( templateParams.unknownCity ) {
+                               template = wp.template( 'community-events-could-not-locate' );
+                               $( '.community-events-could-not-locate' ).html( template( templateParams ) );
+                               wp.a11y.speak( communityEventsData.l10n.could_not_locate_city.replace( l10nPlaceholder, templateParams.unknownCity ) );
+
+                               elementVisibility['.community-events-errors']           = true;
+                               elementVisibility['.community-events-could-not-locate'] = true;
+
+                       } else if ( templateParams.error && 'user' === initiatedBy ) {
+                               /*
+                                * Errors messages are only shown for requests that were initiated
+                                * by the user, not for ones that were initiated by the app itself.
+                                * Showing error messages for an event that user isn't aware of
+                                * could be confusing or unnecessarily distracting.
+                                */
+                               wp.a11y.speak( communityEventsData.l10n.error_occurred_please_try_again );
+
+                               elementVisibility['.community-events-errors']         = true;
+                               elementVisibility['.community-events-error-occurred'] = true;
+
+                       } else {
+                               $locationMessage.text( communityEventsData.l10n.enter_closest_city );
+
+                               elementVisibility['#community-events-location-message'] = true;
+                               elementVisibility['.community-events-toggle-location']  = true;
+                       }
+
+                       // Set the visibility of toggleable elements.
+                       _.each( elementVisibility, function( isVisible, element ) {
+                               $( element ).attr( 'aria-hidden', ! isVisible );
+                       });
+
+                       $( '.community-events-toggle-location' ).attr( 'aria-expanded', elementVisibility['.community-events-toggle-location'] );
+
+                       /*
+                        * During the initial page load, the location form should be hidden
+                        * by default if the user has saved a valid location during a previous
+                        * session. It's safe to assume that they want to continue using that
+                        * location, and displaying the form would unnecessarily clutter the
+                        * widget.
+                        */
+                       if ( 'app' === initiatedBy && templateParams.location ) {
+                               app.toggleLocationForm( 'hide' );
+                       } else {
+                               app.toggleLocationForm( 'show' );
+                       }
+               }
+       };
+
+       if ( $( '#dashboard_primary' ).is( ':visible' ) ) {
+               app.init();
+       } else {
+               $( document ).on( 'postbox-toggled', function( event, postbox ) {
+                       var $postbox = $( postbox );
+
+                       if ( 'dashboard_primary' === $postbox.attr( 'id' ) && $postbox.is( ':visible' ) ) {
+                               app.init();
+                       }
+               });
+       }
+});
</ins></span></pre></div>
<a id="trunksrcwpadminnetworkindexphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-admin/network/index.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-admin/network/index.php      2017-05-10 19:40:56 UTC (rev 40606)
+++ trunk/src/wp-admin/network/index.php        2017-05-10 20:03:01 UTC (rev 40607)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -54,6 +54,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> wp_dashboard_setup();
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> wp_enqueue_script( 'dashboard' );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+wp_localize_script( 'dashboard', 'communityEventsData', wp_get_community_events_script_data() );
</ins><span class="cx" style="display: block; padding: 0 10px"> wp_enqueue_script( 'plugin-install' );
</span><span class="cx" style="display: block; padding: 0 10px"> add_thickbox();
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -73,4 +74,6 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> </div><!-- wrap -->
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-<?php include( ABSPATH . 'wp-admin/admin-footer.php' ); ?>
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+wp_print_community_events_templates();
+include( ABSPATH . 'wp-admin/admin-footer.php' );
</ins></span></pre></div>
<a id="trunksrcwpincludesscriptloaderphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/script-loader.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/script-loader.php   2017-05-10 19:40:56 UTC (rev 40606)
+++ trunk/src/wp-includes/script-loader.php     2017-05-10 20:03:01 UTC (rev 40607)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -724,7 +724,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        'current' => __( 'Current Color' ),
</span><span class="cx" style="display: block; padding: 0 10px">                ) );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $scripts->add( 'dashboard', "/wp-admin/js/dashboard$suffix.js", array( 'jquery', 'admin-comments', 'postbox' ), false, 1 );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $scripts->add( 'dashboard', "/wp-admin/js/dashboard$suffix.js", array( 'jquery', 'admin-comments', 'postbox', 'wp-util', 'wp-a11y' ), false, 1 );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $scripts->add( 'list-revisions', "/wp-includes/js/wp-list-revisions$suffix.js" );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span></span></pre></div>
<a id="trunktestsphpunittestsadminincludesCommunityEventsphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/tests/admin/includesCommunityEvents.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/admin/includesCommunityEvents.php                               (rev 0)
+++ trunk/tests/phpunit/tests/admin/includesCommunityEvents.php 2017-05-10 20:03:01 UTC (rev 40607)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,258 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Unit tests for methods in WP_Community_Events.
+ *
+ * @package WordPress
+ * @subpackage UnitTests
+ * @since 4.8.0
+ */
+
+/**
+ * Class Test_WP_Community_Events.
+ *
+ * @group admin
+ * @group community-events
+ *
+ * @since 4.8.0
+ */
+class Test_WP_Community_Events extends WP_UnitTestCase {
+       /**
+        * An instance of the class to test.
+        *
+        * @access private
+        * @since 4.8.0
+        *
+        * @var WP_Community_Events
+        */
+       private $instance;
+
+       /**
+        * Performs setup tasks for every test.
+        *
+        * @since 4.8.0
+        */
+       public function setUp() {
+               parent::setUp();
+
+               require_once( ABSPATH . 'wp-admin/includes/class-wp-community-events.php' );
+
+               $this->instance = new WP_Community_Events( 1, $this->get_user_location() );
+       }
+
+       /**
+        * Simulates a stored user location.
+        *
+        * @access private
+        * @since 4.8.0
+        *
+        * @return array The mock location.
+        */
+       private function get_user_location() {
+               return array(
+                       'description' => 'San Francisco',
+                       'latitude'    => '37.7749300',
+                       'longitude'   => '-122.4194200',
+                       'country'     => 'US',
+               );
+       }
+
+       /**
+        * Test: get_events() should return an instance of WP_Error if the response code is not 200.
+        *
+        * @since 4.8.0
+        */
+       public function test_get_events_bad_response_code() {
+               add_filter( 'pre_http_request', array( $this, '_http_request_bad_response_code' ) );
+
+               $this->assertWPError( $this->instance->get_events() );
+
+               remove_filter( 'pre_http_request', array( $this, '_http_request_bad_response_code' ) );
+       }
+
+       /**
+        * Test: The response body should not be cached if the response code is not 200.
+        *
+        * @since 4.8.0
+        */
+       public function test_get_cached_events_bad_response_code() {
+               add_filter( 'pre_http_request', array( $this, '_http_request_bad_response_code' ) );
+
+               $this->instance->get_events();
+
+               $this->assertFalse( $this->instance->get_cached_events() );
+
+               remove_filter( 'pre_http_request', array( $this, '_http_request_bad_response_code' ) );
+       }
+
+       /**
+        * Simulates an HTTP response with a non-200 response code.
+        *
+        * @since 4.8.0
+        *
+        * @return array A mock response with a 404 HTTP status code
+        */
+       public function _http_request_bad_response_code() {
+               return array(
+                       'headers'  => '',
+                       'body'     => '',
+                       'response' => array(
+                               'code' => 404,
+                       ),
+                       'cookies'  => '',
+                       'filename' => '',
+               );
+       }
+
+       /**
+        * Test: get_events() should return an instance of WP_Error if the response body does not have
+        * the required properties.
+        *
+        * @since 4.8.0
+        */
+       public function test_get_events_invalid_response() {
+               add_filter( 'pre_http_request', array( $this, '_http_request_invalid_response' ) );
+
+               $this->assertWPError( $this->instance->get_events() );
+
+               remove_filter( 'pre_http_request', array( $this, '_http_request_invalid_response' ) );
+       }
+
+       /**
+        * Test: The response body should not be cached if it does not have the required properties.
+        *
+        * @since 4.8.0
+        */
+       public function test_get_cached_events_invalid_response() {
+               add_filter( 'pre_http_request', array( $this, '_http_request_invalid_response' ) );
+
+               $this->instance->get_events();
+
+               $this->assertFalse( $this->instance->get_cached_events() );
+
+               remove_filter( 'pre_http_request', array( $this, '_http_request_invalid_response' ) );
+       }
+
+       /**
+        * Simulates an HTTP response with a body that does not have the required properties.
+        *
+        * @since 4.8.0
+        *
+        * @return array A mock response that's missing required properties.
+        */
+       public function _http_request_invalid_response() {
+               return array(
+                       'headers'  => '',
+                       'body'     => wp_json_encode( array() ),
+                       'response' => array(
+                               'code' => 200,
+                       ),
+                       'cookies'  => '',
+                       'filename' => '',
+               );
+       }
+
+       /**
+        * Test: With a valid response, get_events() should return an associated array containing a location array and
+        * an events array with individual events that have formatted time and date.
+        *
+        * @since 4.8.0
+        */
+       public function test_get_events_valid_response() {
+               add_filter( 'pre_http_request', array( $this, '_http_request_valid_response' ) );
+
+               $response = $this->instance->get_events();
+
+               $this->assertNotWPError( $response );
+               $this->assertEqualSetsWithIndex( $this->get_user_location(), $response['location'] );
+               $this->assertEquals( date( 'l, M j, Y', strtotime( 'next Sunday 1pm' ) ), $response['events'][0]['formatted_date'] );
+               $this->assertEquals( '1:00 pm', $response['events'][0]['formatted_time'] );
+
+               remove_filter( 'pre_http_request', array( $this, '_http_request_valid_response' ) );
+       }
+
+       /**
+        * Test: get_cached_events() should return the same data as get_events(), including formatted time
+        * and date values for each event.
+        *
+        * @since 4.8.0
+        */
+       public function test_get_cached_events_valid_response() {
+               add_filter( 'pre_http_request', array( $this, '_http_request_valid_response' ) );
+
+               $this->instance->get_events();
+
+               $cached_events = $this->instance->get_cached_events();
+
+               $this->assertNotWPError( $cached_events );
+               $this->assertEqualSetsWithIndex( $this->get_user_location(), $cached_events['location'] );
+               $this->assertEquals( date( 'l, M j, Y', strtotime( 'next Sunday 1pm' ) ), $cached_events['events'][0]['formatted_date'] );
+               $this->assertEquals( '1:00 pm', $cached_events['events'][0]['formatted_time'] );
+
+               remove_filter( 'pre_http_request', array( $this, '_http_request_valid_response' ) );
+       }
+
+       /**
+        * Simulates an HTTP response with valid location and event data.
+        *
+        * @since 4.8.0
+        *
+        * @return array A mock HTTP response with valid data.
+        */
+       public function _http_request_valid_response() {
+               return array(
+                       'headers'  => '',
+                       'body'     => wp_json_encode( array(
+                               'location' => $this->get_user_location(),
+                               'events'   => array(
+                                       array(
+                                               'type'           => 'meetup',
+                                               'title'          => 'Flexbox + CSS Grid: Magic for Responsive Layouts',
+                                               'url'            => 'https://www.meetup.com/Eastbay-WordPress-Meetup/events/236031233/',
+                                               'meetup'         => 'The East Bay WordPress Meetup Group',
+                                               'meetup_url'     => 'https://www.meetup.com/Eastbay-WordPress-Meetup/',
+                                               'date'           => date( 'Y-m-d H:i:s', strtotime( 'next Sunday 1pm' ) ),
+                                               'location'       => array(
+                                                       'location'  => 'Oakland, CA, USA',
+                                                       'country'   => 'us',
+                                                       'latitude'  => 37.808453,
+                                                       'longitude' => -122.26593,
+                                               ),
+                                       ),
+                                       array(
+                                               'type'           => 'meetup',
+                                               'title'          => 'Part 3- Site Maintenance - Tools to Make It Easy',
+                                               'url'            => 'https://www.meetup.com/Wordpress-Bay-Area-CA-Foothills/events/237706839/',
+                                               'meetup'         => 'WordPress Bay Area Foothills Group',
+                                               'meetup_url'     => 'https://www.meetup.com/Wordpress-Bay-Area-CA-Foothills/',
+                                               'date'           => date( 'Y-m-d H:i:s', strtotime( 'next Wednesday 1:30pm' ) ),
+                                               'location'       => array(
+                                                       'location'  => 'Milpitas, CA, USA',
+                                                       'country'   => 'us',
+                                                       'latitude'  => 37.432813,
+                                                       'longitude' => -121.907095,
+                                               ),
+                                       ),
+                                       array(
+                                               'type'           => 'wordcamp',
+                                               'title'          => 'WordCamp Kansas City',
+                                               'url'            => 'https://2017.kansascity.wordcamp.org',
+                                               'meetup'         => null,
+                                               'meetup_url'     => null,
+                                               'date'           => date( 'Y-m-d H:i:s', strtotime( 'next Saturday' ) ),
+                                               'location'       => array(
+                                                       'location'  => 'Kansas City, MO',
+                                                       'country'   => 'US',
+                                                       'latitude'  => 39.0392325,
+                                                       'longitude' => -94.577076,
+                                               ),
+                                       ),
+                               ),
+                       ) ),
+                       'response' => array(
+                               'code' => 200,
+                       ),
+                       'cookies'  => '',
+                       'filename' => '',
+               );
+       }
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/tests/phpunit/tests/admin/includesCommunityEvents.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span></div>

</body>
</html>