org.sakaiproject.gradebook.gwt.client.gxt.view.panel.StatisticsPanel.java Source code

Java tutorial

Introduction

Here is the source code for org.sakaiproject.gradebook.gwt.client.gxt.view.panel.StatisticsPanel.java

Source

/**********************************************************************************
 *
 * $Id:$
 *
 ***********************************************************************************
 *
 * Copyright (c) 2008, 2009, 2010 The Regents of the University of California
 *
 * Licensed under the
 * Educational Community License, Version 2.0 (the "License"); you may
 * not use this file except in compliance with the License. You may
 * obtain a copy of the License at
 * 
 * http://www.osedu.org/licenses/ECL-2.0
 * 
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an "AS IS"
 * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing
 * permissions and limitations under the License.
 *
 **********************************************************************************/

package org.sakaiproject.gradebook.gwt.client.gxt.view.panel;

import java.util.ArrayList;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.sakaiproject.gradebook.gwt.client.AppConstants;
import org.sakaiproject.gradebook.gwt.client.I18nConstants;
import org.sakaiproject.gradebook.gwt.client.RestBuilder;
import org.sakaiproject.gradebook.gwt.client.RestBuilder.Method;
import org.sakaiproject.gradebook.gwt.client.RestCallback;
import org.sakaiproject.gradebook.gwt.client.UrlArgsCallback;
import org.sakaiproject.gradebook.gwt.client.gxt.a11y.AriaButton;
import org.sakaiproject.gradebook.gwt.client.gxt.event.GradebookEvents;
import org.sakaiproject.gradebook.gwt.client.gxt.event.NotificationEvent;
import org.sakaiproject.gradebook.gwt.client.gxt.model.EntityModelComparer;
import org.sakaiproject.gradebook.gwt.client.gxt.model.StatisticsModel;
import org.sakaiproject.gradebook.gwt.client.gxt.view.components.SectionsComboBox;
import org.sakaiproject.gradebook.gwt.client.model.Gradebook;
import org.sakaiproject.gradebook.gwt.client.model.key.SectionKey;
import org.sakaiproject.gradebook.gwt.client.model.key.StatisticsKey;
import org.sakaiproject.gradebook.gwt.client.util.Base64;

import com.extjs.gxt.ui.client.Registry;
import com.extjs.gxt.ui.client.Style.HorizontalAlignment;
import com.extjs.gxt.ui.client.Style.Scroll;
import com.extjs.gxt.ui.client.data.ListLoadResult;
import com.extjs.gxt.ui.client.data.ListLoader;
import com.extjs.gxt.ui.client.data.ModelData;
import com.extjs.gxt.ui.client.event.ButtonEvent;
import com.extjs.gxt.ui.client.event.Events;
import com.extjs.gxt.ui.client.event.GridEvent;
import com.extjs.gxt.ui.client.event.Listener;
import com.extjs.gxt.ui.client.event.SelectionChangedEvent;
import com.extjs.gxt.ui.client.event.SelectionChangedListener;
import com.extjs.gxt.ui.client.event.SelectionListener;
import com.extjs.gxt.ui.client.mvc.Dispatcher;
import com.extjs.gxt.ui.client.store.ListStore;
import com.extjs.gxt.ui.client.widget.ContentPanel;
import com.extjs.gxt.ui.client.widget.HorizontalPanel;
import com.extjs.gxt.ui.client.widget.Text;
import com.extjs.gxt.ui.client.widget.button.Button;
import com.extjs.gxt.ui.client.widget.grid.ColumnConfig;
import com.extjs.gxt.ui.client.widget.grid.ColumnModel;
import com.extjs.gxt.ui.client.widget.grid.Grid;
import com.google.gwt.core.client.GWT;
import com.google.gwt.http.client.Request;
import com.google.gwt.http.client.Response;
import com.google.gwt.json.client.JSONArray;
import com.google.gwt.json.client.JSONObject;
import com.google.gwt.json.client.JSONParser;
import com.google.gwt.json.client.JSONValue;
import com.google.gwt.visualization.client.AbstractDataTable.ColumnType;
import com.google.gwt.visualization.client.DataTable;
import com.google.gwt.visualization.client.LegendPosition;

public class StatisticsPanel extends ContentPanel {

    private final I18nConstants i18n;

    private ListLoader<ListLoadResult<ModelData>> loader;

    private HorizontalPanel gridAndChartHorizontalPanel;

    private StatisticsChartPanel statisticsChartPanel;

    private SectionsComboBox<ModelData> sectionsComboBox;

    private Grid<StatisticsModel> grid = null;

    private DataTable dataTable;

    private int selectedGradeItemRow = -1;
    private String selectedAssignmentId;
    private String selectedSectionId;

    private Map<String, DataTable> dataTableCache = new HashMap<String, DataTable>();

    private final static int FIRST_ROW = 0;

    private final static String COURSE_CACHE_KEY_PREFIX = "course-grade";

    public StatisticsPanel(final I18nConstants i18n) {

        super();

        // Getting needed resources
        this.i18n = i18n;

        // Configure main ContentPanel
        setHeading(i18n.statisticsHeading());
        setFrame(true);
        setBodyBorder(true);
        setButtonAlign(HorizontalAlignment.RIGHT);
        setBodyStyle("padding: 10px");
        setScrollMode(Scroll.AUTO);

        sectionsComboBox = new SectionsComboBox<ModelData>();
        sectionsComboBox.setStyleAttribute("padding-left", "10px");
        sectionsComboBox.addSelectionChangedListener(new SelectionChangedListener<ModelData>() {

            public void selectionChanged(SelectionChangedEvent<ModelData> se) {

                statisticsChartPanel.hide();
                grid.getStore().removeAll();
                grid.getStore().getLoader().load();
                selectedGradeItemRow = -1;
            }
        });

        // Adding the combobox
        add(sectionsComboBox);

        // Adding the instructions on how to show a chart
        Text instructions = new Text(i18n.statisticsGraphInstructions());
        instructions.setStyleAttribute("padding", "10px");
        add(instructions);

        // Creating the grade item statistics grid
        grid = getGrid();

        // Creating the chart panel and initially hide it
        statisticsChartPanel = new StatisticsChartPanel();
        statisticsChartPanel.setLegendPosition(LegendPosition.TOP);
        statisticsChartPanel.setSize(AppConstants.CHART_WIDTH, AppConstants.CHART_HEIGHT + 80);
        statisticsChartPanel.hide();

        gridAndChartHorizontalPanel = new HorizontalPanel();
        gridAndChartHorizontalPanel.setSpacing(10);
        gridAndChartHorizontalPanel.add(grid);
        gridAndChartHorizontalPanel.add(statisticsChartPanel);
        add(gridAndChartHorizontalPanel);

        // Creating the close button to the ContentPanel
        Button closeButton = new AriaButton(i18n.close(), new SelectionListener<ButtonEvent>() {

            @Override
            public void componentSelected(ButtonEvent be) {
                Dispatcher.forwardEvent(GradebookEvents.StopStatistics.getEventType(), Boolean.FALSE);

                // Hide the chart panel
                statisticsChartPanel.hide();

                // Reset the last selected grade item row
                selectedGradeItemRow = -1;

                // Clearing the data table cache
                dataTableCache.clear();

                // Reset the section selection
                sectionsComboBox.reset();
            }
        });

        addButton(closeButton);

    }

    public void onLearnerGradeRecordUpdated(ModelData learner) {

        loader.load();
    }

    private void getGradeItemStatisticsChartData(String assignmentId, String sectionId) {

        // First we check the cache if we have the data already
        String cacheKey = assignmentId + sectionId;

        if (dataTableCache.containsKey(cacheKey)) {

            // Cache hit
            dataTable = dataTableCache.get(cacheKey);
            statisticsChartPanel.setDataTable(dataTable);
            statisticsChartPanel.show();
        } else {

            statisticsChartPanel.showUserFeedback(null);

            // Data is not in cache yet
            Gradebook gbModel = Registry.get(AppConstants.CURRENT);

            RestBuilder builder = RestBuilder.getInstance(Method.GET, GWT.getModuleBaseURL(),
                    AppConstants.REST_FRAGMENT, AppConstants.STATISTICS_FRAGMENT, AppConstants.INSTRUCTOR_FRAGMENT,
                    gbModel.getGradebookUid(), gbModel.getGradebookId().toString(), assignmentId, sectionId);

            // Keeping track of the assignmentId so that we can add the dataTable to
            // the cache once the call returns
            selectedAssignmentId = assignmentId;
            selectedSectionId = sectionId;

            builder.sendRequest(200, 400, null, new RestCallback() {

                public void onError(Request request, Throwable caught) {

                    statisticsChartPanel.hideUserFeedback(null);
                    Dispatcher.forwardEvent(GradebookEvents.Notification.getEventType(), new NotificationEvent(
                            i18n.statisticsDataErrorTitle(), i18n.statisticsDataErrorMsg(), true));
                }

                public void onFailure(Request request, Throwable exception) {

                    statisticsChartPanel.hideUserFeedback(null);
                    Dispatcher.forwardEvent(GradebookEvents.Notification.getEventType(), new NotificationEvent(
                            i18n.statisticsDataErrorTitle(), i18n.statisticsDataErrorMsg(), true));
                }

                public void onSuccess(Request request, Response response) {

                    // Getting two dimensional INT array
                    // GRBK-939 
                    // deprecated method can cause security problems as it evals. 

                    /*
                     * GRBK-939 According to google this will toss exceptions if the input
                     * is null or empty, so we'll guard against that and send a notify if
                     * we have this problem... 
                     * 
                     */
                    String jsonText = response.getText();

                    if (jsonText != null && !"".equals(jsonText)) {

                        JSONValue jsonValue = JSONParser.parseStrict(jsonText);
                        JSONArray jsonArray = jsonValue.isArray();

                        JSONArray positiveFrequencies = jsonArray.get(AppConstants.POSITIVE_NUMBER).isArray();

                        dataTable = statisticsChartPanel.createDataTable();
                        dataTable.addColumn(ColumnType.STRING, i18n.statisticsChartLabelDistribution());
                        dataTable.addColumn(ColumnType.NUMBER, i18n.statisticsChartLabelFrequency());
                        dataTable.addRows(positiveFrequencies.size());

                        String[] xAxisRangeLables = statisticsChartPanel.getXAxisRangeLabels();

                        for (int i = 0; i < positiveFrequencies.size(); i++) {

                            // Set label
                            dataTable.setValue(i, 0, xAxisRangeLables[i]);

                            // Set value
                            double positiveValue = positiveFrequencies.get(i).isNumber().doubleValue();
                            dataTable.setValue(i, 1, positiveValue);
                        }

                        // adding the dataTable to the cache
                        dataTableCache.put(selectedAssignmentId + selectedSectionId, dataTable);
                        statisticsChartPanel.show();
                    } else {
                        Dispatcher.forwardEvent(GradebookEvents.Notification.getEventType(), new NotificationEvent(
                                i18n.statisticsDataErrorTitle(), i18n.statisticsDataErrorMsg(), true));
                    }

                    statisticsChartPanel.hideUserFeedback(null);
                }
            });
        }
    }

    /*
     * Gets the course/section specific statistics data and renders
     * the chart.
     * 
     *  @param sectionId : This needs to be a Base64 encoded string
     */
    private void getCourseStatisticsChartData(String sectionId) {

        // First we check the cache if we have the data already
        String cacheKey = COURSE_CACHE_KEY_PREFIX + sectionId;

        if (dataTableCache.containsKey(cacheKey)) {

            // Cache hit
            dataTable = dataTableCache.get(cacheKey);
            statisticsChartPanel.setDataTable(dataTable);
            statisticsChartPanel.show();
        } else {

            statisticsChartPanel.showUserFeedback(null);

            Gradebook gbModel = Registry.get(AppConstants.CURRENT);

            RestBuilder builder = RestBuilder.getInstance(Method.GET, GWT.getModuleBaseURL(),
                    AppConstants.REST_FRAGMENT, AppConstants.STATISTICS_FRAGMENT, AppConstants.COURSE_FRAGMENT,
                    gbModel.getGradebookUid(), sectionId);

            selectedSectionId = sectionId;

            builder.sendRequest(200, 400, null, new RestCallback() {

                public void onError(Request request, Throwable caught) {

                    statisticsChartPanel.hideUserFeedback(null);
                    Dispatcher.forwardEvent(GradebookEvents.Notification.getEventType(), new NotificationEvent(
                            i18n.statisticsDataErrorTitle(), i18n.statisticsDataErrorMsg(), true));
                }

                public void onFailure(Request request, Throwable exception) {

                    statisticsChartPanel.hideUserFeedback(null);
                    Dispatcher.forwardEvent(GradebookEvents.Notification.getEventType(), new NotificationEvent(
                            i18n.statisticsDataErrorTitle(), i18n.statisticsDataErrorMsg(), true));
                }

                public void onSuccess(Request request, Response response) {

                    /*
                     * The response text contains a sorted linked-list map, where the keys are the letter grades and the values
                     * are the frequency.
                     * e.g. {"F":0, "D-":3, "D":1, "D+":0, "C-":5, "C":0, "C+":1, "B-":0, "B":20, "B+":0, "A-":3, "A":12, "A+":1}
                     * 
                     */
                    JSONValue jsonValue = JSONParser.parseStrict(response.getText());
                    JSONObject jsonObject = jsonValue.isObject();
                    Set<String> keys = jsonObject.keySet();

                    // Initialize the datatable
                    dataTable = statisticsChartPanel.createDataTable();
                    dataTable.addColumn(ColumnType.STRING, i18n.statisticsChartLabelDistribution());
                    dataTable.addColumn(ColumnType.NUMBER, i18n.statisticsChartLabelFrequency());
                    dataTable.addRows(keys.size());

                    Iterator<String> iter = keys.iterator();
                    int index = 0;
                    while (iter.hasNext()) {

                        String key = iter.next();
                        dataTable.setValue(index, 0, key);
                        dataTable.setValue(index, 1, jsonObject.get(key).isNumber().doubleValue());
                        index++;
                    }

                    statisticsChartPanel.show();

                    // adding the dataTable to the cache
                    dataTableCache.put(COURSE_CACHE_KEY_PREFIX + selectedSectionId, dataTable);

                    statisticsChartPanel.hideUserFeedback(null);
                }
            });
        }
    }

    private Grid<StatisticsModel> getGrid() {

        // Passing the selected section to the rest builder
        UrlArgsCallback urlArgsCallback = new UrlArgsCallback() {

            public String getUrlArg() {

                String selectedSectionId = getSelectedSection();
                return selectedSectionId;
            }
        };

        // Get the Statistics grid data
        loader = RestBuilder.getDelayLoader(AppConstants.LIST_ROOT, EnumSet.allOf(StatisticsKey.class), Method.GET,
                urlArgsCallback, null, GWT.getModuleBaseURL(), AppConstants.REST_FRAGMENT,
                AppConstants.STATISTICS_FRAGMENT, AppConstants.INSTRUCTOR_FRAGMENT);

        final ListStore<StatisticsModel> store = new ListStore<StatisticsModel>(loader);
        store.setModelComparer(new EntityModelComparer<StatisticsModel>(StatisticsKey.S_ID.name()));

        final ColumnModel cm = new ColumnModel(configureGrid());

        Grid<StatisticsModel> grid = new Grid<StatisticsModel>(store, cm);
        grid.setBorders(true);
        grid.setAutoHeight(true);
        grid.addListener(Events.RowClick, new Listener<GridEvent<?>>() {

            public void handleEvent(GridEvent<?> gridEvent) {

                // If we click on the first row, which shows the Course Grade data, we don't do anything
                int rowIndex = gridEvent.getRowIndex();

                // We keep track of the selected row in case the user keeps
                // clicking on the same grade item, in which case we don't
                // need to fetch new data
                if (selectedGradeItemRow == rowIndex) {

                    return;
                } else {

                    selectedGradeItemRow = rowIndex;
                }

                String assignmentId = gridEvent.getModel().get(StatisticsKey.S_ITEM_ID.name());

                // Getting the mean so that we can determine if any grades have been entered
                // for the selected grade item
                String mean = gridEvent.getModel().get(StatisticsKey.S_MEAN.name());

                if (null != mean && !AppConstants.STATISTICS_DATA_NA.equals(mean)) {

                    if (FIRST_ROW == rowIndex) {

                        getCourseStatisticsChartData(getSelectedSection());
                    } else {

                        getGradeItemStatisticsChartData(assignmentId, getSelectedSection());
                    }
                } else {

                    // If there is no data to show, we hide the chart
                    statisticsChartPanel.hide();
                }
            }
        });

        return grid;
    }

    private List<ColumnConfig> configureGrid() {

        List<ColumnConfig> configs = new ArrayList<ColumnConfig>();

        ColumnConfig column = new ColumnConfig();
        column.setId(StatisticsKey.S_NM.name());
        column.setHeader(i18n.statsNameHeader());
        column.setWidth(200);
        column.setGroupable(false);
        column.setMenuDisabled(true);
        column.setSortable(false);
        configs.add(column);

        column = new ColumnConfig();
        column.setId(StatisticsKey.S_MEAN.name());
        column.setHeader(i18n.statsMeanHeader());
        column.setWidth(80);
        column.setGroupable(false);
        column.setMenuDisabled(true);
        column.setSortable(false);
        configs.add(column);

        column = new ColumnConfig();
        column.setId(StatisticsKey.S_STD_DEV.name());
        column.setHeader(i18n.statsStdDvHeader());
        column.setWidth(80);
        column.setGroupable(false);
        column.setMenuDisabled(true);
        column.setSortable(false);
        configs.add(column);

        column = new ColumnConfig();
        column.setId(StatisticsKey.S_MEDIAN.name());
        column.setHeader(i18n.statsMedianHeader());
        column.setWidth(80);
        column.setGroupable(false);
        column.setMenuDisabled(true);
        column.setSortable(false);
        configs.add(column);

        column = new ColumnConfig();
        column.setId(StatisticsKey.S_MODE.name());
        column.setHeader(i18n.statsModeHeader());
        column.setWidth(160);
        column.setGroupable(false);
        column.setMenuDisabled(true);
        column.setSortable(false);
        column.setResizable(true);
        configs.add(column);

        return configs;
    }

    // Method that returns the selected section
    private String getSelectedSection() {

        List<ModelData> selection = sectionsComboBox.getSelection();

        String sectionId = null;

        if (null != selection && selection.size() == 1) {

            sectionId = (String) selection.get(0).get(SectionKey.S_ID.name());

        } else {

            sectionId = AppConstants.ALL;
        }

        // GRBK-636 : Since the sctionIds have characters that are not URL safe, we Base64 encode
        // the sectionId here and then decode it on the server side. Initially, we tried to just 
        // URL encode the sectionId but that didn't work because "something" on the server side
        // decoded the URL and then return a 400
        return Base64.encode(sectionId);

    }
}