PageRenderTime 53ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 0ms

/javascript/profile-package/profile.js

https://bitbucket.org/kaekool/ebastabiilne
JavaScript | 970 lines | 746 code | 122 blank | 102 comment | 120 complexity | c3e23fd7af24d9de304d68353633b330 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 (prettyGraphName == 'activity') prettyGraphName = 'Aktiivsus';
  173. else if(prettyGraphName == 'focus') prettyGraphName = 'Fookus';
  174. else if(prettyGraphName == 'exercise progress over time') prettyGraphName = 'Soorituste kõver';
  175. else if(prettyGraphName == 'exercise progress') prettyGraphName = 'Läbitud harjutused';
  176. else if(prettyGraphName == 'exercise problems') prettyGraphName = 'Läbitud ülesanded';
  177. if (graph == "exercise-problems") {
  178. var prettyExName = exercise.replace(/_/gi, " ");
  179. this.updateTitleBreadcrumbs([prettyGraphName, prettyExName]);
  180. }
  181. else {
  182. this.updateTitleBreadcrumbs([prettyGraphName]);
  183. }
  184. if (Profile.profile.get("email")) {
  185. // If we have access to the profiled person's email, load real data.
  186. Profile.loadGraph(href);
  187. } else {
  188. // Otherwise, show some fake stuff.
  189. Profile.renderFakeGraph(graph, timePeriod);
  190. }
  191. },
  192. showExerciseProblems: function(exercise) {
  193. this.showVitalStatistics("exercise-problems", exercise);
  194. },
  195. showVitalStatisticsForTimePeriod: function(graph, timePeriod) {
  196. this.showVitalStatistics(graph, null, timePeriod);
  197. $(".vital-statistics-description ." + graph + " ." + timePeriod).addClass("selected")
  198. .siblings().removeClass("selected");
  199. },
  200. showAchievements: function() {
  201. Profile.populateAchievements();
  202. $("#tab-content-achievements").show()
  203. .siblings().hide();
  204. this.activateRelatedTab($("#tab-content-achievements").attr("rel"));
  205. this.updateTitleBreadcrumbs(["Aumärgid"]);
  206. },
  207. showGoals: function(type) {
  208. type = type || "current";
  209. Profile.populateGoals();
  210. GoalProfileViewsCollection.showGoalType(type);
  211. $("#tab-content-goals").show()
  212. .siblings().hide();
  213. this.activateRelatedTab($("#tab-content-goals").attr("rel"));
  214. this.updateTitleBreadcrumbs(["Eesmärgid"]);
  215. },
  216. activateRelatedTab: function(rel) {
  217. $(".profile-navigation .vertical-tab-list a").removeClass("active-tab");
  218. $("a[rel$='" + rel + "']").addClass("active-tab");
  219. },
  220. /**
  221. * Updates the title of the profile page to show breadcrumbs
  222. * based on the parts in the specified array. Will always pre-pend the profile
  223. * nickname.
  224. * @param {Array.<string>} parts A list of strings that will be HTML-escaped
  225. * to be the breadcrumbs.
  226. */
  227. updateTitleBreadcrumbs: function(parts) {
  228. $(".profile-notification").hide();
  229. var sheetTitle = $(".profile-sheet-title");
  230. if (parts && parts.length) {
  231. var rootCrumb = Profile.profile.get("nickname") || "Profiil";
  232. parts.unshift(rootCrumb);
  233. sheetTitle.text(parts.join(" » ")).show();
  234. if (!Profile.profile.get("email")) {
  235. $(".profile-notification").show();
  236. }
  237. } else {
  238. sheetTitle.text("").hide();
  239. }
  240. }
  241. }),
  242. /**
  243. * Navigate the router appropriately,
  244. * either to change profile sheets or vital-stats time periods.
  245. */
  246. onNavigationElementClicked_: function(e) {
  247. // TODO: Make sure middle-click + windows control-click Do The Right Thing
  248. // in a reusable way
  249. if (!e.metaKey) {
  250. e.preventDefault();
  251. var route = $(e.currentTarget).attr("href");
  252. // The navigation elements have the profileRoot in the href, but
  253. // Router.navigate should be relative to the root.
  254. if (route.indexOf(this.profileRoot) === 0) {
  255. route = route.substring(this.profileRoot.length);
  256. }
  257. Profile.router.navigate(route, true);
  258. }
  259. },
  260. loadGraph: function(href) {
  261. var apiCallbacksTable = {
  262. "/api/v1/user/exercises": this.renderExercisesTable,
  263. "/api/v1/exercises": this.renderFakeExercisesTable_
  264. };
  265. if (!href) {
  266. return;
  267. }
  268. if (this.fLoadingGraph) {
  269. setTimeout(function() {Profile.loadGraph(href);}, 200);
  270. return;
  271. }
  272. this.fLoadingGraph = true;
  273. this.fLoadedGraph = false;
  274. var apiCallback = null;
  275. for (var uri in apiCallbacksTable) {
  276. if (href.indexOf(uri) > -1) {
  277. apiCallback = apiCallbacksTable[uri];
  278. }
  279. }
  280. $.ajax({
  281. type: "GET",
  282. url: Timezone.append_tz_offset_query_param(href),
  283. data: {},
  284. dataType: apiCallback ? "json" : "html",
  285. success: function(data) {
  286. Profile.finishLoadGraph(data, apiCallback);
  287. },
  288. error: function() {
  289. Profile.finishLoadGraphError();
  290. }
  291. });
  292. $("#graph-content").html("");
  293. this.showGraphThrobber(true);
  294. },
  295. finishLoadGraph: function(data, apiCallback) {
  296. this.fLoadingGraph = false;
  297. this.showGraphThrobber(false);
  298. var start = (new Date).getTime();
  299. if (apiCallback) {
  300. apiCallback(data);
  301. } else {
  302. $("#graph-content").html(data);
  303. }
  304. var diff = (new Date).getTime() - start;
  305. KAConsole.log("API call rendered in " + diff + " ms.");
  306. this.fLoadedGraph = true;
  307. },
  308. finishLoadGraphError: function() {
  309. this.fLoadingGraph = false;
  310. this.showGraphThrobber(false);
  311. Profile.showNotification("error-graph");
  312. },
  313. renderFakeGraph: function(graphName, timePeriod) {
  314. if (graphName === "activity") {
  315. ActivityGraph.render(null, timePeriod);
  316. Profile.fLoadedGraph = true;
  317. } else if (graphName === "focus") {
  318. FocusGraph.render();
  319. Profile.fLoadedGraph = true;
  320. } else if (graphName === "exercise-progress") {
  321. Profile.loadGraph("/api/v1/exercises");
  322. } else {
  323. ExerciseGraphOverTime.render();
  324. Profile.fLoadedGraph = true;
  325. }
  326. },
  327. generateFakeExerciseTableData_: function(exerciseData) {
  328. // Generate some vaguely plausible exercise progress data
  329. return _.map(exerciseData, function(exerciseModel) {
  330. // See models.py -- h_position corresponds to the node's vertical position
  331. var position = exerciseModel["h_position"],
  332. totalDone = 0,
  333. states = {},
  334. rand = Math.random();
  335. if (position < 10) {
  336. if (Math.random() < 0.9) {
  337. totalDone = 1;
  338. if (rand < 0.5) {
  339. states["proficient"] = true;
  340. } else if (rand < 0.7) {
  341. states["reviewing"] = true;
  342. }
  343. }
  344. } else if (position < 17) {
  345. if (Math.random() < 0.6) {
  346. totalDone = 1;
  347. if (rand < 0.4) {
  348. states["proficient"] = true;
  349. } else if (rand < 0.7) {
  350. states["reviewing"] = true;
  351. } else if (rand < 0.75) {
  352. states["struggling"] = true;
  353. }
  354. }
  355. } else {
  356. if (Math.random() < 0.1) {
  357. totalDone = 1;
  358. if (rand < 0.2) {
  359. states["proficient"] = true;
  360. } else if (rand < 0.5) {
  361. states["struggling"] = true;
  362. }
  363. }
  364. }
  365. return {
  366. "exercise_model": exerciseModel,
  367. "total_done": totalDone,
  368. "exercise_states": states
  369. };
  370. });
  371. },
  372. renderFakeExercisesTable_: function(exerciseData) {
  373. // Do nothing if the user switches sheets before /api/v1/exercises responds
  374. // (The other fake sheets are rendered randomly client-side)
  375. if (Profile.fLoadedGraph) {
  376. return;
  377. }
  378. var fakeData = Profile.generateFakeExerciseTableData_(exerciseData);
  379. Profile.renderExercisesTable(fakeData, false);
  380. $("#module-progress").addClass("empty-chart");
  381. },
  382. /**
  383. * Renders the exercise blocks given the JSON blob about the exercises.
  384. */
  385. renderExercisesTable: function(data, bindEvents) {
  386. var templateContext = [],
  387. bindEvents = (bindEvents === undefined) ? true : bindEvents,
  388. isEmpty = true,
  389. exerciseModels = [];
  390. for (var i = 0, exercise; exercise = data[i]; i++) {
  391. var stat = "Not started";
  392. var color = "";
  393. var states = exercise["exercise_states"];
  394. var totalDone = exercise["total_done"];
  395. if (totalDone > 0) {
  396. isEmpty = false;
  397. }
  398. if (states["reviewing"]) {
  399. stat = "Review";
  400. color = "review light";
  401. } else if (states["proficient"]) {
  402. // TODO: handle implicit proficiency - is that data in the API?
  403. // (due to proficiency in a more advanced module)
  404. stat = "Proficient";
  405. color = "proficient";
  406. } else if (states["struggling"]) {
  407. stat = "Struggling";
  408. color = "struggling";
  409. } else if (totalDone > 0) {
  410. stat = "Started";
  411. color = "started";
  412. }
  413. if (color) {
  414. color = color + " action-gradient seethrough";
  415. } else {
  416. color = "transparent";
  417. }
  418. var model = exercise["exercise_model"];
  419. exerciseModels.push(model);
  420. templateContext.push({
  421. "name": model["name"],
  422. "color": color,
  423. "status": stat,
  424. "shortName": model["short_display_name"] || model["display_name"],
  425. "displayName": model["display_name"],
  426. "progress": Math.floor(exercise["progress"] * 100) + "%",
  427. "totalDone": totalDone
  428. });
  429. }
  430. if (isEmpty) {
  431. Profile.renderFakeExercisesTable_(exerciseModels);
  432. Profile.showNotification("empty-graph");
  433. return;
  434. }
  435. var template = Templates.get("profile.exercise_progress");
  436. $("#graph-content").html(template({ "exercises": templateContext }));
  437. if (bindEvents) {
  438. Profile.hoverContent($("#module-progress .student-module-status"));
  439. $("#module-progress .student-module-status").click(function(e) {
  440. $("#info-hover-container").hide();
  441. // Extract the name from the ID, which has been prefixed.
  442. var exerciseName = this.id.substring("exercise-".length);
  443. Profile.router.navigate("/vital-statistics/exercise-problems/" + exerciseName, true);
  444. });
  445. }
  446. },
  447. showGraphThrobber: function(fVisible) {
  448. if (fVisible) {
  449. $("#graph-progress-bar").progressbar({value: 100}).slideDown("fast");
  450. } else {
  451. $("#graph-progress-bar").slideUp("fast", function() {
  452. $(this).hide();
  453. });
  454. }
  455. },
  456. /**
  457. * Show a profile notification
  458. * Expects the class name of the div to show, such as "error-graph"
  459. */
  460. showNotification: function(className) {
  461. var jel = $(".profile-notification").removeClass("uncover-nav");
  462. if (className === "empty-graph") {
  463. jel.addClass("uncover-nav");
  464. }
  465. jel.show()
  466. .find("." + className).show()
  467. .siblings().hide();
  468. },
  469. hoverContent: function(elements, containerSelector) {
  470. var lastHoverTime,
  471. mouseX,
  472. mouseY;
  473. containerSelector = containerSelector || "#graph-content";
  474. elements.hover(
  475. function(e) {
  476. var hoverTime = +(new Date()),
  477. el = this;
  478. lastHoverTime = hoverTime;
  479. mouseX = e.pageX;
  480. mouseY = e.pageY;
  481. setTimeout(function() {
  482. if (hoverTime !== lastHoverTime) {
  483. return;
  484. }
  485. var hoverData = $(el).children(".hover-data"),
  486. html = $.trim(hoverData.html());
  487. if (html) {
  488. var jelContainer = $(containerSelector),
  489. leftMax = jelContainer.offset().left + jelContainer.width() - 150,
  490. left = Math.min(mouseX + 15, leftMax),
  491. jHoverEl = $("#info-hover-container");
  492. if (jHoverEl.length === 0) {
  493. jHoverEl = $('<div id="info-hover-container"></div>').appendTo("body");
  494. }
  495. jHoverEl
  496. .html(html)
  497. .css({left: left, top: mouseY + 5})
  498. .show();
  499. }
  500. }, 100);
  501. },
  502. function(e) {
  503. lastHoverTime = null;
  504. $("#info-hover-container").hide();
  505. }
  506. );
  507. },
  508. AddObjectiveHover: function(element) {
  509. Profile.hoverContent(element.find(".objective"), "#profile-goals-content");
  510. },
  511. render: function() {
  512. var profileTemplate = Templates.get("profile.profile");
  513. Handlebars.registerHelper("graph-date-picker-wrapper", function(block) {
  514. this.graph = block.hash.graph;
  515. return block(this);
  516. });
  517. Handlebars.registerPartial("graph-date-picker", Templates.get("profile.graph-date-picker"));
  518. Handlebars.registerPartial("vital-statistics", Templates.get("profile.vital-statistics"));
  519. $("#profile-content").html(profileTemplate({
  520. profileRoot: this.profileRoot,
  521. profileData: this.profile.toJSON(),
  522. countVideos: UserCardView.countVideos,
  523. countExercises: UserCardView.countExercises
  524. }));
  525. // Show only the user card tab,
  526. // since the Backbone default route isn't triggered
  527. // when visiting khanacademy.org/profile
  528. $("#tab-content-user-profile").show().siblings().hide();
  529. Profile.populateUserCard();
  530. this.profile.bind("change:nickname", function(profile) {
  531. var nickname = profile.get("nickname") || "Profile";
  532. $("#profile-tab-link").text(nickname);
  533. $("#top-header-links .user-name a").text(nickname);
  534. });
  535. this.profile.bind("change:avatarSrc", function(profile) {
  536. var src = profile.get("avatarSrc");
  537. $(".profile-tab-avatar").attr("src", src);
  538. $("#top-header #user-info .user-avatar").attr("src", src);
  539. });
  540. },
  541. userCardPopulated_: false,
  542. populateUserCard: function() {
  543. if (Profile.userCardPopulated_) {
  544. return;
  545. }
  546. var view = new UserCardView({model: this.profile});
  547. $(".user-info-container").html(view.render().el);
  548. var publicBadgeList = new Badges.BadgeList(
  549. this.profile.get("publicBadges"));
  550. publicBadgeList.setSaveUrl("/api/v1/user/badges/public");
  551. var displayCase = new Badges.DisplayCase({ model: publicBadgeList });
  552. $(".sticker-book").append(displayCase.render().el);
  553. Profile.displayCase = displayCase;
  554. Profile.userCardPopulated_ = true;
  555. },
  556. achievementsDeferred_: null,
  557. populateAchievements: function() {
  558. if (Profile.achievementsDeferred_) {
  559. return Profile.achievementsDeferred_;
  560. }
  561. // Asynchronously load the full badge information in the background.
  562. return Profile.achievementsDeferred_ = $.ajax({
  563. type: "GET",
  564. url: "/api/v1/user/badges",
  565. data: {
  566. casing: "camel",
  567. email: USER_EMAIL
  568. },
  569. dataType: "json",
  570. success: function(data) {
  571. if (Profile.profile.isEditable()) {
  572. // The display-case is only editable if you're viewing your
  573. // own profile
  574. // TODO: save and cache these objects
  575. var fullBadgeList = new Badges.UserBadgeList();
  576. var collection = data["badgeCollections"];
  577. $.each(collection, function(i, categoryJson) {
  578. $.each(categoryJson["userBadges"], function(j, json) {
  579. fullBadgeList.add(new Badges.UserBadge(json));
  580. });
  581. });
  582. Profile.displayCase.setFullBadgeList(fullBadgeList);
  583. }
  584. // TODO: make the rendering of the full badge page use the models above
  585. // and consolidate the information
  586. var badgeInfo = [
  587. {
  588. icon: "/images/badges/meteorite-medium.png",
  589. className: "bronze",
  590. label: "Meteoriit"
  591. },
  592. {
  593. icon: "/images/badges/moon-medium.png",
  594. className: "silver",
  595. label: "Kuu"
  596. },
  597. {
  598. icon: "/images/badges/earth-medium.png",
  599. className: "gold",
  600. label: "Maa"
  601. },
  602. {
  603. icon: "/images/badges/sun-medium.png",
  604. className: "diamond",
  605. label: "P&auml;ike"
  606. },
  607. {
  608. icon: "/images/badges/eclipse-medium.png",
  609. className: "platinum",
  610. label: "Must auk"
  611. },
  612. {
  613. icon: "/images/badges/master-challenge-blue.png",
  614. className: "master",
  615. label: "V&auml;ljakutse"
  616. }
  617. ];
  618. Handlebars.registerHelper("toMediumIconSrc", function(category) {
  619. return badgeInfo[category].icon;
  620. });
  621. Handlebars.registerHelper("toBadgeClassName", function(category) {
  622. return badgeInfo[category].className;
  623. });
  624. Handlebars.registerHelper("toBadgeLabel", function(category, fStandardView) {
  625. var label = badgeInfo[category].label;
  626. if (fStandardView) {
  627. if (label === "V&auml;ljakutse") {
  628. label = "Embleen: " + label;
  629. } else {
  630. label = "Aum&auml;rk: " + label;
  631. }
  632. }
  633. return label;
  634. });
  635. Handlebars.registerPartial(
  636. "badge-container",
  637. Templates.get("profile.badge-container"));
  638. Handlebars.registerPartial(
  639. "badge",
  640. Templates.get("profile.badge"));
  641. Handlebars.registerPartial(
  642. "user-badge",
  643. Templates.get("profile.user-badge"));
  644. $.each(data["badgeCollections"], function(collectionIndex, collection) {
  645. $.each(collection["userBadges"], function(badgeIndex, badge) {
  646. var targetContextNames = badge["targetContextNames"];
  647. var numHidden = targetContextNames.length - 1;
  648. badge["visibleContextName"] = targetContextNames[0] || "";
  649. badge["listContextNamesHidden"] = $.map(
  650. targetContextNames.slice(1),
  651. function(name, nameIndex) {
  652. return {
  653. name: name,
  654. isLast: (nameIndex === numHidden - 1)
  655. };
  656. });
  657. badge["hasMultiple"] = (badge["count"] > 1);
  658. });
  659. });
  660. // TODO: what about mobile-view?
  661. data.fStandardView = true;
  662. var achievementsTemplate = Templates.get("profile.achievements");
  663. $("#tab-content-achievements").html(achievementsTemplate(data));
  664. $("#achievements #achievement-list > ul li").click(function() {
  665. var category = $(this).attr("id");
  666. var clickedBadge = $(this);
  667. $("#badge-container").css("display", "");
  668. clickedBadge.siblings().removeClass("selected");
  669. if ($("#badge-container > #" + category).is(":visible")) {
  670. if (clickedBadge.parents().hasClass("standard-view")) {
  671. $("#badge-container > #" + category).slideUp(300, function() {
  672. $("#badge-container").css("display", "none");
  673. clickedBadge.removeClass("selected");
  674. });
  675. }
  676. else {
  677. $("#badge-container > #" + category).hide();
  678. $("#badge-container").css("display", "none");
  679. clickedBadge.removeClass("selected");
  680. }
  681. }
  682. else {
  683. var jelContainer = $("#badge-container");
  684. var oldHeight = jelContainer.height();
  685. $(jelContainer).children().hide();
  686. if (clickedBadge.parents().hasClass("standard-view")) {
  687. $(jelContainer).css("min-height", oldHeight);
  688. $("#" + category, jelContainer).slideDown(300, function() {
  689. $(jelContainer).animate({"min-height": 0}, 200);
  690. });
  691. } else {
  692. $("#" + category, jelContainer).show();
  693. }
  694. clickedBadge.addClass("selected");
  695. }
  696. });
  697. $("abbr.timeago").timeago();
  698. // Start with meteorite badges displayed
  699. $("#category-0").click();
  700. // TODO: move into profile-goals.js?
  701. var currentGoals = window.GoalBook.map(function(g) { return g.get("title"); });
  702. _($(".add-goal")).map(function(elt) {
  703. var button = $(elt);
  704. var badge = button.closest(".achievement-badge");
  705. var goalTitle = badge.find(".achievement-title").text();
  706. // remove +goal button if present in list of active goals
  707. if (_.indexOf(currentGoals, goalTitle) > -1) {
  708. button.remove();
  709. // add +goal behavior to button, once.
  710. } else {
  711. button.one("click", function() {
  712. var goalObjectives = _(badge.data("objectives")).map(function(exercise) {
  713. return {
  714. "type" : "GoalObjectiveExerciseProficiency",
  715. "internal_id" : exercise
  716. };
  717. });
  718. var goal = new Goal({
  719. title: goalTitle,
  720. objectives: goalObjectives
  721. });
  722. window.GoalBook.add(goal);
  723. goal.save()
  724. .fail(function(err) {
  725. var error = err.responseText;
  726. button.addClass("failure")
  727. .text("oh ei!").attr("title", "Seda eesm&auml;rki ei saa salvestada.");
  728. KAConsole.log("Viga uue aum&auml;rgi salvestamisel", goal);
  729. window.GoalBook.remove(goal);
  730. })
  731. .success(function() {
  732. button.text("Eesm&auml;rk lisatud!").addClass("success");
  733. badge.find(".energy-points-badge").addClass("goal-added");
  734. });
  735. });
  736. }
  737. });
  738. }
  739. });
  740. },
  741. goalsDeferred_: null,
  742. populateGoals: function() {
  743. if (Profile.goalsDeferred_) {
  744. return Profile.goalsDeferred_;
  745. }
  746. // TODO: Abstract away profile + actor privileges
  747. // Also in profile.handlebars
  748. var email = Profile.profile.get("email");
  749. if (email) {
  750. Profile.goalsDeferred_ = $.ajax({
  751. type: "GET",
  752. url: "/api/v1/user/goals",
  753. data: {email: email},
  754. dataType: "json",
  755. success: function(data) {
  756. GoalProfileViewsCollection.render(data);
  757. }
  758. });
  759. } else {
  760. Profile.renderFakeGoals_();
  761. Profile.goalsDeferred_ = new $.Deferred();
  762. Profile.goalsDeferred_.resolve();
  763. }
  764. return Profile.goalsDeferred_;
  765. },
  766. renderFakeGoals_: function() {
  767. var exerciseGoal = new Goal(Goal.defaultExerciseProcessGoalAttrs_),
  768. videoGoal = new Goal(Goal.defaultVideoProcessGoalAttrs_),
  769. fakeGoalBook = new GoalCollection([exerciseGoal, videoGoal]),
  770. fakeView = new GoalProfileView({model: fakeGoalBook});
  771. $("#profile-goals-content").append(fakeView.show().addClass("empty-chart"));
  772. },
  773. populateSuggestedActivity: function(activities) {
  774. var suggestedTemplate = Templates.get("profile.suggested-activity");
  775. var attachProgress = function(activity) {
  776. var progress = activity["progress"] || 0;
  777. var formattedProgress = progress ?
  778. (100 * progress).toPrecision(4) + "%" :
  779. "not started";
  780. activity["streakBar"] = {
  781. "proficient": false,
  782. "suggested": true,
  783. "progressDisplay": formattedProgress,
  784. // TODO: is this the right width??
  785. "maxWidth": 228,
  786. "width": activity["progress"] * 228
  787. };
  788. };
  789. _.each(activities["exercises"] || [], attachProgress);
  790. _.each(activities["videos"] || [], attachProgress);
  791. $("#suggested-activity").append(suggestedTemplate(activities));
  792. },
  793. populateRecentActivity: function(activities) {
  794. var listTemplate = Templates.get("profile.recent-activity-list"),
  795. exerciseTemplate = Templates.get("profile.recent-activity-exercise"),
  796. badgeTemplate = Templates.get("profile.recent-activity-badge"),
  797. videoTemplate = Templates.get("profile.recent-activity-video"),
  798. goalTemplate = Templates.get("profile.recent-activity-goal");
  799. Handlebars.registerHelper("renderActivity", function(activity) {
  800. _.extend(activity, {profileRoot: Profile.profileRoot});
  801. if (activity.sType === "Exercise") {
  802. return exerciseTemplate(activity);
  803. } else if (activity.sType === "Badge") {
  804. return badgeTemplate(activity);
  805. } else if (activity.sType === "Video") {
  806. return videoTemplate(activity);
  807. } else if (activity.sType === "Goal") {
  808. return goalTemplate(activity);
  809. }
  810. return "";
  811. });
  812. $("#recent-activity").append(listTemplate(activities))
  813. .find("span.timeago").timeago();
  814. },
  815. activityDeferred_: null,
  816. populateActivity: function() {
  817. if (Profile.activityDeferred_) {
  818. return Profile.activityDeferred_;
  819. }
  820. $("#recent-activity-progress-bar").progressbar({value: 100});
  821. // TODO: Abstract away profile + actor privileges
  822. var email = Profile.profile.get("email");
  823. if (email) {
  824. Profile.activityDeferred_ = $.ajax({
  825. type: "GET",
  826. url: "/api/v1/user/activity",
  827. data: {
  828. email: email,
  829. casing: "camel"
  830. },
  831. dataType: "json",
  832. success: function(data) {
  833. $("#activity-loading-placeholder").fadeOut(
  834. "slow", function() {
  835. $(this).hide();
  836. });
  837. Profile.populateSuggestedActivity(data.suggested);
  838. Profile.populateRecentActivity(data.recent);
  839. $("#activity-contents").show();
  840. }
  841. });
  842. } else {
  843. Profile.activityDeferred_ = new $.Deferred();
  844. Profile.activityDeferred_.resolve();
  845. }
  846. return Profile.activityDeferred_;
  847. }
  848. };