PageRenderTime 52ms CodeModel.GetById 15ms RepoModel.GetById 1ms app.codeStats 0ms

/javascript/profile-package/profile.js

https://bitbucket.org/lmeius/lehor
JavaScript | 965 lines | 741 code | 122 blank | 102 comment | 106 complexity | 0f12b16a8dc6c84a295c689b2ff16630 MD5 | raw file
Possible License(s): MIT, BSD-3-Clause, CC-BY-3.0, GPL-3.0
  1. /**
  2. * Code to handle the logic for the profile page.
  3. */
  4. // TODO: clean up all event listeners. This page does not remove any
  5. // event listeners when tearing down the graphs.
  6. var Profile = {
  7. version: 0,
  8. email: null, // Filled in by the template after script load.
  9. fLoadingGraph: false,
  10. fLoadedGraph: false,
  11. profile: null,
  12. /**
  13. * The root segment of the URL for the profile page for this user.
  14. * Will be of the form "/profile/<identifier>" where identifier
  15. * can be a username, or other identifier sent by the server.
  16. */
  17. profileRoot: "",
  18. /**
  19. * Whether or not we can collect sensitive information like the user's
  20. * name. Users under 13 without parental consent should not be able
  21. * to enter data.
  22. */
  23. isDataCollectible: false,
  24. /**
  25. * Overridden w profile-intro.js if necessary
  26. */
  27. showIntro_: function() {},
  28. /**
  29. * Called to initialize the profile page. Passed in with JSON information
  30. * rendered from the server. See templates/viewprofile.html for details.
  31. */
  32. init: function(json) {
  33. this.profile = new ProfileModel(json.profileData);
  34. this.profile.bind("savesuccess", this.onProfileUpdated_, this);
  35. var root = json.profileRoot;
  36. if (window.location.pathname.indexOf("@") > -1) {
  37. // Note the path should be encoded so that @ turns to %40. However,
  38. // there is a bug (https://bugs.webkit.org/show_bug.cgi?id=30225)
  39. // that makes Safari always return the decoded part. Also, if
  40. // the user manually types in an @ sign, it will be returned
  41. // decoded. So we need to be robust to this.
  42. root = decodeURIComponent(root);
  43. }
  44. this.profileRoot = root;
  45. this.isDataCollectible = json.isDataCollectible;
  46. UserCardView.countVideos = json.countVideos;
  47. UserCardView.countExercises = json.countExercises;
  48. Profile.render();
  49. Profile.router = new Profile.TabRouter({routes: this.getRoutes_()});
  50. Backbone.history.start({
  51. pushState: true,
  52. root: this.profileRoot
  53. });
  54. Profile.showIntro_();
  55. // Remove goals from IE<=8
  56. $(".lte8 .goals-accordion-content").remove();
  57. // Init Highcharts global options
  58. Highcharts.setOptions({
  59. credits: {
  60. enabled: false
  61. },
  62. title: {
  63. text: ""
  64. },
  65. subtitle: {
  66. text: ""
  67. }
  68. });
  69. var navElementHandler = _.bind(this.onNavigationElementClicked_, this);
  70. // Delegate clicks for tab navigation
  71. $(".profile-navigation .vertical-tab-list").delegate("a",
  72. "click", navElementHandler);
  73. // Delegate clicks for vital statistics time period navigation
  74. $("#tab-content-vital-statistics").delegate(".graph-date-picker a",
  75. "click", navElementHandler);
  76. $("#tab-content-goals").delegate(".graph-picker .type a",
  77. "click", navElementHandler);
  78. // Delegate clicks for recent badge-related activity
  79. $(".achievement .ach-text").delegate("a", "click", function(event) {
  80. if (!event.metaKey) {
  81. event.preventDefault();
  82. Profile.router.navigate("/achievements", true);
  83. $("#achievement-list ul li#category-" + $(this).data("category")).click();
  84. }
  85. });
  86. },
  87. /**
  88. * All the tabs that you could encounter on the profile page.
  89. */
  90. subRoutes: {
  91. "/achievements": "showAchievements",
  92. "/goals/:type": "showGoals",
  93. "/goals": "showGoals",
  94. "/vital-statistics": "showVitalStatistics",
  95. "/vital-statistics/exercise-problems/:exercise": "showExerciseProblems",
  96. "/vital-statistics/:graph/:timePeriod": "showVitalStatisticsForTimePeriod",
  97. "/vital-statistics/:graph": "showVitalStatistics",
  98. "": "showDefault",
  99. // If the user types /profile/username/ with a trailing slash
  100. // it should work, too
  101. "/": "showDefault",
  102. // A minor hack to ensure that if the user navigates to /profile without
  103. // her username, it still shows the default profile screen. Note that
  104. // these routes aren't relative to the root URL, but will still work.
  105. "/profile": "showDefault",
  106. "/profile/": "showDefault"
  107. },
  108. /**
  109. * Generate routes hash to be used by Profile.router
  110. */
  111. getRoutes_: function() {
  112. return this.subRoutes;
  113. },
  114. /**
  115. * Handle a change to the profile root.
  116. */
  117. onProfileUpdated_: function() {
  118. var username = this.profile.get("username");
  119. if (username && Profile.profileRoot != ("/profile/" + username)) {
  120. // Profile root changed - we need to reload the page since
  121. // Backbone.router isn't happy when the root changes.
  122. window.location.replace("/profile/" + username);
  123. }
  124. },
  125. TabRouter: Backbone.Router.extend({
  126. showDefault: function() {
  127. Profile.populateActivity().then(function() {
  128. // Pre-fetch badges, after the activity has been loaded, since
  129. // they're needed to edit the display-case.
  130. if (Profile.profile.isEditable()) {
  131. Profile.populateAchievements();
  132. }
  133. });
  134. $("#tab-content-user-profile").show().siblings().hide();
  135. this.activateRelatedTab($("#tab-content-user-profile").attr("rel"));
  136. this.updateTitleBreadcrumbs();
  137. },
  138. showVitalStatistics: function(graph, exercise, timePeriod) {
  139. var graph = graph || "activity",
  140. exercise = exercise || "addition_1",
  141. timePeriod = timePeriod || "last-week",
  142. emailEncoded = encodeURIComponent(USER_EMAIL),
  143. hrefLookup = {
  144. "activity": "/profile/graph/activity?student_email=" + emailEncoded,
  145. "focus": "/profile/graph/focus?student_email=" + emailEncoded,
  146. "exercise-progress-over-time": "/profile/graph/exercisesovertime?student_email=" + emailEncoded,
  147. "exercise-progress": "/api/v1/user/exercises?email=" + emailEncoded,
  148. "exercise-problems": "/profile/graph/exerciseproblems?" +
  149. "exercise_name=" + exercise +
  150. "&" + "student_email=" + emailEncoded
  151. },
  152. timePeriodLookup = {
  153. "today": "&dt_start=today",
  154. "yesterday": "&dt_start=yesterday",
  155. "last-week": "&dt_start=lastweek&dt_end=today",
  156. "last-month": "&dt_start=lastmonth&dt_end=today"
  157. },
  158. timeURLParameter = timePeriod ? timePeriodLookup[timePeriod] : "",
  159. href = hrefLookup[graph] + timeURLParameter;
  160. // Known bug: the wrong graph-date-picker item is selected when
  161. // server man decides to show 30 days instead of the default 7.
  162. // See redirect_for_more_data in util_profile.py for more on this tragedy.
  163. $("#tab-content-vital-statistics").show()
  164. .find(".vital-statistics-description ." + graph).show()
  165. .find(".graph-date-picker .tabrow .last-week").addClass("selected")
  166. .siblings().removeClass("selected").end()
  167. .end()
  168. .siblings().hide().end()
  169. .end().siblings().hide();
  170. this.activateRelatedTab($("#tab-content-vital-statistics").attr("rel") + " " + graph);
  171. var prettyGraphName = graph.replace(/-/gi, " ");
  172. if (graph == "exercise-problems") {
  173. var prettyExName = exercise.replace(/_/gi, " ");
  174. this.updateTitleBreadcrumbs([prettyGraphName, prettyExName]);
  175. }
  176. else {
  177. this.updateTitleBreadcrumbs([prettyGraphName]);
  178. }
  179. if (Profile.profile.get("email")) {
  180. // If we have access to the profiled person's email, load real data.
  181. Profile.loadGraph(href);
  182. } else {
  183. // Otherwise, show some fake stuff.
  184. Profile.renderFakeGraph(graph, timePeriod);
  185. }
  186. },
  187. showExerciseProblems: function(exercise) {
  188. this.showVitalStatistics("exercise-problems", exercise);
  189. },
  190. showVitalStatisticsForTimePeriod: function(graph, timePeriod) {
  191. this.showVitalStatistics(graph, null, timePeriod);
  192. $(".vital-statistics-description ." + graph + " ." + timePeriod).addClass("selected")
  193. .siblings().removeClass("selected");
  194. },
  195. showAchievements: function() {
  196. Profile.populateAchievements();
  197. $("#tab-content-achievements").show()
  198. .siblings().hide();
  199. this.activateRelatedTab($("#tab-content-achievements").attr("rel"));
  200. this.updateTitleBreadcrumbs(["Achievements"]);
  201. },
  202. showGoals: function(type) {
  203. type = type || "current";
  204. Profile.populateGoals();
  205. GoalProfileViewsCollection.showGoalType(type);
  206. $("#tab-content-goals").show()
  207. .siblings().hide();
  208. this.activateRelatedTab($("#tab-content-goals").attr("rel"));
  209. this.updateTitleBreadcrumbs(["Goals"]);
  210. },
  211. activateRelatedTab: function(rel) {
  212. $(".profile-navigation .vertical-tab-list a").removeClass("active-tab");
  213. $("a[rel$='" + rel + "']").addClass("active-tab");
  214. },
  215. /**
  216. * Updates the title of the profile page to show breadcrumbs
  217. * based on the parts in the specified array. Will always pre-pend the profile
  218. * nickname.
  219. * @param {Array.<string>} parts A list of strings that will be HTML-escaped
  220. * to be the breadcrumbs.
  221. */
  222. updateTitleBreadcrumbs: function(parts) {
  223. $(".profile-notification").hide();
  224. var sheetTitle = $(".profile-sheet-title");
  225. if (parts && parts.length) {
  226. var rootCrumb = Profile.profile.get("nickname") || "Profile";
  227. parts.unshift(rootCrumb);
  228. sheetTitle.text(parts.join(" Âť ")).show();
  229. if (!Profile.profile.get("email")) {
  230. $(".profile-notification").show();
  231. }
  232. } else {
  233. sheetTitle.text("").hide();
  234. }
  235. }
  236. }),
  237. /**
  238. * Navigate the router appropriately,
  239. * either to change profile sheets or vital-stats time periods.
  240. */
  241. onNavigationElementClicked_: function(e) {
  242. // TODO: Make sure middle-click + windows control-click Do The Right Thing
  243. // in a reusable way
  244. if (!e.metaKey) {
  245. e.preventDefault();
  246. var route = $(e.currentTarget).attr("href");
  247. // The navigation elements have the profileRoot in the href, but
  248. // Router.navigate should be relative to the root.
  249. if (route.indexOf(this.profileRoot) === 0) {
  250. route = route.substring(this.profileRoot.length);
  251. }
  252. Profile.router.navigate(route, true);
  253. }
  254. },
  255. loadGraph: function(href) {
  256. var apiCallbacksTable = {
  257. "/api/v1/user/exercises": this.renderExercisesTable,
  258. "/api/v1/exercises": this.renderFakeExercisesTable_
  259. };
  260. if (!href) {
  261. return;
  262. }
  263. if (this.fLoadingGraph) {
  264. setTimeout(function() {Profile.loadGraph(href);}, 200);
  265. return;
  266. }
  267. this.fLoadingGraph = true;
  268. this.fLoadedGraph = false;
  269. var apiCallback = null;
  270. for (var uri in apiCallbacksTable) {
  271. if (href.indexOf(uri) > -1) {
  272. apiCallback = apiCallbacksTable[uri];
  273. }
  274. }
  275. $.ajax({
  276. type: "GET",
  277. url: Timezone.append_tz_offset_query_param(href),
  278. data: {},
  279. dataType: apiCallback ? "json" : "html",
  280. success: function(data) {
  281. Profile.finishLoadGraph(data, apiCallback);
  282. },
  283. error: function() {
  284. Profile.finishLoadGraphError();
  285. }
  286. });
  287. $("#graph-content").html("");
  288. this.showGraphThrobber(true);
  289. },
  290. finishLoadGraph: function(data, apiCallback) {
  291. this.fLoadingGraph = false;
  292. this.showGraphThrobber(false);
  293. var start = (new Date).getTime();
  294. if (apiCallback) {
  295. apiCallback(data);
  296. } else {
  297. $("#graph-content").html(data);
  298. }
  299. var diff = (new Date).getTime() - start;
  300. KAConsole.log("API call rendered in " + diff + " ms.");
  301. this.fLoadedGraph = true;
  302. },
  303. finishLoadGraphError: function() {
  304. this.fLoadingGraph = false;
  305. this.showGraphThrobber(false);
  306. Profile.showNotification("error-graph");
  307. },
  308. renderFakeGraph: function(graphName, timePeriod) {
  309. if (graphName === "activity") {
  310. ActivityGraph.render(null, timePeriod);
  311. Profile.fLoadedGraph = true;
  312. } else if (graphName === "focus") {
  313. FocusGraph.render();
  314. Profile.fLoadedGraph = true;
  315. } else if (graphName === "exercise-progress") {
  316. Profile.loadGraph("/api/v1/exercises");
  317. } else {
  318. ExerciseGraphOverTime.render();
  319. Profile.fLoadedGraph = true;
  320. }
  321. },
  322. generateFakeExerciseTableData_: function(exerciseData) {
  323. // Generate some vaguely plausible exercise progress data
  324. return _.map(exerciseData, function(exerciseModel) {
  325. // See models.py -- h_position corresponds to the node's vertical position
  326. var position = exerciseModel["h_position"],
  327. totalDone = 0,
  328. states = {},
  329. rand = Math.random();
  330. if (position < 10) {
  331. if (Math.random() < 0.9) {
  332. totalDone = 1;
  333. if (rand < 0.5) {
  334. states["proficient"] = true;
  335. } else if (rand < 0.7) {
  336. states["reviewing"] = true;
  337. }
  338. }
  339. } else if (position < 17) {
  340. if (Math.random() < 0.6) {
  341. totalDone = 1;
  342. if (rand < 0.4) {
  343. states["proficient"] = true;
  344. } else if (rand < 0.7) {
  345. states["reviewing"] = true;
  346. } else if (rand < 0.75) {
  347. states["struggling"] = true;
  348. }
  349. }
  350. } else {
  351. if (Math.random() < 0.1) {
  352. totalDone = 1;
  353. if (rand < 0.2) {
  354. states["proficient"] = true;
  355. } else if (rand < 0.5) {
  356. states["struggling"] = true;
  357. }
  358. }
  359. }
  360. return {
  361. "exercise_model": exerciseModel,
  362. "total_done": totalDone,
  363. "exercise_states": states
  364. };
  365. });
  366. },
  367. renderFakeExercisesTable_: function(exerciseData) {
  368. // Do nothing if the user switches sheets before /api/v1/exercises responds
  369. // (The other fake sheets are rendered randomly client-side)
  370. if (Profile.fLoadedGraph) {
  371. return;
  372. }
  373. var fakeData = Profile.generateFakeExerciseTableData_(exerciseData);
  374. Profile.renderExercisesTable(fakeData, false);
  375. $("#module-progress").addClass("empty-chart");
  376. },
  377. /**
  378. * Renders the exercise blocks given the JSON blob about the exercises.
  379. */
  380. renderExercisesTable: function(data, bindEvents) {
  381. var templateContext = [],
  382. bindEvents = (bindEvents === undefined) ? true : bindEvents,
  383. isEmpty = true,
  384. exerciseModels = [];
  385. for (var i = 0, exercise; exercise = data[i]; i++) {
  386. var stat = "Not started";
  387. var color = "";
  388. var states = exercise["exercise_states"];
  389. var totalDone = exercise["total_done"];
  390. if (totalDone > 0) {
  391. isEmpty = false;
  392. }
  393. if (states["reviewing"]) {
  394. stat = "Review";
  395. color = "review light";
  396. } else if (states["proficient"]) {
  397. // TODO: handle implicit proficiency - is that data in the API?
  398. // (due to proficiency in a more advanced module)
  399. stat = "Proficient";
  400. color = "proficient";
  401. } else if (states["struggling"]) {
  402. stat = "Struggling";
  403. color = "struggling";
  404. } else if (totalDone > 0) {
  405. stat = "Started";
  406. color = "started";
  407. }
  408. if (color) {
  409. color = color + " action-gradient seethrough";
  410. } else {
  411. color = "transparent";
  412. }
  413. var model = exercise["exercise_model"];
  414. exerciseModels.push(model);
  415. templateContext.push({
  416. "name": model["name"],
  417. "color": color,
  418. "status": stat,
  419. "shortName": model["short_display_name"] || model["display_name"],
  420. "displayName": model["display_name"],
  421. "progress": Math.floor(exercise["progress"] * 100) + "%",
  422. "totalDone": totalDone
  423. });
  424. }
  425. if (isEmpty) {
  426. Profile.renderFakeExercisesTable_(exerciseModels);
  427. Profile.showNotification("empty-graph");
  428. return;
  429. }
  430. var template = Templates.get("profile.exercise_progress");
  431. $("#graph-content").html(template({ "exercises": templateContext }));
  432. if (bindEvents) {
  433. Profile.hoverContent($("#module-progress .student-module-status"));
  434. $("#module-progress .student-module-status").click(function(e) {
  435. $("#info-hover-container").hide();
  436. // Extract the name from the ID, which has been prefixed.
  437. var exerciseName = this.id.substring("exercise-".length);
  438. Profile.router.navigate("/vital-statistics/exercise-problems/" + exerciseName, true);
  439. });
  440. }
  441. },
  442. showGraphThrobber: function(fVisible) {
  443. if (fVisible) {
  444. $("#graph-progress-bar").progressbar({value: 100}).slideDown("fast");
  445. } else {
  446. $("#graph-progress-bar").slideUp("fast", function() {
  447. $(this).hide();
  448. });
  449. }
  450. },
  451. /**
  452. * Show a profile notification
  453. * Expects the class name of the div to show, such as "error-graph"
  454. */
  455. showNotification: function(className) {
  456. var jel = $(".profile-notification").removeClass("uncover-nav");
  457. if (className === "empty-graph") {
  458. jel.addClass("uncover-nav");
  459. }
  460. jel.show()
  461. .find("." + className).show()
  462. .siblings().hide();
  463. },
  464. hoverContent: function(elements, containerSelector) {
  465. var lastHoverTime,
  466. mouseX,
  467. mouseY;
  468. containerSelector = containerSelector || "#graph-content";
  469. elements.hover(
  470. function(e) {
  471. var hoverTime = +(new Date()),
  472. el = this;
  473. lastHoverTime = hoverTime;
  474. mouseX = e.pageX;
  475. mouseY = e.pageY;
  476. setTimeout(function() {
  477. if (hoverTime !== lastHoverTime) {
  478. return;
  479. }
  480. var hoverData = $(el).children(".hover-data"),
  481. html = $.trim(hoverData.html());
  482. if (html) {
  483. var jelContainer = $(containerSelector),
  484. leftMax = jelContainer.offset().left + jelContainer.width() - 150,
  485. left = Math.min(mouseX + 15, leftMax),
  486. jHoverEl = $("#info-hover-container");
  487. if (jHoverEl.length === 0) {
  488. jHoverEl = $('<div id="info-hover-container"></div>').appendTo("body");
  489. }
  490. jHoverEl
  491. .html(html)
  492. .css({left: left, top: mouseY + 5})
  493. .show();
  494. }
  495. }, 100);
  496. },
  497. function(e) {
  498. lastHoverTime = null;
  499. $("#info-hover-container").hide();
  500. }
  501. );
  502. },
  503. AddObjectiveHover: function(element) {
  504. Profile.hoverContent(element.find(".objective"), "#profile-goals-content");
  505. },
  506. render: function() {
  507. var profileTemplate = Templates.get("profile.profile");
  508. Handlebars.registerHelper("graph-date-picker-wrapper", function(block) {
  509. this.graph = block.hash.graph;
  510. return block(this);
  511. });
  512. Handlebars.registerPartial("graph-date-picker", Templates.get("profile.graph-date-picker"));
  513. Handlebars.registerPartial("vital-statistics", Templates.get("profile.vital-statistics"));
  514. $("#profile-content").html(profileTemplate({
  515. profileRoot: this.profileRoot,
  516. profileData: this.profile.toJSON(),
  517. countVideos: UserCardView.countVideos,
  518. countExercises: UserCardView.countExercises
  519. }));
  520. // Show only the user card tab,
  521. // since the Backbone default route isn't triggered
  522. // when visiting khanacademy.org/profile
  523. $("#tab-content-user-profile").show().siblings().hide();
  524. Profile.populateUserCard();
  525. this.profile.bind("change:nickname", function(profile) {
  526. var nickname = profile.get("nickname") || "Profile";
  527. $("#profile-tab-link").text(nickname);
  528. $("#top-header-links .user-name a").text(nickname);
  529. });
  530. this.profile.bind("change:avatarSrc", function(profile) {
  531. var src = profile.get("avatarSrc");
  532. $(".profile-tab-avatar").attr("src", src);
  533. $("#top-header #user-info .user-avatar").attr("src", src);
  534. });
  535. },
  536. userCardPopulated_: false,
  537. populateUserCard: function() {
  538. if (Profile.userCardPopulated_) {
  539. return;
  540. }
  541. var view = new UserCardView({model: this.profile});
  542. $(".user-info-container").html(view.render().el);
  543. var publicBadgeList = new Badges.BadgeList(
  544. this.profile.get("publicBadges"));
  545. publicBadgeList.setSaveUrl("/api/v1/user/badges/public");
  546. var displayCase = new Badges.DisplayCase({ model: publicBadgeList });
  547. $(".sticker-book").append(displayCase.render().el);
  548. Profile.displayCase = displayCase;
  549. Profile.userCardPopulated_ = true;
  550. },
  551. achievementsDeferred_: null,
  552. populateAchievements: function() {
  553. if (Profile.achievementsDeferred_) {
  554. return Profile.achievementsDeferred_;
  555. }
  556. // Asynchronously load the full badge information in the background.
  557. return Profile.achievementsDeferred_ = $.ajax({
  558. type: "GET",
  559. url: "/api/v1/user/badges",
  560. data: {
  561. casing: "camel",
  562. email: USER_EMAIL
  563. },
  564. dataType: "json",
  565. success: function(data) {
  566. if (Profile.profile.isEditable()) {
  567. // The display-case is only editable if you're viewing your
  568. // own profile
  569. // TODO: save and cache these objects
  570. var fullBadgeList = new Badges.UserBadgeList();
  571. var collection = data["badgeCollections"];
  572. $.each(collection, function(i, categoryJson) {
  573. $.each(categoryJson["userBadges"], function(j, json) {
  574. fullBadgeList.add(new Badges.UserBadge(json));
  575. });
  576. });
  577. Profile.displayCase.setFullBadgeList(fullBadgeList);
  578. }
  579. // TODO: make the rendering of the full badge page use the models above
  580. // and consolidate the information
  581. var badgeInfo = [
  582. {
  583. icon: "/images/badges/meteorite-medium.png",
  584. className: "bronze",
  585. label: "Meteorite"
  586. },
  587. {
  588. icon: "/images/badges/moon-medium.png",
  589. className: "silver",
  590. label: "Moon"
  591. },
  592. {
  593. icon: "/images/badges/earth-medium.png",
  594. className: "gold",
  595. label: "Earth"
  596. },
  597. {
  598. icon: "/images/badges/sun-medium.png",
  599. className: "diamond",
  600. label: "Sun"
  601. },
  602. {
  603. icon: "/images/badges/eclipse-medium.png",
  604. className: "platinum",
  605. label: "Black Hole"
  606. },
  607. {
  608. icon: "/images/badges/master-challenge-blue.png",
  609. className: "master",
  610. label: "Challenge"
  611. }
  612. ];
  613. Handlebars.registerHelper("toMediumIconSrc", function(category) {
  614. return badgeInfo[category].icon;
  615. });
  616. Handlebars.registerHelper("toBadgeClassName", function(category) {
  617. return badgeInfo[category].className;
  618. });
  619. Handlebars.registerHelper("toBadgeLabel", function(category, fStandardView) {
  620. var label = badgeInfo[category].label;
  621. if (fStandardView) {
  622. if (label === "Challenge") {
  623. label += " Patches";
  624. } else {
  625. label += " Badges";
  626. }
  627. }
  628. return label;
  629. });
  630. Handlebars.registerPartial(
  631. "badge-container",
  632. Templates.get("profile.badge-container"));
  633. Handlebars.registerPartial(
  634. "badge",
  635. Templates.get("profile.badge"));
  636. Handlebars.registerPartial(
  637. "user-badge",
  638. Templates.get("profile.user-badge"));
  639. $.each(data["badgeCollections"], function(collectionIndex, collection) {
  640. $.each(collection["userBadges"], function(badgeIndex, badge) {
  641. var targetContextNames = badge["targetContextNames"];
  642. var numHidden = targetContextNames.length - 1;
  643. badge["visibleContextName"] = targetContextNames[0] || "";
  644. badge["listContextNamesHidden"] = $.map(
  645. targetContextNames.slice(1),
  646. function(name, nameIndex) {
  647. return {
  648. name: name,
  649. isLast: (nameIndex === numHidden - 1)
  650. };
  651. });
  652. badge["hasMultiple"] = (badge["count"] > 1);
  653. });
  654. });
  655. // TODO: what about mobile-view?
  656. data.fStandardView = true;
  657. var achievementsTemplate = Templates.get("profile.achievements");
  658. $("#tab-content-achievements").html(achievementsTemplate(data));
  659. $("#achievements #achievement-list > ul li").click(function() {
  660. var category = $(this).attr("id");
  661. var clickedBadge = $(this);
  662. $("#badge-container").css("display", "");
  663. clickedBadge.siblings().removeClass("selected");
  664. if ($("#badge-container > #" + category).is(":visible")) {
  665. if (clickedBadge.parents().hasClass("standard-view")) {
  666. $("#badge-container > #" + category).slideUp(300, function() {
  667. $("#badge-container").css("display", "none");
  668. clickedBadge.removeClass("selected");
  669. });
  670. }
  671. else {
  672. $("#badge-container > #" + category).hide();
  673. $("#badge-container").css("display", "none");
  674. clickedBadge.removeClass("selected");
  675. }
  676. }
  677. else {
  678. var jelContainer = $("#badge-container");
  679. var oldHeight = jelContainer.height();
  680. $(jelContainer).children().hide();
  681. if (clickedBadge.parents().hasClass("standard-view")) {
  682. $(jelContainer).css("min-height", oldHeight);
  683. $("#" + category, jelContainer).slideDown(300, function() {
  684. $(jelContainer).animate({"min-height": 0}, 200);
  685. });
  686. } else {
  687. $("#" + category, jelContainer).show();
  688. }
  689. clickedBadge.addClass("selected");
  690. }
  691. });
  692. $("abbr.timeago").timeago();
  693. // Start with meteorite badges displayed
  694. $("#category-0").click();
  695. // TODO: move into profile-goals.js?
  696. var currentGoals = window.GoalBook.map(function(g) { return g.get("title"); });
  697. _($(".add-goal")).map(function(elt) {
  698. var button = $(elt);
  699. var badge = button.closest(".achievement-badge");
  700. var goalTitle = badge.find(".achievement-title").text();
  701. // remove +goal button if present in list of active goals
  702. if (_.indexOf(currentGoals, goalTitle) > -1) {
  703. button.remove();
  704. // add +goal behavior to button, once.
  705. } else {
  706. button.one("click", function() {
  707. var goalObjectives = _(badge.data("objectives")).map(function(exercise) {
  708. return {
  709. "type" : "GoalObjectiveExerciseProficiency",
  710. "internal_id" : exercise
  711. };
  712. });
  713. var goal = new Goal({
  714. title: goalTitle,
  715. objectives: goalObjectives
  716. });
  717. window.GoalBook.add(goal);
  718. goal.save()
  719. .fail(function(err) {
  720. var error = err.responseText;
  721. button.addClass("failure")
  722. .text("oh no!").attr("title", "This goal could not be saved.");
  723. KAConsole.log("Error while saving new badge goal", goal);
  724. window.GoalBook.remove(goal);
  725. })
  726. .success(function() {
  727. button.text("Goal Added!").addClass("success");
  728. badge.find(".energy-points-badge").addClass("goal-added");
  729. });
  730. });
  731. }
  732. });
  733. }
  734. });
  735. },
  736. goalsDeferred_: null,
  737. populateGoals: function() {
  738. if (Profile.goalsDeferred_) {
  739. return Profile.goalsDeferred_;
  740. }
  741. // TODO: Abstract away profile + actor privileges
  742. // Also in profile.handlebars
  743. var email = Profile.profile.get("email");
  744. if (email) {
  745. Profile.goalsDeferred_ = $.ajax({
  746. type: "GET",
  747. url: "/api/v1/user/goals",
  748. data: {email: email},
  749. dataType: "json",
  750. success: function(data) {
  751. GoalProfileViewsCollection.render(data);
  752. }
  753. });
  754. } else {
  755. Profile.renderFakeGoals_();
  756. Profile.goalsDeferred_ = new $.Deferred();
  757. Profile.goalsDeferred_.resolve();
  758. }
  759. return Profile.goalsDeferred_;
  760. },
  761. renderFakeGoals_: function() {
  762. var exerciseGoal = new Goal(Goal.defaultExerciseProcessGoalAttrs_),
  763. videoGoal = new Goal(Goal.defaultVideoProcessGoalAttrs_),
  764. fakeGoalBook = new GoalCollection([exerciseGoal, videoGoal]),
  765. fakeView = new GoalProfileView({model: fakeGoalBook});
  766. $("#profile-goals-content").append(fakeView.show().addClass("empty-chart"));
  767. },
  768. populateSuggestedActivity: function(activities) {
  769. var suggestedTemplate = Templates.get("profile.suggested-activity");
  770. var attachProgress = function(activity) {
  771. var progress = activity["progress"] || 0;
  772. var formattedProgress = progress ?
  773. (100 * progress).toPrecision(4) + "%" :
  774. "not started";
  775. activity["streakBar"] = {
  776. "proficient": false,
  777. "suggested": true,
  778. "progressDisplay": formattedProgress,
  779. // TODO: is this the right width?
  780. "maxWidth": 228,
  781. "width": activity["progress"] * 228
  782. };
  783. };
  784. _.each(activities["exercises"] || [], attachProgress);
  785. _.each(activities["videos"] || [], attachProgress);
  786. $("#suggested-activity").append(suggestedTemplate(activities));
  787. },
  788. populateRecentActivity: function(activities) {
  789. var listTemplate = Templates.get("profile.recent-activity-list"),
  790. exerciseTemplate = Templates.get("profile.recent-activity-exercise"),
  791. badgeTemplate = Templates.get("profile.recent-activity-badge"),
  792. videoTemplate = Templates.get("profile.recent-activity-video"),
  793. goalTemplate = Templates.get("profile.recent-activity-goal");
  794. Handlebars.registerHelper("renderActivity", function(activity) {
  795. _.extend(activity, {profileRoot: Profile.profileRoot});
  796. if (activity.sType === "Exercise") {
  797. return exerciseTemplate(activity);
  798. } else if (activity.sType === "Badge") {
  799. return badgeTemplate(activity);
  800. } else if (activity.sType === "Video") {
  801. return videoTemplate(activity);
  802. } else if (activity.sType === "Goal") {
  803. return goalTemplate(activity);
  804. }
  805. return "";
  806. });
  807. $("#recent-activity").append(listTemplate(activities))
  808. .find("span.timeago").timeago();
  809. },
  810. activityDeferred_: null,
  811. populateActivity: function() {
  812. if (Profile.activityDeferred_) {
  813. return Profile.activityDeferred_;
  814. }
  815. $("#recent-activity-progress-bar").progressbar({value: 100});
  816. // TODO: Abstract away profile + actor privileges
  817. var email = Profile.profile.get("email");
  818. if (email) {
  819. Profile.activityDeferred_ = $.ajax({
  820. type: "GET",
  821. url: "/api/v1/user/activity",
  822. data: {
  823. email: email,
  824. casing: "camel"
  825. },
  826. dataType: "json",
  827. success: function(data) {
  828. $("#activity-loading-placeholder").fadeOut(
  829. "slow", function() {
  830. $(this).hide();
  831. });
  832. Profile.populateSuggestedActivity(data.suggested);
  833. Profile.populateRecentActivity(data.recent);
  834. $("#activity-contents").show();
  835. }
  836. });
  837. } else {
  838. Profile.activityDeferred_ = new $.Deferred();
  839. Profile.activityDeferred_.resolve();
  840. }
  841. return Profile.activityDeferred_;
  842. }
  843. };