summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRyan Campbell <ryanjcampbell@google.com>2017-08-23 12:38:16 -0700
committerRyan Campbell <ryanjcampbell@google.com>2017-08-23 12:38:16 -0700
commit788031ccad40b70b0c4479a96a4d82b88e88c90c (patch)
tree196a7764dedb113d4dacd17e905a276aa444c53d
parentbf87c647f9907b73df940150bd2ea98c99329e10 (diff)
downloaddashboard-788031ccad40b70b0c4479a96a4d82b88e88c90c.tar.gz
Allow user to mute notifications on a favorite.
Allow the user to disable email notifications for a test without removing it from favorites. This is the first step in some actionable user feedback because this change can be used to allow the user to unsubscribe from email alerts. Test: staging Bug: 62999724 Change-Id: I4df89ba3ac00ab7b173fb503913bda65227fca86
-rw-r--r--src/main/java/com/android/vts/api/UserFavoriteRestServlet.java133
-rw-r--r--src/main/java/com/android/vts/entity/UserFavoriteEntity.java34
-rw-r--r--src/main/java/com/android/vts/servlet/DashboardMainServlet.java27
-rw-r--r--src/main/java/com/android/vts/util/EmailHelper.java6
-rw-r--r--src/main/webapp/WEB-INF/jsp/dashboard_main.jsp70
-rw-r--r--src/main/webapp/css/dashboard_main.css6
6 files changed, 225 insertions, 51 deletions
diff --git a/src/main/java/com/android/vts/api/UserFavoriteRestServlet.java b/src/main/java/com/android/vts/api/UserFavoriteRestServlet.java
index f863051..bbb95da 100644
--- a/src/main/java/com/android/vts/api/UserFavoriteRestServlet.java
+++ b/src/main/java/com/android/vts/api/UserFavoriteRestServlet.java
@@ -25,11 +25,11 @@ import com.google.appengine.api.datastore.EntityNotFoundException;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;
import com.google.appengine.api.datastore.Query;
-import com.google.appengine.api.datastore.Transaction;
import com.google.appengine.api.datastore.Query.CompositeFilterOperator;
import com.google.appengine.api.datastore.Query.Filter;
import com.google.appengine.api.datastore.Query.FilterOperator;
import com.google.appengine.api.datastore.Query.FilterPredicate;
+import com.google.appengine.api.datastore.Transaction;
import com.google.appengine.api.users.User;
import com.google.appengine.api.users.UserService;
import com.google.appengine.api.users.UserServiceFactory;
@@ -50,40 +50,37 @@ public class UserFavoriteRestServlet extends HttpServlet {
Logger.getLogger(UserFavoriteRestServlet.class.getName());
/**
- * Add a test to the user's favorites.
+ * Add a new favorite entity.
+ *
+ * @param user The user for which to add a favorite.
+ * @param test The name of the test.
+ * @param muteNotifications True if the subscriber has muted notifications, false otherwise.
+ * @param response The servlet response object.
+ * @return a json object with the generated key to the new favorite entity.
+ * @throws IOException
*/
- @Override
- public void doPost(HttpServletRequest request, HttpServletResponse response)
+ private static JsonObject addFavorite(
+ User user, String test, boolean muteNotifications, HttpServletResponse response)
throws IOException {
- UserService userService = UserServiceFactory.getUserService();
- User currentUser = userService.getCurrentUser();
DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
-
- // Retrieve the added tests from the request.
- String test = request.getPathInfo();
- if (test == null) {
- response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
- return;
- }
- if (test.startsWith("/")) {
- test = test.substring(1);
- }
Key addedTestKey = KeyFactory.createKey(TestEntity.KIND, test);
// Filter the tests that exist from the set of tests to add
try {
datastore.get(addedTestKey);
} catch (EntityNotFoundException e) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
- return;
+ return null;
}
Filter userFilter =
- new FilterPredicate(UserFavoriteEntity.USER, FilterOperator.EQUAL, currentUser);
- Filter testFilter = new FilterPredicate(
- UserFavoriteEntity.TEST_KEY, FilterOperator.EQUAL, addedTestKey);
- Query q = new Query(UserFavoriteEntity.KIND)
- .setFilter(CompositeFilterOperator.and(userFilter, testFilter))
- .setKeysOnly();
+ new FilterPredicate(UserFavoriteEntity.USER, FilterOperator.EQUAL, user);
+ Filter testFilter =
+ new FilterPredicate(
+ UserFavoriteEntity.TEST_KEY, FilterOperator.EQUAL, addedTestKey);
+ Query q =
+ new Query(UserFavoriteEntity.KIND)
+ .setFilter(CompositeFilterOperator.and(userFilter, testFilter))
+ .setKeysOnly();
Key favoriteKey = null;
@@ -94,7 +91,8 @@ public class UserFavoriteRestServlet extends HttpServlet {
break;
}
if (favoriteKey == null) {
- UserFavoriteEntity favorite = new UserFavoriteEntity(currentUser, addedTestKey);
+ UserFavoriteEntity favorite =
+ new UserFavoriteEntity(user, addedTestKey, muteNotifications);
Entity entity = favorite.toEntity();
datastore.put(entity);
favoriteKey = entity.getKey();
@@ -102,24 +100,95 @@ public class UserFavoriteRestServlet extends HttpServlet {
txn.commit();
} finally {
if (txn.isActive()) {
- logger.log(Level.WARNING,
+ logger.log(
+ Level.WARNING,
"Transaction rollback forced for favorite creation: " + test);
txn.rollback();
}
}
-
- response.setContentType("application/json");
- PrintWriter writer = response.getWriter();
JsonObject json = new JsonObject();
json.add("key", new JsonPrimitive(KeyFactory.keyToString(favoriteKey)));
- writer.print(new Gson().toJson(json));
- writer.flush();
- response.setStatus(HttpServletResponse.SC_OK);
+ return json;
}
/**
- * Remove a test from the user's favorites.
+ * @param user The user for which to add a favorite.
+ * @param favoriteKey The database key to the favorite entity to update.
+ * @param muteNotifications True if the subscriber has muted notifications, false otherwise.
+ * @param response The servlet response object.
+ * @return a json object with the generated key to the new favorite entity.
+ * @throws IOException
*/
+ private static JsonObject updateFavorite(
+ User user, Key favoriteKey, boolean muteNotifications, HttpServletResponse response)
+ throws IOException {
+ DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
+ Entity favoriteEntity;
+ try {
+ favoriteEntity = datastore.get(favoriteKey);
+ } catch (EntityNotFoundException e) {
+ response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+ return null;
+ }
+ UserFavoriteEntity favorite = UserFavoriteEntity.fromEntity(favoriteEntity);
+ if (favorite.user.getUserId() == user.getUserId()) {
+ response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+ return null;
+ }
+ if (favorite.muteNotifications != muteNotifications) {
+ Transaction txn = datastore.beginTransaction();
+ try {
+ favorite.muteNotifications = muteNotifications;
+ datastore.put(favorite.toEntity());
+ txn.commit();
+ } finally {
+ if (txn.isActive()) {
+ logger.log(
+ Level.WARNING,
+ "Transaction rollback forced for favorite update: " + favoriteKey);
+ txn.rollback();
+ }
+ }
+ }
+ JsonObject json = new JsonObject();
+ json.add("key", new JsonPrimitive(KeyFactory.keyToString(favoriteKey)));
+ return json;
+ }
+
+ /** Add a test to the user's favorites. */
+ @Override
+ public void doPost(HttpServletRequest request, HttpServletResponse response)
+ throws IOException {
+ UserService userService = UserServiceFactory.getUserService();
+ User currentUser = userService.getCurrentUser();
+
+ boolean muteNotifications = false;
+ if (request.getParameter(UserFavoriteEntity.MUTE_NOTIFICATIONS) != null) {
+ muteNotifications =
+ Boolean.parseBoolean(
+ request.getParameter(UserFavoriteEntity.MUTE_NOTIFICATIONS));
+ }
+
+ String userFavoritesKeyString = request.getParameter("userFavoritesKey");
+ String testName = request.getParameter("testName");
+
+ JsonObject returnData = null;
+ if (userFavoritesKeyString != null) {
+ Key userFavoritesKey = KeyFactory.stringToKey(userFavoritesKeyString);
+ returnData = updateFavorite(currentUser, userFavoritesKey, muteNotifications, response);
+ } else if (testName != null) {
+ returnData = addFavorite(currentUser, testName, muteNotifications, response);
+ }
+
+ if (returnData != null) {
+ response.setContentType("application/json");
+ PrintWriter writer = response.getWriter();
+ writer.print(new Gson().toJson(returnData));
+ writer.flush();
+ }
+ }
+
+ /** Remove a test from the user's favorites. */
@Override
public void doDelete(HttpServletRequest request, HttpServletResponse response)
throws IOException {
diff --git a/src/main/java/com/android/vts/entity/UserFavoriteEntity.java b/src/main/java/com/android/vts/entity/UserFavoriteEntity.java
index a2820a6..132d9b4 100644
--- a/src/main/java/com/android/vts/entity/UserFavoriteEntity.java
+++ b/src/main/java/com/android/vts/entity/UserFavoriteEntity.java
@@ -31,26 +31,50 @@ public class UserFavoriteEntity implements DashboardEntity {
// Property keys
public static final String USER = "user";
public static final String TEST_KEY = "testKey";
+ public static final String MUTE_NOTIFICATIONS = "muteNotifications";
+ private final Key key;
public final User user;
public final Key testKey;
+ public boolean muteNotifications;
/**
* Create a user favorite relationship.
*
+ * @param key The key of the entity in the database.
* @param user The User object for the subscribing user.
* @param testKey The key of the TestEntity object describing the test.
+ * @param muteNotifications True if the subscriber has muted notifications, false otherwise.
*/
- public UserFavoriteEntity(User user, Key testKey) {
+ private UserFavoriteEntity(Key key, User user, Key testKey, boolean muteNotifications) {
+ this.key = key;
this.user = user;
this.testKey = testKey;
+ this.muteNotifications = muteNotifications;
+ }
+
+ /**
+ * Create a user favorite relationship.
+ *
+ * @param user The User object for the subscribing user.
+ * @param testKey The key of the TestEntity object describing the test.
+ * @param muteNotifications True if the subscriber has muted notifications, false otherwise.
+ */
+ public UserFavoriteEntity(User user, Key testKey, boolean muteNotifications) {
+ this(null, user, testKey, muteNotifications);
}
@Override
public Entity toEntity() {
- Entity favoriteEntity = new Entity(KIND);
+ Entity favoriteEntity;
+ if (this.key != null) {
+ favoriteEntity = new Entity(key);
+ } else {
+ favoriteEntity = new Entity(KIND);
+ }
favoriteEntity.setProperty(USER, this.user);
favoriteEntity.setProperty(TEST_KEY, this.testKey);
+ favoriteEntity.setProperty(MUTE_NOTIFICATIONS, this.muteNotifications);
return favoriteEntity;
}
@@ -69,7 +93,11 @@ public class UserFavoriteEntity implements DashboardEntity {
try {
User user = (User) e.getProperty(USER);
Key testKey = (Key) e.getProperty(TEST_KEY);
- return new UserFavoriteEntity(user, testKey);
+ boolean muteNotifications = false;
+ if (e.hasProperty(MUTE_NOTIFICATIONS)) {
+ muteNotifications = (boolean) e.getProperty(MUTE_NOTIFICATIONS);
+ }
+ return new UserFavoriteEntity(e.getKey(), user, testKey, muteNotifications);
} catch (ClassCastException exception) {
// Invalid cast
logger.log(Level.WARNING, "Error parsing user favorite entity.", exception);
diff --git a/src/main/java/com/android/vts/servlet/DashboardMainServlet.java b/src/main/java/com/android/vts/servlet/DashboardMainServlet.java
index e0eded1..17b9b38 100644
--- a/src/main/java/com/android/vts/servlet/DashboardMainServlet.java
+++ b/src/main/java/com/android/vts/servlet/DashboardMainServlet.java
@@ -74,6 +74,7 @@ public class DashboardMainServlet extends BaseServlet {
private final Key testKey;
private final int passCount;
private final int failCount;
+ private boolean muteNotifications;
/**
* Test display constructor.
@@ -86,6 +87,7 @@ public class DashboardMainServlet extends BaseServlet {
this.testKey = testKey;
this.passCount = passCount;
this.failCount = failCount;
+ this.muteNotifications = false;
}
/**
@@ -115,6 +117,20 @@ public class DashboardMainServlet extends BaseServlet {
return this.failCount;
}
+ /**
+ * Get the notification mute status.
+ *
+ * @return True if the subscriber has muted notifications, false otherwise.
+ */
+ public boolean getMuteNotifications() {
+ return this.muteNotifications;
+ }
+
+ /** Set the notification mute status. */
+ public void setMuteNotifications(boolean muteNotifications) {
+ this.muteNotifications = muteNotifications;
+ }
+
@Override
public int compareTo(TestDisplay test) {
return this.testKey.getName().compareTo(test.getName());
@@ -199,14 +215,17 @@ public class DashboardMainServlet extends BaseServlet {
UserFavoriteEntity.USER, FilterOperator.EQUAL, currentUser);
query = new Query(UserFavoriteEntity.KIND).setFilter(userFilter);
- for (Entity favorite : datastore.prepare(query).asIterable()) {
- Key testKey = (Key) favorite.getProperty(UserFavoriteEntity.TEST_KEY);
+ for (Entity favoriteEntity : datastore.prepare(query).asIterable()) {
+ UserFavoriteEntity favorite = UserFavoriteEntity.fromEntity(favoriteEntity);
+ Key testKey = favorite.testKey;
if (!testMap.containsKey(testKey)) {
continue;
}
- displayedTests.add(testMap.get(testKey));
+ TestDisplay display = testMap.get(testKey);
+ display.setMuteNotifications(favorite.muteNotifications);
+ displayedTests.add(display);
subscriptionMap.put(
- testKey.getName(), KeyFactory.keyToString(favorite.getKey()));
+ testKey.getName(), KeyFactory.keyToString(favoriteEntity.getKey()));
}
}
header = FAVORITES_HEADER;
diff --git a/src/main/java/com/android/vts/util/EmailHelper.java b/src/main/java/com/android/vts/util/EmailHelper.java
index dd14d19..1277306 100644
--- a/src/main/java/com/android/vts/util/EmailHelper.java
+++ b/src/main/java/com/android/vts/util/EmailHelper.java
@@ -65,8 +65,10 @@ public class EmailHelper {
}
for (Entity favorite : datastore.prepare(favoritesQuery).asIterable()) {
UserFavoriteEntity favoriteEntity = UserFavoriteEntity.fromEntity(favorite);
- if (favoriteEntity != null && favoriteEntity.user != null
- && favoriteEntity.user.getEmail().endsWith(EMAIL_DOMAIN)) {
+ if (favoriteEntity != null
+ && favoriteEntity.user != null
+ && favoriteEntity.user.getEmail().endsWith(EMAIL_DOMAIN)
+ && !favoriteEntity.muteNotifications) {
emailSet.add(favoriteEntity.user.getEmail());
}
}
diff --git a/src/main/webapp/WEB-INF/jsp/dashboard_main.jsp b/src/main/webapp/WEB-INF/jsp/dashboard_main.jsp
index e0e8f57..796e295 100644
--- a/src/main/webapp/WEB-INF/jsp/dashboard_main.jsp
+++ b/src/main/webapp/WEB-INF/jsp/dashboard_main.jsp
@@ -35,7 +35,7 @@
return;
}
$('#add-button').addClass('disabled');
- $.post('/api/favorites/' + test).then(function(data) {
+ $.post('/api/favorites', { testName: test}).then(function(data) {
if (!data.key) {
return;
}
@@ -49,12 +49,24 @@
var span = $('<span class="entry valign"></span>').text(test);
span.appendTo(div);
a.appendTo(wrapper);
- var clear = $('<a class="col s1 btn-flat center"></a>');
- clear.addClass('clear-button');
+
+ var btnContainer = $('<div class="col s1 center btn-container"></div>');
+ var silence = $('<a class="col s6 btn-flat notification-button active"></a>');
+ silence.append('<i class="material-icons">notifications_active</i>');
+ silence.attr('test', test);
+ silence.attr('title', 'Disable notifications');
+ silence.appendTo(btnContainer);
+ silence.click(toggleNotifications);
+
+ var clear = $('<a class="col s6 btn-flat remove-button"></a>');
clear.append('<i class="material-icons">clear</i>');
clear.attr('test', test);
- clear.appendTo(wrapper);
+ clear.attr('title', 'Remove favorite');
+ clear.appendTo(btnContainer);
clear.click(removeFavorite);
+
+ btnContainer.appendTo(wrapper);
+
wrapper.prependTo('#options').hide()
.slideDown(150);
$('#input-box').val(null);
@@ -64,6 +76,40 @@
});
}
+ var toggleNotifications = function() {
+ var self = $(this);
+ if (self.hasClass('disabled')) {
+ return;
+ }
+ self.addClass('disabled');
+ var test = self.attr('test');
+ if (!(test in subscriptionMap)) {
+ return;
+ }
+ var muteStatus = self.hasClass('active');
+ var element = self;
+ $.post('/api/favorites', { userFavoritesKey: subscriptionMap[test], muteNotifications: muteStatus}).then(function(data) {
+ element = self.clone();
+ if (element.hasClass('active')) {
+ element.find('i').text('notifications_off')
+ element.removeClass('active');
+ element.addClass('inactive');
+ element.attr('title', 'Enable notifications');
+ } else {
+ element.find('i').text('notifications_active')
+ element.removeClass('inactive');
+ element.addClass('active');
+ element.attr('title', 'Disable notifications');
+ }
+ element.click(toggleNotifications);
+ self.replaceWith(function() {
+ return element;
+ });
+ }).always(function() {
+ element.removeClass('disabled');
+ });
+ }
+
var removeFavorite = function() {
var self = $(this);
if (self.hasClass('disabled')) {
@@ -81,7 +127,7 @@
self.removeClass('disabled');
}).then(function() {
delete subscriptionMap[test];
- self.parent().slideUp(150, function() {
+ self.parent().parent().slideUp(150, function() {
self.remove();
});
});
@@ -107,7 +153,8 @@
}
});
- $('.clear-button').click(removeFavorite);
+ $('.remove-button').click(removeFavorite);
+ $('.notification-button').click(toggleNotifications);
$('#add-button').click(addFavorite);
});
</script>
@@ -154,9 +201,14 @@
</div>
</a>
<c:if test='${not showAll}'>
- <a class='col s1 btn-flat center clear-button' test='${test.name}'>
- <i class='material-icons'>clear</i>
- </a>
+ <div class='col s1 center btn-container'>
+ <a class='col s6 btn-flat notification-button ${test.muteNotifications ? "inactive" : "active"}' test='${test.name}' title='${test.muteNotifications ? "Enable" : "Disable"} notifications'>
+ <i class='material-icons'>notifications_${test.muteNotifications ? "off" : "active"}</i>
+ </a>
+ <a class='col s6 btn-flat remove-button' test='${test.name}' title='Remove favorite'>
+ <i class='material-icons'>clear</i>
+ </a>
+ </div>
</c:if>
</div>
</c:forEach>
diff --git a/src/main/webapp/css/dashboard_main.css b/src/main/webapp/css/dashboard_main.css
index e6e899f..0db4c94 100644
--- a/src/main/webapp/css/dashboard_main.css
+++ b/src/main/webapp/css/dashboard_main.css
@@ -27,12 +27,16 @@
height: 61px;
}
-.btn-flat.clear-button {
+.btn-container > .btn-flat {
margin-top: 8px;
user-select: none;
color: grey;
}
+.btn-container > .btn-flat.inactive {
+ color: lightgray;
+}
+
.row .col.card.option {
padding: 6px 15px 6px 15px;
margin: 5px 0;