PageRenderTime 88ms CodeModel.GetById 49ms RepoModel.GetById 0ms app.codeStats 1ms

/javascript/profile-package/profile.js

https://bitbucket.org/taewony/stable
JavaScript | 1239 lines | 996 code | 183 blank | 60 comment | 194 complexity | 42dff3d54b108aa459c56de4b84d58e8 MD5 | raw file
Possible License(s): BSD-3-Clause, LGPL-3.0, 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. initialGraphUrl: null, // Filled in by the template after script load.
  9. email: null, // Filled in by the template after script load.
  10. fLoadingGraph: false,
  11. fLoadedGraph: false,
  12. userGoalsHref: '',
  13. init: function() {
  14. $('.share-link').hide();
  15. $('.sharepop').hide();
  16. $(".achievement,.exercise,.video").hover(
  17. function () {
  18. $(this).find(".share-link").show();
  19. },
  20. function () {
  21. $(this).find(".share-link").hide();
  22. $(this).find(".sharepop").hide();
  23. });
  24. $('.share-link').click(function() {
  25. if ( $.browser.msie && (parseInt($.browser.version, 10) < 8) ) {
  26. $(this).next(".sharepop").toggle();
  27. } else {
  28. $(this).next(".sharepop").toggle(
  29. "drop", { direction:'up' }, "fast" );
  30. }
  31. return false;
  32. });
  33. // Init Highcharts global options.
  34. Highcharts.setOptions({
  35. credits: {
  36. enabled: false
  37. },
  38. title: {
  39. text: ''
  40. },
  41. subtitle: {
  42. text: ''
  43. }
  44. });
  45. if ($.address){
  46. // this is hackish, but it prevents the change event from being fired twice on load
  47. if ( $.address.value() === "/" ){
  48. window.location = window.location + "#" + $(".graph-link:eq(0)").attr("href");
  49. }
  50. $.address.change(function( evt ){
  51. if ( $.address.path() !== "/"){
  52. Profile.historyChange( evt );
  53. }
  54. });
  55. }
  56. $(".graph-link").click(
  57. function(evt){
  58. evt.preventDefault();
  59. if($.address){
  60. // only visit the resource described by the url, leave the params unchanged
  61. var href = $( this ).attr( "href" );
  62. var path = href.split("?")[0];
  63. // visiting a different resource
  64. if ( path !== $.address.path() ){
  65. $.address.path( path );
  66. }
  67. // applying filters for same resource via querystring
  68. else{
  69. // make a dict of current qs params and merge with the link's
  70. var currentParams = {};
  71. _.map( $.address.parameterNames(), function(e){ currentParams[e] = $.address.parameter( e ); } );
  72. var linkParams = Profile.parseQueryString( href );
  73. $.extend( currentParams, linkParams );
  74. $.address.queryString( Profile.reconstructQueryString( currentParams ) );
  75. }
  76. }
  77. }
  78. );
  79. $("#individual_report #achievements #achievement-list > ul li").click(function() {
  80. var category = $(this).attr('id');
  81. var clickedBadge = $(this);
  82. $("#badge-container").css("display", "");
  83. clickedBadge.siblings().removeClass("selected");
  84. if ($("#badge-container > #" + category ).is(":visible")) {
  85. if (clickedBadge.parents().hasClass("standard-view")) {
  86. $("#badge-container > #" + category ).slideUp(300, function(){
  87. $("#badge-container").css("display", "none");
  88. clickedBadge.removeClass("selected");
  89. });
  90. }
  91. else {
  92. $("#badge-container > #" + category ).hide();
  93. $("#badge-container").css("display", "none");
  94. clickedBadge.removeClass("selected");
  95. }
  96. }
  97. else {
  98. var jelContainer = $("#badge-container");
  99. var oldHeight = jelContainer.height();
  100. $(jelContainer).children().hide();
  101. if (clickedBadge.parents().hasClass("standard-view")) {
  102. $(jelContainer).css("min-height", oldHeight);
  103. $("#" + category, jelContainer).slideDown(300, function() {
  104. $(jelContainer).animate({"min-height": 0}, 200);
  105. });
  106. } else {
  107. $("#" + category, jelContainer).show();
  108. }
  109. clickedBadge.addClass("selected");
  110. }
  111. });
  112. var currentGoals = window.GoalBook.map( function(g){ return g.get("title"); });
  113. _( $(".add-goal") ).map( function( elt ){
  114. var button = $( elt );
  115. var badge = button.closest( ".achievement-badge" );
  116. var goalTitle = badge.find( ".achievement-title" ).text();
  117. // remove +goal button if present in list of active goals
  118. if( _.indexOf( currentGoals, goalTitle ) > -1){
  119. button.remove();
  120. // add +goal behavior to button, once.
  121. } else {
  122. button.one("click", function(){
  123. var goalObjectives = _( badge.data("objectives") ).map( function( exercise ){
  124. return {
  125. "type" : "GoalObjectiveExerciseProficiency",
  126. "internal_id" : exercise
  127. };
  128. });
  129. var goal = new Goal({
  130. title: goalTitle,
  131. objectives: goalObjectives
  132. });
  133. window.GoalBook.add(goal);
  134. goal.save()
  135. .fail(function(err) {
  136. var error = err.responseText;
  137. button.addClass("failure")
  138. .text("oh no!").attr("title","This goal could not be saved.");
  139. KAConsole.log("Error while saving new badge goal", goal);
  140. window.GoalBook.remove(goal);
  141. })
  142. .success(function(){
  143. button.text("Goal Added!").addClass("success");
  144. badge.find(".energy-points-badge").addClass("goal-added");
  145. });
  146. });
  147. }
  148. });
  149. $("#stats-nav #nav-accordion")
  150. .accordion({
  151. header:".header",
  152. active:".graph-link-selected",
  153. autoHeight: false,
  154. clearStyle: true
  155. });
  156. setTimeout(function(){
  157. if (!Profile.fLoadingGraph && !Profile.fLoadedGraph)
  158. {
  159. // If 1000 millis after document.ready fires we still haven't
  160. // started loading a graph, load manually.
  161. // The externalChange trigger may have fired before we hooked
  162. // up a listener.
  163. Profile.historyChange();
  164. }
  165. }, 1000);
  166. Profile.ProgressSummaryView = new ProgressSummaryView();
  167. },
  168. highlightPoints: function(chart, fxnHighlight) {
  169. if (!chart) return;
  170. for (var ix = 0; ix < chart.series.length; ix++) {
  171. var series = chart.series[ix];
  172. this.muteSeriesStyles(series);
  173. for (var ixData = 0; ixData < series.data.length; ixData++) {
  174. var pointOptions = series.data[ixData].options;
  175. if (!pointOptions.marker) pointOptions.marker = {};
  176. pointOptions.marker.enabled = fxnHighlight(pointOptions);
  177. if (pointOptions.marker.enabled) pointOptions.marker.radius = 6;
  178. }
  179. series.isDirty = true;
  180. }
  181. chart.redraw();
  182. },
  183. muteSeriesStyles: function(series) {
  184. if (series.options.fMuted) return;
  185. series.graph.attr('opacity', 0.1);
  186. series.graph.attr('stroke', '#CCCCCC');
  187. series.options.lineWidth = 1;
  188. series.options.shadow = false;
  189. series.options.fMuted = true;
  190. },
  191. accentuateSeriesStyles: function(series) {
  192. series.options.lineWidth = 3.5;
  193. series.options.shadow = true;
  194. series.options.fMuted = false;
  195. },
  196. highlightSeries: function(chart, seriesHighlight) {
  197. if (!chart || !seriesHighlight) return;
  198. for (var ix = 0; ix < chart.series.length; ix++)
  199. {
  200. var series = chart.series[ix];
  201. var fSelected = (series == seriesHighlight);
  202. if (series.fSelectedLast == null || series.fSelectedLast != fSelected)
  203. {
  204. if (fSelected)
  205. this.accentuateSeriesStyles(series);
  206. else
  207. this.muteSeriesStyles(series);
  208. for (var ixData = 0; ixData < series.data.length; ixData++) {
  209. series.data[ixData].options.marker = {
  210. enabled: fSelected,
  211. radius: fSelected ? 5 : 4
  212. };
  213. }
  214. series.isDirty = true;
  215. series.fSelectedLast = fSelected;
  216. }
  217. }
  218. var options = seriesHighlight.options;
  219. options.color = '#0080C9';
  220. seriesHighlight.remove(false);
  221. chart.addSeries(options, false, false);
  222. chart.redraw();
  223. },
  224. collapseAccordion: function() {
  225. // Turn on collapsing, collapse everything, and turn off collapsing
  226. $("#stats-nav #nav-accordion").accordion(
  227. "option", "collapsible", true).accordion(
  228. "activate", false).accordion(
  229. "option", "collapsible", false);
  230. },
  231. baseGraphHref: function(href) {
  232. // regex for matching scheme:// part of uri
  233. // see http://tools.ietf.org/html/rfc3986#section-3.1
  234. var reScheme = /^\w[\w\d+-.]*:\/\//;
  235. var match = href.match(reScheme);
  236. if (match) {
  237. href = href.substring(match[0].length);
  238. }
  239. var ixSlash = href.indexOf("/");
  240. if (ixSlash > -1)
  241. href = href.substring(href.indexOf("/"));
  242. var ixQuestionMark = href.indexOf("?");
  243. if (ixQuestionMark > -1)
  244. href = href.substring(0, ixQuestionMark);
  245. return href;
  246. },
  247. /**
  248. * Expands the navigation accordion according to the link specified.
  249. * @return {boolean} whether or not a link was found to be a valid link.
  250. */
  251. expandAccordionForHref: function(href) {
  252. if (!href) {
  253. return false;
  254. }
  255. href = this.baseGraphHref(href);
  256. href = href.replace(/[<>']/g, "");
  257. var selectorAccordionSection =
  258. ".graph-link-header[href*='" + href + "']";
  259. if ( $(selectorAccordionSection).length ) {
  260. $("#stats-nav #nav-accordion").accordion(
  261. "activate", selectorAccordionSection);
  262. return true;
  263. }
  264. this.collapseAccordion();
  265. return false;
  266. },
  267. styleSublinkFromHref: function(href) {
  268. if (!href) return;
  269. var reDtStart = /dt_start=[^&]+/;
  270. var matchStart = href.match(reDtStart);
  271. var sDtStart = matchStart ? matchStart[0] : "dt_start=lastweek";
  272. href = href.replace(/[<>']/g, "");
  273. $(".graph-sub-link").removeClass("graph-sub-link-selected");
  274. $(".graph-sub-link[href*='" + this.baseGraphHref(href) + "'][href*='" + sDtStart + "']").addClass("graph-sub-link-selected");
  275. },
  276. // called whenever user clicks graph type accordion
  277. loadGraphFromLink: function(el) {
  278. if (!el) return;
  279. Profile.loadGraphStudentListAware(el.href);
  280. },
  281. loadGraphStudentListAware: function(url) {
  282. var $dropdown = $('#studentlists_dropdown ol');
  283. if ($dropdown.length == 1) {
  284. var list_id = $dropdown.data('selected').key;
  285. var qs = this.parseQueryString(url);
  286. qs['list_id'] = list_id;
  287. qs['version'] = Profile.version;
  288. qs['dt'] = $("#targetDatepicker").val();
  289. url = this.baseGraphHref(url) + '?' + this.reconstructQueryString(qs);
  290. }
  291. this.loadGraph(url);
  292. },
  293. loadFilters : function( href ){
  294. // fix the hrefs for each filter
  295. var a = $("#stats-filters a[href^=\"" + href + "\"]").parent();
  296. $("#stats-filters .filter:visible").not(a).slideUp("slow");
  297. a.slideDown();
  298. },
  299. loadGraph: function(href, fNoHistoryEntry) {
  300. var apiCallbacksTable = {
  301. '/api/v1/user/goals': this.renderUserGoals,
  302. '/api/v1/user/exercises': this.renderExercisesTable,
  303. '/api/v1/user/students/goals': this.renderStudentGoals,
  304. '/api/v1/user/students/progressreport': window.ClassProfile ? ClassProfile.renderStudentProgressReport : null,
  305. '/api/v1/user/students/progress/summary': this.ProgressSummaryView.render
  306. };
  307. if (!href) return;
  308. if (this.fLoadingGraph) {
  309. setTimeout(function(){Profile.loadGraph(href);}, 200);
  310. return;
  311. }
  312. this.styleSublinkFromHref(href);
  313. this.fLoadingGraph = true;
  314. this.fLoadedGraph = true;
  315. var apiCallback = null;
  316. for (var uri in apiCallbacksTable) {
  317. if (href.indexOf(uri) > -1) {
  318. apiCallback = apiCallbacksTable[uri];
  319. }
  320. }
  321. $.ajax({
  322. type: "GET",
  323. url: Timezone.append_tz_offset_query_param(href),
  324. data: {},
  325. dataType: apiCallback ? 'json' : 'html',
  326. success: function(data){
  327. Profile.finishLoadGraph(data, href, fNoHistoryEntry, apiCallback);
  328. },
  329. error: function() {
  330. Profile.finishLoadGraphError();
  331. }
  332. });
  333. $("#graph-content").html("");
  334. this.showGraphThrobber(true);
  335. },
  336. finishLoadGraph: function(data, href, fNoHistoryEntry, apiCallback) {
  337. this.fLoadingGraph = false;
  338. if (!fNoHistoryEntry) {
  339. // Add history entry for browser
  340. // if ($.address) {
  341. // $.address(href);
  342. // }
  343. }
  344. this.showGraphThrobber(false);
  345. this.styleSublinkFromHref(href);
  346. var start = (new Date).getTime();
  347. if (apiCallback) {
  348. apiCallback(data, href);
  349. } else {
  350. $("#graph-content").html(data);
  351. }
  352. var diff = (new Date).getTime() - start;
  353. KAConsole.log('API call rendered in ' + diff + ' ms.');
  354. },
  355. renderUserGoals: function(data, href) {
  356. current_goals = [];
  357. completed_goals = [];
  358. abandoned_goals = [];
  359. var qs = Profile.parseQueryString(href);
  360. // We don't handle the difference between API calls requiring email and
  361. // legacy calls requiring student_email very well, so this page gets
  362. // called with both. Need to fix the root cause (and hopefully redo all
  363. // the URLs for this page), but for now just be liberal in what we
  364. // accept.
  365. var qsEmail = qs.email || qs.student_email || null;
  366. var viewingOwnGoals = qsEmail === null || qsEmail === USER_EMAIL;
  367. $.each(data, function(idx, goal) {
  368. if (goal.completed) {
  369. if (goal.abandoned)
  370. abandoned_goals.push(goal);
  371. else
  372. completed_goals.push(goal);
  373. } else {
  374. current_goals.push(goal);
  375. }
  376. });
  377. if (viewingOwnGoals)
  378. GoalBook.reset(current_goals);
  379. else
  380. CurrentGoalBook = new GoalCollection(current_goals);
  381. CompletedGoalBook = new GoalCollection(completed_goals);
  382. AbandonedGoalBook = new GoalCollection(abandoned_goals);
  383. $("#graph-content").html('<div id="current-goals-list"></div><div id="completed-goals-list"></div><div id="abandoned-goals-list"></div>');
  384. Profile.goalsViews = {};
  385. Profile.goalsViews.current = new GoalProfileView({
  386. el: "#current-goals-list",
  387. model: viewingOwnGoals ? GoalBook : CurrentGoalBook,
  388. type: 'current',
  389. readonly: !viewingOwnGoals
  390. });
  391. Profile.goalsViews.completed = new GoalProfileView({
  392. el: "#completed-goals-list",
  393. model: CompletedGoalBook,
  394. type: 'completed',
  395. readonly: true
  396. });
  397. Profile.goalsViews.abandoned = new GoalProfileView({
  398. el: "#abandoned-goals-list",
  399. model: AbandonedGoalBook,
  400. type: 'abandoned',
  401. readonly: true
  402. });
  403. Profile.userGoalsHref = href;
  404. Profile.showGoalType('current');
  405. if (completed_goals.length > 0) {
  406. $('#goal-show-completed-link').parent().show();
  407. } else {
  408. $('#goal-show-completed-link').parent().hide();
  409. }
  410. if (abandoned_goals.length > 0) {
  411. $('#goal-show-abandoned-link').parent().show();
  412. } else {
  413. $('#goal-show-abandoned-link').parent().hide();
  414. }
  415. if (viewingOwnGoals) {
  416. $('.new-goal').addClass('green').removeClass('disabled').click(function(e) {
  417. e.preventDefault();
  418. window.newGoalDialog.show();
  419. });
  420. }
  421. },
  422. showGoalType: function(type) {
  423. if (Profile.goalsViews) {
  424. $.each(['current','completed','abandoned'], function(idx, atype) {
  425. if (type == atype) {
  426. Profile.goalsViews[atype].show();
  427. $('#goal-show-' + atype + '-link').addClass('graph-sub-link-selected');
  428. } else {
  429. Profile.goalsViews[atype].hide();
  430. $('#goal-show-' + atype + '-link').removeClass('graph-sub-link-selected');
  431. }
  432. });
  433. }
  434. },
  435. renderStudentGoals: function(data, href) {
  436. var studentGoalsViewModel = {
  437. rowData: [],
  438. sortDesc: '',
  439. filterDesc: ''
  440. };
  441. $.each(data, function(idx1, student) {
  442. student.goal_count = 0;
  443. student.most_recent_update = null;
  444. student.profile_url = "/profile?student_email="+ student.email +"#/api/v1/user/goals?email="+student.email;
  445. if (student.goals && student.goals.length > 0) {
  446. $.each(student.goals, function(idx2, goal) {
  447. // Sort objectives by status
  448. var progress_count = 0;
  449. var found_struggling = false;
  450. goal.objectiveWidth = 100/goal.objectives.length;
  451. goal.objectives.sort(function(a,b) { return b.progress-a.progress; });
  452. $.each(goal.objectives, function(idx3, objective) {
  453. Goal.calcObjectiveDependents(objective, goal.objectiveWidth);
  454. if (objective.status == 'proficient')
  455. progress_count += 1000;
  456. else if (objective.status == 'started' || objective.status == 'struggling')
  457. progress_count += 1;
  458. if (objective.status == 'struggling') {
  459. found_struggling = true;
  460. objective.struggling = true;
  461. }
  462. objective.statusCSS = objective.status ? objective.status : "not-started";
  463. objective.objectiveID = idx3;
  464. });
  465. if (!student.most_recent_update || goal.updated > student.most_recent_update)
  466. student.most_recent_update = goal;
  467. student.goal_count++;
  468. row = {
  469. rowID: studentGoalsViewModel.rowData.length,
  470. student: student,
  471. goal: goal,
  472. progress_count: progress_count,
  473. goal_idx: student.goal_count,
  474. struggling: found_struggling
  475. };
  476. $.each(goal.objectives, function(idx3, objective) {
  477. objective.row = row;
  478. });
  479. studentGoalsViewModel.rowData.push(row);
  480. });
  481. } else {
  482. studentGoalsViewModel.rowData.push({
  483. rowID: studentGoalsViewModel.rowData.length,
  484. student: student,
  485. goal: {objectives: []},
  486. progress_count: -1,
  487. goal_idx: 0,
  488. struggling: false
  489. });
  490. }
  491. });
  492. var template = Templates.get( "profile.profile-class-goals" );
  493. $("#graph-content").html( template(studentGoalsViewModel) );
  494. $("#class-student-goal .goal-row").each(function() {
  495. var jRowEl = $(this);
  496. var goalViewModel = studentGoalsViewModel.rowData[jRowEl.attr('data-id')];
  497. goalViewModel.rowElement = this;
  498. goalViewModel.countElement = jRowEl.find('.goal-count');
  499. goalViewModel.startTimeElement = jRowEl.find('.goal-start-time');
  500. goalViewModel.updateTimeElement = jRowEl.find('.goal-update-time');
  501. Profile.AddObjectiveHover(jRowEl);
  502. jRowEl.find("a.objective").each(function() {
  503. var obj = goalViewModel.goal.objectives[$(this).attr('data-id')];
  504. obj.blockElement = this;
  505. if ( obj.internal_id !== "" &&
  506. (obj.type === "GoalObjectiveExerciseProficiency" ||
  507. obj.type === "GoalObjectiveAnyExerciseProficiency")
  508. ) {
  509. $(this).click(function( e ) {
  510. e.preventDefault();
  511. Profile.collapseAccordion();
  512. var url = Profile.exerciseProgressUrl(obj.internal_id,
  513. goalViewModel.student.email);
  514. Profile.loadGraph(url);
  515. });
  516. }
  517. });
  518. });
  519. $("#student-goals-sort").change(function() { Profile.sortStudentGoals(studentGoalsViewModel); });
  520. $("input.student-goals-filter-check").change(function() { Profile.filterStudentGoals(studentGoalsViewModel); });
  521. $("#student-goals-search").keyup(function() { Profile.filterStudentGoals(studentGoalsViewModel); });
  522. Profile.sortStudentGoals(studentGoalsViewModel);
  523. Profile.filterStudentGoals(studentGoalsViewModel);
  524. },
  525. sortStudentGoals: function(studentGoalsViewModel) {
  526. var sort = $("#student-goals-sort").val();
  527. var show_updated = false;
  528. if (sort == 'name') {
  529. studentGoalsViewModel.rowData.sort(function(a,b) {
  530. if (b.student.nickname > a.student.nickname)
  531. return -1;
  532. if (b.student.nickname < a.student.nickname)
  533. return 1;
  534. return a.goal_idx-b.goal_idx;
  535. });
  536. studentGoalsViewModel.sortDesc = 'student name';
  537. show_updated = false; // started
  538. } else if (sort == 'progress') {
  539. studentGoalsViewModel.rowData.sort(function(a,b) {
  540. return b.progress_count - a.progress_count;
  541. });
  542. studentGoalsViewModel.sortDesc = 'goal progress';
  543. show_updated = true; // updated
  544. } else if (sort == 'created') {
  545. studentGoalsViewModel.rowData.sort(function(a,b) {
  546. if (a.goal && !b.goal)
  547. return -1;
  548. if (b.goal && !a.goal)
  549. return 1;
  550. if (a.goal && b.goal) {
  551. if (b.goal.created > a.goal.created)
  552. return 1;
  553. if (b.goal.created < a.goal.created)
  554. return -1;
  555. }
  556. return 0;
  557. });
  558. studentGoalsViewModel.sortDesc = 'goal creation time';
  559. show_updated = false; // started
  560. } else if (sort == 'updated') {
  561. studentGoalsViewModel.rowData.sort(function(a,b) {
  562. if (a.goal && !b.goal)
  563. return -1;
  564. if (b.goal && !a.goal)
  565. return 1;
  566. if (a.goal && b.goal) {
  567. if (b.goal.updated > a.goal.updated)
  568. return 1;
  569. if (b.goal.updated < a.goal.updated)
  570. return -1;
  571. }
  572. return 0;
  573. });
  574. studentGoalsViewModel.sortDesc = 'last work logged time';
  575. show_updated = true; // updated
  576. }
  577. var container = $('#class-student-goal').detach();
  578. $.each(studentGoalsViewModel.rowData, function(idx, row) {
  579. $(row.rowElement).detach();
  580. $(row.rowElement).appendTo(container);
  581. if (show_updated) {
  582. row.startTimeElement.hide();
  583. row.updateTimeElement.show();
  584. } else {
  585. row.startTimeElement.show();
  586. row.updateTimeElement.hide();
  587. }
  588. });
  589. container.insertAfter('#class-goal-filter-desc');
  590. Profile.updateStudentGoalsFilterText(studentGoalsViewModel);
  591. },
  592. updateStudentGoalsFilterText: function(studentGoalsViewModel) {
  593. var text = 'Sorted by ' + studentGoalsViewModel.sortDesc + '. ' + studentGoalsViewModel.filterDesc + '.';
  594. $('#class-goal-filter-desc').html(text);
  595. },
  596. filterStudentGoals: function(studentGoalsViewModel) {
  597. var filter_text = $.trim($("#student-goals-search").val().toLowerCase());
  598. var filters = {};
  599. $("input.student-goals-filter-check").each(function(idx, element) {
  600. filters[$(element).attr('name')] = $(element).is(":checked");
  601. });
  602. studentGoalsViewModel.filterDesc = '';
  603. if (filters['most-recent']) {
  604. studentGoalsViewModel.filterDesc += 'most recently worked on goals';
  605. }
  606. if (filters['in-progress']) {
  607. if (studentGoalsViewModel.filterDesc !== '') studentGoalsViewModel.filterDesc += ', ';
  608. studentGoalsViewModel.filterDesc += 'goals in progress';
  609. }
  610. if (filters['struggling']) {
  611. if (studentGoalsViewModel.filterDesc !== '') studentGoalsViewModel.filterDesc += ', ';
  612. studentGoalsViewModel.filterDesc += 'students who are struggling';
  613. }
  614. if (filter_text !== '') {
  615. if (studentGoalsViewModel.filterDesc !== '') studentGoalsViewModel.filterDesc += ', ';
  616. studentGoalsViewModel.filterDesc += 'students/goals matching "' + filter_text + '"';
  617. }
  618. if (studentGoalsViewModel.filterDesc !== '')
  619. studentGoalsViewModel.filterDesc = 'Showing only ' + studentGoalsViewModel.filterDesc;
  620. else
  621. studentGoalsViewModel.filterDesc = 'No filters applied';
  622. var container = $('#class-student-goal').detach();
  623. $.each(studentGoalsViewModel.rowData, function(idx, row) {
  624. var row_visible = true;
  625. if (filters['most-recent']) {
  626. row_visible = row_visible && (!row.goal || (row.goal == row.student.most_recent_update));
  627. }
  628. if (filters['in-progress']) {
  629. row_visible = row_visible && (row.goal && (row.progress_count > 0));
  630. }
  631. if (filters['struggling']) {
  632. row_visible = row_visible && (row.struggling);
  633. }
  634. if (row_visible) {
  635. if (filter_text === '' || row.student.nickname.toLowerCase().indexOf(filter_text) >= 0) {
  636. if (row.goal) {
  637. $.each(row.goal.objectives, function(idx, objective) {
  638. $(objective.blockElement).removeClass('matches-filter');
  639. });
  640. }
  641. } else {
  642. row_visible = false;
  643. if (row.goal) {
  644. $.each(row.goal.objectives, function(idx, objective) {
  645. if ((objective.description.toLowerCase().indexOf(filter_text) >= 0)) {
  646. row_visible = true;
  647. $(objective.blockElement).addClass('matches-filter');
  648. } else {
  649. $(objective.blockElement).removeClass('matches-filter');
  650. }
  651. });
  652. }
  653. }
  654. }
  655. if (row_visible)
  656. $(row.rowElement).show();
  657. else
  658. $(row.rowElement).hide();
  659. if (filters['most-recent'])
  660. row.countElement.hide();
  661. else
  662. row.countElement.show();
  663. });
  664. container.insertAfter('#class-goal-filter-desc');
  665. Profile.updateStudentGoalsFilterText(studentGoalsViewModel);
  666. },
  667. finishLoadGraphError: function() {
  668. this.fLoadingGraph = false;
  669. this.showGraphThrobber(false);
  670. $("#graph-content").html("<div class='graph-notification'>It's our fault. We ran into a problem loading this graph. Try again later, and if this continues to happen please <a href='/reportissue?type=Defect'>let us know</a>.</div>");
  671. },
  672. /**
  673. * Renders the exercise blocks given the JSON blob about the exercises.
  674. */
  675. renderExercisesTable: function(data) {
  676. var templateContext = [];
  677. for ( var i = 0, exercise; exercise = data[i]; i++ ) {
  678. var stat = "Not started";
  679. var color = "";
  680. var states = exercise["exercise_states"];
  681. var totalDone = exercise["total_done"];
  682. if ( states["reviewing"] ) {
  683. stat = "Review";
  684. color = "review light";
  685. } else if ( states["proficient"] ) {
  686. // TODO: handle implicit proficiency - is that data in the API?
  687. // (due to proficiency in a more advanced module)
  688. stat = "Proficient";
  689. color = "proficient";
  690. } else if ( states["struggling"] ) {
  691. stat = "Struggling";
  692. color = "struggling";
  693. } else if ( totalDone > 0 ) {
  694. stat = "Started";
  695. color = "started";
  696. }
  697. if ( color ) {
  698. color = color + " action-gradient seethrough";
  699. } else {
  700. color = "transparent";
  701. }
  702. var model = exercise["exercise_model"];
  703. templateContext.push({
  704. "name": model["name"],
  705. "color": color,
  706. "status": stat,
  707. "shortName": model["short_display_name"] || model["display_name"],
  708. "displayName": model["display_name"],
  709. "progress": Math.floor( exercise["progress"] * 100 ) + "%",
  710. "totalDone": totalDone
  711. });
  712. }
  713. var template = Templates.get( "profile.exercise_progress" );
  714. $("#graph-content").html( template({ "exercises": templateContext }) );
  715. Profile.hoverContent($("#module-progress .student-module-status"));
  716. $("#module-progress .student-module-status").click(function(e) {
  717. $("#info-hover-container").hide();
  718. Profile.collapseAccordion();
  719. // Extract the name from the ID, which has been prefixed.
  720. var exerciseName = this.id.substring( "exercise-".length );
  721. var url = Profile.exerciseProgressUrl(exerciseName, Profile.email);
  722. Profile.loadGraph(url);
  723. });
  724. },
  725. // TODO: move history management out to a common utility
  726. historyChange: function(e) {
  727. var href = ( $.address.value() === "/" ) ? this.initialGraphUrl : $.address.value();
  728. var url = ( $.address.path() === "/" ) ? this.initialGraphUrl : $.address.path();
  729. if ( href ) {
  730. if ( this.expandAccordionForHref(href) ) {
  731. this.loadGraph( href , true );
  732. this.loadFilters( url );
  733. } else {
  734. // Invalid URL - just try the first link available.
  735. var links = $(".graph-link");
  736. if ( links.length ) {
  737. Profile.loadGraphFromLink( links[0] );
  738. }
  739. }
  740. }
  741. },
  742. showGraphThrobber: function(fVisible) {
  743. if (fVisible)
  744. $("#graph-progress-bar").progressbar({value: 100}).slideDown("fast");
  745. else
  746. $("#graph-progress-bar").slideUp("fast");
  747. },
  748. // TODO: move this out to a more generic utility file.
  749. parseQueryString: function(url) {
  750. var qs = {};
  751. var parts = url.split('?');
  752. if(parts.length == 2) {
  753. var querystring = parts[1].split('&');
  754. for(var i = 0; i<querystring.length; i++) {
  755. var kv = querystring[i].split('=');
  756. if(kv[0].length > 0) { //fix trailing &
  757. key = decodeURIComponent(kv[0]);
  758. value = decodeURIComponent(kv[1]);
  759. qs[key] = value;
  760. }
  761. }
  762. }
  763. return qs;
  764. },
  765. // TODO: move this out to a more generic utility file.
  766. reconstructQueryString: function(hash, kvjoin, eljoin) {
  767. kvjoin = kvjoin || '=';
  768. eljoin = eljoin || '&';
  769. qs = [];
  770. for(var key in hash) {
  771. if(hash.hasOwnProperty(key))
  772. qs.push(key + kvjoin + hash[key]);
  773. }
  774. return qs.join(eljoin);
  775. },
  776. exerciseProgressUrl: function(exercise, email) {
  777. return "/profile/graph/exerciseproblems" +
  778. "?exercise_name=" + exercise +
  779. "&student_email=" + encodeURIComponent(email);
  780. },
  781. hoverContent: function(elements) {
  782. var lastHoverTime;
  783. var mouseX;
  784. var mouseY;
  785. elements.hover(
  786. function( e ) {
  787. var hoverTime = +(new Date());
  788. lastHoverTime = hoverTime;
  789. mouseX = e.pageX;
  790. mouseY = e.pageY;
  791. var el = this;
  792. setTimeout(function() {
  793. if (hoverTime != lastHoverTime) {
  794. return;
  795. }
  796. var hoverData = $(el).children(".hover-data");
  797. var html = $.trim(hoverData.html());
  798. if ( html ) {
  799. var jelGraph = $("#graph-content");
  800. var leftMax = jelGraph.offset().left +
  801. jelGraph.width() - 150;
  802. var left = Math.min(mouseX + 15, leftMax);
  803. var jHoverEl = $("#info-hover-container");
  804. if ( jHoverEl.length === 0 ) {
  805. jHoverEl = $('<div id="info-hover-container"></div>').appendTo('body');
  806. }
  807. jHoverEl
  808. .html(html)
  809. .css({left: left, top: mouseY + 5})
  810. .show();
  811. }
  812. }, 100);
  813. },
  814. function( e ) {
  815. lastHoverTime = null;
  816. $("#info-hover-container").hide();
  817. }
  818. );
  819. },
  820. AddObjectiveHover: function(element) {
  821. Profile.hoverContent(element.find(".objective"));
  822. }
  823. };
  824. var GoalProfileView = Backbone.View.extend({
  825. template: Templates.get( "profile.profile-goals" ),
  826. needsRerender: true,
  827. initialize: function() {
  828. this.model.bind('change', this.render, this);
  829. this.model.bind('reset', this.render, this);
  830. this.model.bind('remove', this.render, this);
  831. this.model.bind('add', this.render, this);
  832. // only hookup event handlers if the view allows edits
  833. if (this.options.readonly) return;
  834. $(this.el)
  835. // edit titles
  836. .delegate('input.goal-title', 'focusout', $.proxy(this.changeTitle, this))
  837. .delegate('input.goal-title', 'keypress', $.proxy(function( e ) {
  838. if (e.which == '13') { // enter
  839. e.preventDefault();
  840. this.changeTitle(e);
  841. $(e.target).blur();
  842. }
  843. }, this))
  844. .delegate('input.goal-title', 'keyup', $.proxy(function( e ) {
  845. if ( e.which == '27' ) { // escape
  846. e.preventDefault();
  847. // restore old title
  848. var jel = $(e.target);
  849. var goal = this.model.get(jel.closest('.goal').data('id'));
  850. jel.val(goal.get('title'));
  851. jel.blur();
  852. }
  853. }, this))
  854. // show abandon button on hover
  855. .delegate('.goal', 'mouseenter mouseleave', function( e ) {
  856. var el = $(e.currentTarget);
  857. if ( e.type == 'mouseenter' ) {
  858. el.find(".goal-description .summary-light").hide();
  859. el.find(".goal-description .goal-controls").show();
  860. } else {
  861. el.find(".goal-description .goal-controls").hide();
  862. el.find(".goal-description .summary-light").show();
  863. }
  864. })
  865. // respond to abandon button
  866. .delegate('.abandon', 'click', $.proxy(this.abandon, this));
  867. },
  868. changeTitle: function( e, options ) {
  869. var jel = $(e.target);
  870. var goal = this.model.get(jel.closest('.goal').data('id'));
  871. var newTitle = jel.val();
  872. if (newTitle !== goal.get('title')) {
  873. goal.save({title: newTitle});
  874. }
  875. },
  876. show: function() {
  877. // render if necessary
  878. if (this.needsRerender) {
  879. this.render();
  880. }
  881. $(this.el).show();
  882. },
  883. hide: function() {
  884. $(this.el).hide();
  885. },
  886. render: function() {
  887. var jel = $(this.el);
  888. // delay rendering until the view is actually visible
  889. this.needsRerender = false;
  890. var json = _.pluck(this.model.models, 'attributes');
  891. jel.html(this.template({
  892. goals: json,
  893. isCurrent: (this.options.type == 'current'),
  894. isCompleted: (this.options.type == 'completed'),
  895. isAbandoned: (this.options.type == 'abandoned'),
  896. readonly: this.options.readonly
  897. }));
  898. // attach a NewGoalView to the new goals html
  899. var newGoalEl = this.$(".goalpicker");
  900. if ( newGoalEl.length > 0) {
  901. this.newGoalsView = new NewGoalView({
  902. el: newGoalEl,
  903. model: this.model
  904. });
  905. }
  906. Profile.AddObjectiveHover(jel);
  907. return jel;
  908. },
  909. abandon: function( evt ) {
  910. var goalEl = $(evt.target).closest('.goal');
  911. var goal = this.model.get(goalEl.data('id'));
  912. if ( !goal ) {
  913. // haven't yet received a reponse from the server after creating the
  914. // goal. Shouldn't happen too often, so just show a message.
  915. alert("Please wait a few seconds and try again. If this is the second time you've seen this message, reload the page");
  916. return;
  917. }
  918. if (confirm("Abandoning a goal is permanent and cannot be undone. Do you really want to abandon this goal?")) {
  919. // move the model to the abandoned collection
  920. this.model.remove(goal);
  921. goal.set({'abandoned': true});
  922. AbandonedGoalBook.add(goal);
  923. // persist to server
  924. goal.save().fail(function() {
  925. KAConsole.log("Warning: failed to abandon goal", goal);
  926. AbandonedGoalBook.remove(goal);
  927. this.model.add(goal);
  928. });
  929. }
  930. }
  931. });
  932. var ProgressSummaryView = function() {
  933. var fInitialized = false,
  934. template = Templates.get("profile.class-progress-summary"),
  935. statusInfo = {
  936. 'not-started': {
  937. fShowOnLeft: true,
  938. order: 0},
  939. struggling: {
  940. fShowOnLeft: true,
  941. order: 1},
  942. started: {
  943. fShowOnLeft: false,
  944. order: 2},
  945. proficient: {
  946. fShowOnLeft: false,
  947. order: 3},
  948. review: {
  949. fShowOnLeft: false,
  950. order: 4}
  951. },
  952. updateFilterTimeout = null;
  953. function toPixelWidth(num) {
  954. return Math.round(200 * num / Profile.numStudents);
  955. }
  956. function filterSummaryRows() {
  957. updateFilterTimeout = null;
  958. var filterText = $("#student-progresssummary-search").val()
  959. .trim().toLowerCase();
  960. $(".exercise-row").each(function(index) {
  961. var jel = $(this),
  962. exerciseName = jel.find(".exercise-name span")
  963. .text().toLowerCase();
  964. if (filterText === "" || exerciseName.indexOf(filterText) > -1) {
  965. jel.show();
  966. } else {
  967. jel.hide();
  968. }
  969. });
  970. }
  971. function init() {
  972. fInitialized = true;
  973. // Register partials and helpers
  974. Handlebars.registerPartial("class-progress-column", Templates.get("profile.class-progress-column"));
  975. Handlebars.registerHelper("toPixelWidth", function(num) {
  976. return toPixelWidth(num);
  977. });
  978. Handlebars.registerHelper("toNumberOfStudents", function(num) {
  979. if (toPixelWidth(num) < 20) {
  980. return "";
  981. }
  982. return num;
  983. });
  984. Handlebars.registerHelper("toDisplay", function(status) {
  985. if (status === "not-started") {
  986. return "unstarted";
  987. }
  988. return status;
  989. });
  990. Handlebars.registerHelper("progressColumn", function(block) {
  991. this.progressSide = block.hash.side;
  992. return block(this);
  993. });
  994. Handlebars.registerHelper("progressIter", function(progress, block) {
  995. var result = "",
  996. fOnLeft = (block.hash.side === "left");
  997. $.each(progress, function(index, p) {
  998. if (fOnLeft === statusInfo[p.status].fShowOnLeft) {
  999. result += block(p);
  1000. }
  1001. });
  1002. return result;
  1003. });
  1004. // Delegate clicks to expand rows and load student graphs
  1005. $("#graph-content").delegate(".exercise-row", "click", function(e) {
  1006. var jRow = $(this),
  1007. studentLists = jRow.find(".student-lists");
  1008. if (studentLists.is(":visible")) {
  1009. jRow.find(".segment").each(function(index) {
  1010. var jel = $(this),
  1011. width = jel.data("width"),
  1012. span = width < 20 ? "" : jel.data("num");
  1013. jel.animate({width: width}, 350, "easeInOutCubic")
  1014. .find("span").html(span);
  1015. });
  1016. studentLists.fadeOut(100, "easeInOutCubic");
  1017. } else {
  1018. jRow.find(".segment").animate({width: 100}, 450, "easeInOutCubic", function() {
  1019. var jel = $(this),
  1020. status = jel.data("status");
  1021. jel.find("span").html(status);
  1022. });
  1023. studentLists.delay(150).fadeIn(650, "easeInOutCubic");
  1024. }
  1025. });
  1026. $("#graph-content").delegate(".student-link", "click", function(e) {
  1027. e.preventDefault();
  1028. var jel = $(this),
  1029. exercise = jel.data("exercise"),
  1030. email = jel.data("email");
  1031. Profile.collapseAccordion();
  1032. var url = Profile.exerciseProgressUrl(exercise, email);
  1033. Profile.loadGraph(url);
  1034. });
  1035. $("#stats-filters").delegate("#student-progresssummary-search", "keyup", function() {
  1036. if (updateFilterTimeout == null) {
  1037. updateFilterTimeout = setTimeout(filterSummaryRows, 250);
  1038. }
  1039. });
  1040. }
  1041. return {
  1042. render: function(context) {
  1043. if (!fInitialized) {
  1044. init();
  1045. }
  1046. Profile.numStudents = context.num_students;
  1047. $.each(context.exercises, function(index, exercise) {
  1048. exercise.progress.sort(function(first, second) {
  1049. return statusInfo[first.status].order - statusInfo[second.status].order;
  1050. });
  1051. });
  1052. $("#graph-content").html(template(context));
  1053. }
  1054. };
  1055. };
  1056. $(function(){Profile.init();});