PageRenderTime 73ms CodeModel.GetById 12ms RepoModel.GetById 1ms app.codeStats 0ms

/javascript/profile-package/coaches.js

https://gitlab.com/gregtyka/KhanLatest
JavaScript | 383 lines | 275 code | 69 blank | 39 comment | 14 complexity | 8435ddd433c8e53538e016e7fc274f13 MD5 | raw file
  1. /**
  2. * Namespace for logic to manage the coach list of a student.
  3. * This is rendered as a sub-page of the profile page and has access to
  4. * the Profile namespace.
  5. */
  6. var Coaches = {
  7. coachCollection: null,
  8. requestCollection: null,
  9. url: "/api/v1/user/coaches",
  10. /**
  11. * Whether or not the actor has privileges to mutate the list of coaches
  12. * for the profile viewing viewed.
  13. */
  14. isMutable: false,
  15. init: function(isMutable) {
  16. this.isMutable = !!isMutable;
  17. var template = Templates.get("profile.coaches");
  18. var context = {
  19. "profile": Profile.profile.toJSON(),
  20. "identifier": Profile.profile.get("email") ||
  21. Profile.profile.get("username"),
  22. "isMutable": this.isMutable
  23. };
  24. $("#tab-content-coaches").html(template(context));
  25. this.delegateEvents_();
  26. return $.ajax({
  27. type: "GET",
  28. url: this.url,
  29. data: Profile.getBaseRequestParams_(),
  30. dataType: "json",
  31. success: _.bind(this.onDataLoaded_, this),
  32. error: function(jqXhr) {
  33. if (jqXhr.status === 401) {
  34. // Unauthorized. There should be no UI flow to have gotten
  35. // the user into this state, so it could be they typed
  36. // in the URL manually. Just redirect them to the root.
  37. Profile.router.navigate(null, true);
  38. }
  39. }
  40. });
  41. },
  42. onDataLoaded_: function(users) {
  43. this.coachCollection = new Coaches.CoachCollection(users);
  44. // See https://github.com/documentcloud/backbone/issues/814
  45. // for why markCoachesAsSaved cannot be called in inititialize
  46. this.coachCollection.markCoachesAsSaved();
  47. new Coaches.CoachCollectionView({
  48. collection: Coaches.coachCollection,
  49. el: "#coach-list-container"
  50. }).render();
  51. },
  52. delegateEvents_: function() {
  53. $("#tab-content-coaches").on("keyup", "#coach-email",
  54. _.bind(this.onCoachEmailKeyup_, this));
  55. $("#tab-content-coaches").on("click", "#add-coach",
  56. _.bind(this.onAddCoach_, this));
  57. },
  58. onCoachEmailKeyup_: function(e) {
  59. if (e.keyCode === $.ui.keyCode.ENTER) {
  60. this.onAddCoach_();
  61. }
  62. },
  63. onAddCoach_: function() {
  64. var email = $.trim($("#coach-email").val());
  65. if (email) {
  66. Coaches.disableInput();
  67. this.coachCollection.addByEmail(email);
  68. }
  69. },
  70. disableInput: function() {
  71. $("#add-coach").addClass("disabled")
  72. .prop("disabled", true);
  73. $("#coach-email").prop("disabled", true);
  74. $(".coach-throbber").show();
  75. },
  76. enableInput: function() {
  77. $("#add-coach").removeClass("disabled")
  78. .prop("disabled", false);
  79. $("#coach-email").prop("disabled", false)
  80. .focus();
  81. $(".coach-throbber").hide();
  82. }
  83. };
  84. Coaches.CoachView = Backbone.View.extend({
  85. className: "coach-row",
  86. collection_: null,
  87. template_: null,
  88. events: {
  89. "click .controls .remove": "onRemoveCoach_",
  90. "click .controls .accept": "onAcceptCoach_",
  91. "click .controls .deny": "onDenyCoach_",
  92. "mouseenter .controls .remove": "onMouseEnterRemove_",
  93. "mouseleave .controls .remove": "onMouseLeaveRemove_"
  94. },
  95. initialize: function(options) {
  96. this.model.bind("change", this.render, this);
  97. this.collection_ = options.collection;
  98. this.template_ = Templates.get("profile.coach");
  99. },
  100. render: function() {
  101. var context = this.model.toJSON();
  102. context["isMutable"] = Coaches.isMutable &&
  103. !this.model.get("isParentOfLoggedInUser");
  104. $(this.el).html(this.template_(context));
  105. // TODO(marcia): Figure out why I need to call this..
  106. this.delegateEvents();
  107. return this;
  108. },
  109. onRemoveCoach_: function() {
  110. this.collection_.remove(this.model);
  111. },
  112. onAcceptCoach_: function() {
  113. this.model.set({
  114. isCoachingLoggedInUser: true,
  115. isRequestingToCoachLoggedInUser: false
  116. });
  117. },
  118. onDenyCoach_: function() {
  119. this.collection_.remove(this.model);
  120. },
  121. onMouseEnterRemove_: function(evt) {
  122. this.$(".controls .remove").addClass("orange");
  123. },
  124. onMouseLeaveRemove_: function(evt) {
  125. this.$(".controls .remove").removeClass("orange");
  126. }
  127. });
  128. Coaches.Coach = ProfileModel.extend({
  129. /**
  130. * Override toJSON to delete the id attribute since it is only used for
  131. * client-side bookkeeping.
  132. */
  133. toJSON: function() {
  134. var json = Coaches.Coach.__super__.toJSON.call(this);
  135. delete json["id"];
  136. return json;
  137. }
  138. });
  139. Coaches.CoachCollection = Backbone.Collection.extend({
  140. model: Coaches.Coach,
  141. initialize: function() {
  142. this.bind("add", this.save, this);
  143. this.bind("remove", this.save, this);
  144. this.bind("change", this.save, this);
  145. },
  146. comparator: function(model) {
  147. // TODO(marcia): Once we upgrade to Backbone 0.9,
  148. // we could define this as a sort instead of a sortBy
  149. // http://documentcloud.github.com/backbone/#Collection-comparator
  150. var isCoaching = model.get("isCoachingLoggedInUser"),
  151. email = model.get("email").toLowerCase();
  152. // Show pending requests before coaches,
  153. // then order alphabetically
  154. return (isCoaching ? "b" : "a") + " " + email;
  155. },
  156. findByEmail: function(email) {
  157. return this.find(function(model) {
  158. return model.get("email") === email;
  159. });
  160. },
  161. addByEmail: function(email) {
  162. var attrs = {
  163. email: email,
  164. isCoachingLoggedInUser: true
  165. };
  166. var model = this.findByEmail(email);
  167. if (model) {
  168. if (model.get("isCoachingLoggedInUser")) {
  169. // Already a coach
  170. var message = email + " is already your coach.";
  171. this.trigger("showError", message);
  172. } else {
  173. // Ã…ccept the pending coach request
  174. model.set({isCoachingLoggedInUser: true});
  175. }
  176. } else {
  177. // Add the coach to the collection
  178. this.add(attrs);
  179. }
  180. },
  181. save: function() {
  182. this.debouncedSave_();
  183. },
  184. debouncedSave_: _.debounce(function() {
  185. var options = {
  186. url: Coaches.url,
  187. contentType: "application/json",
  188. success: _.bind(this.onSaveSuccess_, this),
  189. error: _.bind(this.onSaveError_, this)
  190. };
  191. options["data"] = JSON.stringify(this.toJSON());
  192. Backbone.sync("update", null, options);
  193. }, 750),
  194. onSaveSuccess_: function() {
  195. this.markCoachesAsSaved();
  196. this.trigger("saveSuccess");
  197. Coaches.enableInput();
  198. },
  199. onSaveError_: function() {
  200. this.removeUnsavedCoaches_();
  201. this.trigger("saveError");
  202. },
  203. increasingId: 0,
  204. /**
  205. * Mark which coach models have been saved to server,
  206. * which lets us remove un-saved / invalid coaches on error.
  207. */
  208. markCoachesAsSaved: function() {
  209. this.each(function(model) {
  210. // Backbone models without an id are considered
  211. // to be new, as in not yet saved to server.
  212. // Append an increasing number since collections cannot have
  213. // models with the same id, as of Backbone 0.9
  214. model.set({id: "marks-model-as-saved-on-server" + this.increasingId++},
  215. {silent: true});
  216. }, this);
  217. },
  218. removeUnsavedCoaches_: function() {
  219. var modelsToRemove = this.filter(function(model) {
  220. return model.isNew();
  221. });
  222. // Don't trigger saves when removing invalid coaches
  223. this.remove(modelsToRemove, {silent: true});
  224. // Trigger removal from view
  225. _.each(modelsToRemove, _.bind(function(model) {
  226. this.trigger("removeFromView", model);
  227. }, this));
  228. }
  229. });
  230. Coaches.CoachCollectionView = Backbone.View.extend({
  231. rendered_: false,
  232. onlyAddingCoaches_: true,
  233. initialize: function(options) {
  234. this.coachViews_ = [];
  235. this.collection.each(this.onAdd_, this);
  236. this.collection.bind("add", this.onAdd_, this)
  237. .bind("remove", this.onRemove_, this)
  238. .bind("removeFromView", this.onRemove_, this);
  239. this.collection.bind("add", this.handleEmptyNotification_, this)
  240. .bind("remove", this.handleEmptyNotification_, this)
  241. .bind("removeFromView", this.handleEmptyNotification_, this);
  242. this.collection.bind("saveSuccess", this.onSaveSuccess_, this)
  243. .bind("saveError", this.onSaveError_, this)
  244. .bind("showError", this.showError_, this);
  245. },
  246. onSaveSuccess_: function() {
  247. // Clear textfield only if we successfully added a coach,
  248. // as opposed to removing a coach.
  249. if (this.onlyAddingCoaches_) {
  250. $("#coach-email").val("");
  251. }
  252. this.onlyAddingCoaches_ = true;
  253. },
  254. onSaveError_: function() {
  255. this.showError_("We couldn't find anyone with that email.");
  256. },
  257. onAdd_: function(model) {
  258. var coachView = new Coaches.CoachView({
  259. model: model,
  260. collection: this.collection
  261. });
  262. this.coachViews_.push(coachView);
  263. if (this.rendered_) {
  264. $(this.el).prepend(coachView.render().el);
  265. }
  266. },
  267. onRemove_: function(model) {
  268. var viewToRemove = _.find(this.coachViews_, function(view) {
  269. return view.model === model;
  270. });
  271. if (viewToRemove) {
  272. this.onlyAddingCoaches_ = false;
  273. this.coachViews_ = _.without(this.coachViews_, viewToRemove);
  274. if (this.rendered_) {
  275. $(viewToRemove.el).fadeOut(function() {
  276. viewToRemove.remove();
  277. });
  278. }
  279. }
  280. },
  281. showEmptyNotification_: function() {
  282. if (!this.emptyNotification_) {
  283. var template = Templates.get("profile.no-coaches");
  284. this.emptyNotification_ = $("<div>").addClass("empty-notification").html(template());
  285. $(this.el).append(this.emptyNotification_);
  286. }
  287. this.$(".empty-notification").show();
  288. },
  289. handleEmptyNotification_: function() {
  290. if (this.collection.isEmpty()) {
  291. this.showEmptyNotification_();
  292. } else {
  293. this.$(".empty-notification").hide();
  294. }
  295. },
  296. showError_: function(message) {
  297. $(".coaches-section .notification.error").text(message)
  298. .show()
  299. .delay(2000)
  300. .fadeOut(function() {
  301. $(this).text("");
  302. });
  303. Coaches.enableInput();
  304. },
  305. render: function() {
  306. this.rendered_ = true;
  307. $(this.el).empty();
  308. this.handleEmptyNotification_();
  309. _.each(this.coachViews_, function(view) {
  310. $(this.el).append(view.render().el);
  311. }, this);
  312. return this;
  313. }
  314. });