Java tutorial
/********************************************************************************** * * Copyright (c) 2008, 2009, 2010, 2011 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.Iterator; import java.util.List; import java.util.Set; import org.sakaiproject.gradebook.gwt.client.AppConstants; import org.sakaiproject.gradebook.gwt.client.DataTypeConversionUtil; 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.gxt.a11y.AriaButton; import org.sakaiproject.gradebook.gwt.client.gxt.event.GradeMapUpdate; import org.sakaiproject.gradebook.gwt.client.gxt.event.GradebookEvents; import org.sakaiproject.gradebook.gwt.client.gxt.event.ItemUpdate; 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.ItemModel; import org.sakaiproject.gradebook.gwt.client.gxt.view.TreeView; import org.sakaiproject.gradebook.gwt.client.gxt.view.components.NullSensitiveCheckBox; import org.sakaiproject.gradebook.gwt.client.model.Gradebook; import org.sakaiproject.gradebook.gwt.client.model.Item; import org.sakaiproject.gradebook.gwt.client.model.key.GradeFormatKey; import org.sakaiproject.gradebook.gwt.client.model.key.GradeMapKey; import org.sakaiproject.gradebook.gwt.client.model.key.ItemKey; import org.sakaiproject.gradebook.gwt.client.model.type.GradeType; 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.LoadEvent; import com.extjs.gxt.ui.client.data.Loader; 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.FieldEvent; 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.store.Record; import com.extjs.gxt.ui.client.widget.HorizontalPanel; import com.extjs.gxt.ui.client.widget.Label; import com.extjs.gxt.ui.client.widget.Text; import com.extjs.gxt.ui.client.widget.button.Button; import com.extjs.gxt.ui.client.widget.form.CheckBox; import com.extjs.gxt.ui.client.widget.form.ComboBox; import com.extjs.gxt.ui.client.widget.form.LabelField; import com.extjs.gxt.ui.client.widget.form.NumberField; import com.extjs.gxt.ui.client.widget.grid.CellEditor; 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.EditorGrid; import com.extjs.gxt.ui.client.widget.layout.RowLayout; import com.extjs.gxt.ui.client.widget.toolbar.SeparatorToolItem; import com.extjs.gxt.ui.client.widget.toolbar.ToolBar; 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.i18n.client.NumberFormat; 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.user.client.Element; import com.google.gwt.user.client.Window; import com.google.gwt.visualization.client.AbstractDataTable.ColumnType; import com.google.gwt.visualization.client.DataTable; import com.google.gwt.visualization.client.LegendPosition; public class GradeScalePanel extends GradebookPanel implements StatisticsChartLoaderListener { private ListLoader<ListLoadResult<ModelData>> loader; private ListLoader<ListLoadResult<ModelData>> gradeFormatLoader; private ListStore<ModelData> gradeFormatStore; private ComboBox<ModelData> gradeFormatListBox; private EditorGrid<ModelData> grid; private ToolBar toolbar; private Long currentGradeScaleId; private boolean isEditable; private Text letterGradeScaleMessage = new Text(i18n.gradeScaleLetterGradeMessage()); private Button closeButton; private Button resetToDefaultButton; private NumberFormat shortNumberFormat = DataTypeConversionUtil.getShortNumberFormat(); private HorizontalPanel horizontalPanel; private StatisticsChartPanel statisticsChartPanel; private Label instructionLabel; private CheckBox toggleCheckBox; // GRBK-981 private boolean hasChartUpdates = false; // GRBK-1013 private boolean hasGradeScaleUpdates = false; /* * GRBK-1071: * We don't need to get the statistics data if the visualization * API hasn't been loaded yet. */ private boolean canGetStatisticsChartData = false; public GradeScalePanel(boolean isEditable, final TreeView treeView) { super(); this.isEditable = isEditable; toolbar = new ToolBar(); LabelField gradeScale = new LabelField(i18n.gradeFormatLabel()); toolbar.add(gradeScale); gradeFormatLoader = RestBuilder.getDelayLoader(AppConstants.LIST_ROOT, EnumSet.allOf(GradeFormatKey.class), Method.GET, null, null, GWT.getModuleBaseURL(), AppConstants.REST_FRAGMENT, AppConstants.GRADE_FORMAT_FRAGMENT); gradeFormatLoader.setRemoteSort(true); gradeFormatStore = new ListStore<ModelData>(gradeFormatLoader); gradeFormatStore.setModelComparer(new EntityModelComparer<ModelData>(GradeFormatKey.L_ID.name())); gradeFormatListBox = new ComboBox<ModelData>(); gradeFormatListBox.setAllQuery(null); gradeFormatListBox.setEditable(false); gradeFormatListBox.setFieldLabel("Grade Format"); gradeFormatListBox.setDisplayField(GradeFormatKey.S_NM.name()); gradeFormatListBox.setStore(gradeFormatStore); gradeFormatListBox.setForceSelection(true); gradeFormatListBox.setLazyRender(true); gradeFormatListBox.addSelectionChangedListener(new SelectionChangedListener<ModelData>() { @Override public void selectionChanged(SelectionChangedEvent<ModelData> se) { Boolean isFormatLocked = Boolean.FALSE; Gradebook selectedGradebookModel = Registry.get(AppConstants.CURRENT); Item selectedItemModel = selectedGradebookModel.getGradebookItemModel(); ModelData gradeFormatModel = se.getSelectedItem(); currentGradeScaleId = gradeFormatModel == null ? null : (Long) gradeFormatModel.get(GradeFormatKey.L_ID.name()); if (gradeFormatModel != null) { isFormatLocked = (Boolean) gradeFormatModel.get(GradeFormatKey.B_LK.name()); // Unsure if this could happen, but will guard it anyways.. if (isFormatLocked == null) { isFormatLocked = Boolean.FALSE; } } if (currentGradeScaleId != null && !currentGradeScaleId.equals(selectedItemModel.getGradeScaleId())) { if (isFormatLocked.booleanValue()) { Window.alert(i18n.gradeFormatCannotBeChangedWarning()); loader.load(); } else { showUserFeedback(); Record record = treeView.getTreeStore().getRecord((ItemModel) selectedItemModel); record.beginEdit(); record.set(ItemKey.L_GRD_SCL_ID.name(), currentGradeScaleId); grid.mask(); ItemUpdate itemUpdate = new ItemUpdate(treeView.getTreeStore(), record, selectedItemModel, false); itemUpdate.property = ItemKey.L_GRD_SCL_ID.name(); Dispatcher.forwardEvent(GradebookEvents.UpdateItem.getEventType(), itemUpdate); } } else { loader.load(); } } }); toolbar.add(gradeFormatListBox); // GRBK-982 : Adding tool item separator and chart update toggle button SeparatorToolItem separatorToolItem = new SeparatorToolItem(); separatorToolItem.addStyleName(resources.css().gbGradeScaleSeparatorToolItem()); toolbar.add(separatorToolItem); final String toolTipOn = i18n.gradeScaleChartUpdateToggleToolTipOn(); final String toolTipOff = i18n.gradeScaleChartUpdateToggleToolTipOff(); toggleCheckBox = new NullSensitiveCheckBox(); toggleCheckBox.setToolTip(toolTipOn); toggleCheckBox.setValue(Boolean.TRUE); toggleCheckBox.setBoxLabel(i18n.gradeScaleChartUpdateToggle()); toggleCheckBox.addListener(Events.Change, new Listener<FieldEvent>() { public void handleEvent(FieldEvent be) { boolean isChecked = DataTypeConversionUtil.checkBoolean(((CheckBox) be.getField()).getValue()); if (isChecked) { toggleCheckBox.setToolTip(toolTipOn); statisticsChartPanel.unmask(); if (hasChartUpdates) { getStatisticsChartData(); hasChartUpdates = false; } } else { toggleCheckBox.setToolTip(toolTipOff); statisticsChartPanel.mask(); } } }); toolbar.add(toggleCheckBox); setTopComponent(toolbar); gradeFormatLoader.addListener(Loader.Load, new Listener<LoadEvent>() { public void handleEvent(LoadEvent be) { loadIfPossible(); } }); List<ColumnConfig> configs = new ArrayList<ColumnConfig>(); ColumnConfig column = new ColumnConfig(); column.setId(GradeMapKey.S_LTR_GRD.name()); column.setHeader(i18n.letterGradeHeader()); column.setAlignment(HorizontalAlignment.CENTER); column.setWidth(100); column.setGroupable(false); column.setMenuDisabled(true); column.setSortable(false); configs.add(column); column = new ColumnConfig(); column.setId(GradeMapKey.D_FROM.name()); column.setHeader(i18n.fromHeader()); column.setAlignment(HorizontalAlignment.CENTER); column.setWidth(70); column.setGroupable(false); column.setMenuDisabled(true); column.setSortable(false); column.setNumberFormat(shortNumberFormat); // GRBK-668: We determine if this columns is editable via setState() configs.add(column); column = new ColumnConfig(); column.setId(GradeMapKey.D_TO.name()); column.setHeader(i18n.toHeader()); column.setAlignment(HorizontalAlignment.CENTER); column.setWidth(100); column.setGroupable(false); column.setMenuDisabled(true); column.setSortable(false); column.setNumberFormat(shortNumberFormat); column.setStyle("background-color:#A9A9A9!important;"); // GRBK-874 configs.add(column); loader = RestBuilder.getDelayLoader(AppConstants.LIST_ROOT, EnumSet.allOf(GradeMapKey.class), Method.GET, null, null, GWT.getModuleBaseURL(), AppConstants.REST_FRAGMENT, AppConstants.GRADE_MAP_FRAGMENT); final ListStore<ModelData> store = new ListStore<ModelData>(loader); store.setModelComparer(new EntityModelComparer<ModelData>(GradeMapKey.S_LTR_GRD.name())); loader.addListener(Loader.Load, new Listener<LoadEvent>() { public void handleEvent(LoadEvent be) { grid.unmask(); } }); final ColumnModel cm = new ColumnModel(configs); setBodyBorder(false); setHeaderVisible(false); setHeading("Selected Grade Mapping"); setButtonAlign(HorizontalAlignment.RIGHT); setLayout(new RowLayout()); grid = new EditorGrid<ModelData>(store, cm); grid.setStyleAttribute("borderTop", "none"); grid.setBorders(true); grid.setAutoHeight(true); grid.addListener(Events.ValidateEdit, new Listener<GridEvent<ModelData>>() { public void handleEvent(GridEvent<ModelData> ge) { // By setting ge.doit to false, we ensure that the AfterEdit event is not thrown. Which means we have to throw it ourselves onSuccess ge.stopEvent(); final Record record = ge.getRecord(); Object newValue = ge.getValue(); Object originalValue = ge.getStartValue(); Double nValue = (Double) newValue; Double oValue = (Double) originalValue; // Only update if the user actually changed a grade scale value if (null != nValue && nValue.compareTo(oValue) != 0) { hasGradeScaleUpdates = true; if (!toggleCheckBox.getValue().booleanValue()) { hasChartUpdates = true; } showUserFeedback(); Dispatcher.forwardEvent(GradebookEvents.UpdateGradeMap.getEventType(), new GradeMapUpdate(record, newValue, originalValue)); } } }); closeButton = new AriaButton(i18n.close(), new SelectionListener<ButtonEvent>() { @Override public void componentSelected(ButtonEvent be) { onClose(); Dispatcher.forwardEvent(GradebookEvents.HideEastPanel.getEventType(), Boolean.FALSE); } }); resetToDefaultButton = new AriaButton(i18n.resetGradingScale(), new SelectionListener<ButtonEvent>() { @Override public void componentSelected(ButtonEvent ce) { Dispatcher.forwardEvent(GradebookEvents.DeleteGradeMap.getEventType()); } }); // GRBK-668 letterGradeScaleMessage.setStyleAttribute("padding", "10px"); letterGradeScaleMessage.setStyleAttribute("color", "red"); add(letterGradeScaleMessage); horizontalPanel = new HorizontalPanel(); horizontalPanel.add(grid); statisticsChartPanel = new StatisticsChartPanel(this); statisticsChartPanel.setLegendPosition(LegendPosition.TOP); statisticsChartPanel.setChartWidth(500); horizontalPanel.add(statisticsChartPanel); horizontalPanel.setSpacing(10); add(horizontalPanel); // GRBK-874 instructionLabel = new Label(i18n.gradeScaleInstructions()); instructionLabel.addStyleName(resources.css().gradeScaleInstructions()); add(instructionLabel); addButton(resetToDefaultButton); addButton(closeButton); // GRBK-959 setScrollMode(Scroll.AUTO); } public void onGradeScaleUpdateError() { hideUserFeedback(); } public void onFailedToUpdateItem(ItemUpdate itemUpdate) { // Ensure that the failure is on an attempt to update the GRADESCALEID if (itemUpdate.property != null && itemUpdate.property.equals(ItemKey.L_GRD_SCL_ID.name())) { Long gradeScaleId = itemUpdate.item.get(ItemKey.L_GRD_SCL_ID.name()); if (gradeScaleId != null && currentGradeScaleId != null && !currentGradeScaleId.equals(gradeScaleId)) { loadGradeScaleData(gradeScaleId); } } hideUserFeedback(); } /* * This method is called if GradeScalePanel is closed, set inactive, in the eastCardLayout. * For example, this happens when a grade item edit is started and the ItemFromPanel is shown. */ public void onClose() { /* * This is done so when the user closes the grade scale, the checkbox is active * and the chart is rendered the next time they enter the grade scale panel. If we * don't "reset" the checkbox, then the chart area is grayed out "inactive" the next * time they view the grade scale. */ toggleCheckBox.setValue(Boolean.TRUE); hideUserFeedback(); refreshCourseGrades(); } public void onRefreshGradeScale(Gradebook selectedGradebook) { loader.load(); // The onRefreshGradeScale method is called after the user selects a grade scale from the ComboBox if (toggleCheckBox.getValue().booleanValue()) { getStatisticsChartData(); } } /* * GRBK-668 * Method that adjusts the UI according to the grade type */ public void setState() { Gradebook gradebookModel = Registry.get(AppConstants.CURRENT); Item itemModel = gradebookModel.getGradebookItemModel(); GradeType gradeType = itemModel.getGradeType(); if (GradeType.LETTERS == gradeType) { letterGradeScaleMessage.show(); resetToDefaultButton.hide(); grid.getColumnModel().getColumnById(GradeMapKey.D_FROM.name()).setEditor(null); } else { letterGradeScaleMessage.hide(); resetToDefaultButton.show(); if (isEditable) { NumberField numberField = new NumberField(); numberField.addInputStyleName(resources.css().gbNumericFieldInput()); NumberFormat defaultNumberFormat = DataTypeConversionUtil.getDefaultNumberFormat(); numberField.setFormat(defaultNumberFormat); grid.getColumnModel().getColumnById(GradeMapKey.D_FROM.name()) .setEditor(new CellEditor(numberField)); } } getStatisticsChartData(); } @Override protected void onRender(Element parent, int pos) { super.onRender(parent, pos); gradeFormatLoader.load(); } private void loadGradeScaleData(Long selectedGradeScaleId) { for (int i = 0; i < gradeFormatStore.getCount(); i++) { ModelData m = gradeFormatStore.getAt(i); Long id1 = m.get(GradeFormatKey.L_ID.name()); if (id1 != null && id1.equals(selectedGradeScaleId)) { if (currentGradeScaleId == null || !currentGradeScaleId.equals(selectedGradeScaleId)) { gradeFormatListBox.setValue(m); } break; } } } private void loadGradeScaleData(Gradebook selectedGradebook) { Long selectedGradeScaleId = selectedGradebook.getGradebookItemModel().getGradeScaleId(); loadGradeScaleData(selectedGradeScaleId); } private void loadIfPossible() { Gradebook selectedGradebook = Registry.get(AppConstants.CURRENT); if (selectedGradebook != null) { loadGradeScaleData(selectedGradebook); } } private void getStatisticsChartData() { /* * GRBK-1071 * No need to get the data if the visualization API hasn't been loaded yet */ if (!canGetStatisticsChartData) { return; } showUserFeedback(); 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()); builder.sendRequest(200, 400, null, new RestCallback() { public void onError(Request request, Throwable caught) { hideUserFeedback(); Dispatcher.forwardEvent(GradebookEvents.Notification.getEventType(), new NotificationEvent( i18n.statisticsDataErrorTitle(), i18n.statisticsDataErrorMsg(), true)); } public void onFailure(Request request, Throwable exception) { hideUserFeedback(); 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 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(); hideUserFeedback(); } }); } /* * Dispatching showUserFeedback() as well as handle local logic */ private void showUserFeedback() { statisticsChartPanel.showUserFeedback(toggleCheckBox.getValue().booleanValue()); if (toggleCheckBox.getValue().booleanValue()) { toggleCheckBox.disable(); } } /* * Dispatching hideUserFeedback() as well as handle local logic */ private void hideUserFeedback() { statisticsChartPanel.hideUserFeedback(toggleCheckBox.getValue().booleanValue()); if (toggleCheckBox.getValue().booleanValue()) { toggleCheckBox.enable(); } } /* * GRBK-981 : Helper method that send an event to refresh the course grades */ private void refreshCourseGrades() { if (hasGradeScaleUpdates) { // This used to be done in the ServiceController's onUpdateGradeMap(...) method Gradebook selectedGradebook = Registry.get(AppConstants.CURRENT); Dispatcher.forwardEvent(GradebookEvents.RefreshCourseGrades.getEventType(), selectedGradebook); hasChartUpdates = false; hasGradeScaleUpdates = false; } } /* * Implementing the StatisticsChartLoaderListener API. The StatisticsChartPanel * does calls this method once the visualization API has been loaded. */ public void load() { canGetStatisticsChartData = true; getStatisticsChartData(); } }