package fi.utu.ville.exercises.helpers; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import com.vaadin.ui.Button; import com.vaadin.ui.Button.ClickEvent; import com.vaadin.ui.Component; import com.vaadin.ui.Table; import com.vaadin.ui.UI; import com.vaadin.ui.VerticalLayout; import com.vaadin.ui.Window; import fi.utu.ville.exercises.helpers.StatsGiverHelper.StatSubmInfoFilter; import fi.utu.ville.exercises.model.ExerciseData; import fi.utu.ville.exercises.model.ExerciseException; import fi.utu.ville.exercises.model.ExerciseTypeDescriptor; import fi.utu.ville.exercises.model.StatisticalSubmissionInfo; import fi.utu.ville.exercises.model.StatisticsInfoColumn; import fi.utu.ville.exercises.model.SubmissionInfo; import fi.utu.ville.exercises.model.SubmissionStatisticsGiver; import fi.utu.ville.exercises.model.SubmissionVisualizer; import fi.utu.ville.standardutils.Localizer; import fi.utu.ville.standardutils.StandardUIConstants; import fi.utu.ville.standardutils.StandardUIFactory; import fi.utu.ville.standardutils.TempFilesManager; /** * <p> * This class can be used to generate a table of a {@link List} of {@link StatisticalSubmissionInfo}-objects that can be filtered and from which a set of rows * can be selected. * </p> * <p> * More columns (also {@link SubmissionInfo}-type specific) can be added by providing a {@link List} of {@link SubmInfoColumnGenerator}s. By default there are * three columns: time-on-task, time-done and evaluation. {@link ShowSubmissionColGenerator}-class can be used to generate a column with a button that opens a * {@link SubmissionVisualizer} initialized with info from that row in a popup. * </p> * <p> * This class can be used to provide filtering functionalities and to possible also to connect some other functionality (eg. visualizations such as a graph or a * heatmap) that can be applied to the filtered set of {@link StatisticalSubmissionInfo}. * </p> * * @author Riku Haavisto * * @param <S> * accepted {@link SubmissionInfo}-type * * @see StatsGiverHelper * @see StatSubmInfoFilterEditor */ public class StatSubmInfoFilterTable<S extends SubmissionInfo> { private final List<StatisticalSubmissionInfo<S>> allStatInfos; private final Table infosTable; private final List<SubmInfoColumnGenerator<?, S>> colGenerators = new ArrayList<SubmInfoColumnGenerator<?, S>>(); private StatSubmInfoFilter<S> currFilter = null; private final List<StatisticalSubmissionInfo<S>> currMatch = new ArrayList<StatisticalSubmissionInfo<S>>(); private final Localizer localizer; /** * Initializes a new {@link StatSubmInfoFilterTable} instance. The actual table can be fetched by {@link #getStatInfoTableView()}. * * @param localizer * {@link Localizer} used to localize UI * @param allStatInfos * {@link List} of {@link StatisticalSubmissionInfo}-objects backing the table * @param extraColumns * {@link List} of {@link SubmInfoColumnGenerator}s for generating extra columns to the table; can be null */ public StatSubmInfoFilterTable(Localizer localizer, List<StatisticalSubmissionInfo<S>> allStatInfos, List<SubmInfoColumnGenerator<?, S>> extraColumns) { this.localizer = localizer; this.allStatInfos = new ArrayList<StatisticalSubmissionInfo<S>>( allStatInfos); infosTable = new Table(); initTable(); if (extraColumns != null) { for (SubmInfoColumnGenerator<?, S> extraGen : extraColumns) { addExtraColumn(extraGen); } } updateTable(); } /** * Sets the filter used to select which items to show in the table. You can use the {@link StatSubmInfoFilterEditor}-class to generate and apply filters * through its GUI. * * @param aFilter * {@link StatSubmInfoFilter} to use */ public void setFilter(StatSubmInfoFilter<S> aFilter) { this.currFilter = aFilter; updateTable(); } private void initTable() { infosTable.setSelectable(true); infosTable.setMultiSelect(true); infosTable.setWidth("100%"); addExtraColGen(new DoneTimeColumnGen<S>()); addExtraColGen(new TimeOnTaskColumnGen<S>()); addExtraColGen(new EvalColumnGen<S>()); } private boolean matchesFilters(StatisticalSubmissionInfo<S> toTest) { if (currFilter == null) { return true; } return currFilter.matches(toTest); } /** * @return all the {@link StatisticalSubmissionInfo}s matching current filter */ public List<StatisticalSubmissionInfo<S>> getAllMatchingInfos() { return currMatch; } /** * This method can be used to allow user to select certain rows that will be used in some further calculations or visualizations. If user has not explicitly * selected any rows, this method defaults to returning all rows matching current filter. * * @return all the selected {@link StatisticalSubmissionInfo}s in the table, or same as {@link #getAllMatchingInfos()} if no rows are selected */ public List<StatisticalSubmissionInfo<S>> getSelectedInfos() { Set<?> selection = (Set<?>) infosTable.getValue(); if (selection == null || selection.isEmpty()) { return getAllMatchingInfos(); } else { List<StatisticalSubmissionInfo<S>> res = new ArrayList<StatisticalSubmissionInfo<S>>(); for (Object selObj : selection) { if (StatisticalSubmissionInfo.class.isAssignableFrom(selObj .getClass())) { // unfortunately there is no info to make this any safer; // however we can rest assured that these items are safe // (from allStatInfos) // unless someone has accessed the infosTable directly @SuppressWarnings("unchecked") StatisticalSubmissionInfo<S> submInfo = StatisticalSubmissionInfo.class .cast(selObj); res.add(submInfo); } else { throw new IllegalStateException( "Info-table contained items with ids that were not of class StatisticalSubmissionInfo; item was: " + selObj); } } return res; } } private void addExtraColGen(SubmInfoColumnGenerator<?, S> toAdd) { colGenerators.add(toAdd); infosTable.addContainerProperty(toAdd, toAdd.getColumnDataType(), toAdd.getDefaultValue()); infosTable.setColumnHeader(toAdd, toAdd.getColumnHeader(localizer)); } /** * Adds a new {@link SubmInfoColumnGenerator} to the table and updates the table. * * @param toAdd * {@link SubmInfoColumnGenerator} to use for generating a new column */ public void addExtraColumn(SubmInfoColumnGenerator<?, S> toAdd) { addExtraColGen(toAdd); updateTable(); } private void updateTable() { infosTable.removeAllItems(); currMatch.clear(); for (StatisticalSubmissionInfo<S> statInfo : allStatInfos) { if (matchesFilters(statInfo)) { Object[] values = new Object[colGenerators.size()]; for (int i = 0, n = colGenerators.size(); i < n; i++) { values[i] = colGenerators.get(i).getColValueFor(statInfo, localizer); } currMatch.add(statInfo); infosTable.addItem(values, statInfo); } } } /** * @return {@link Component} representing a view to the {@link StatSubmInfoFilterTable} */ public Component getStatInfoTableView() { return infosTable; } /** * Generates a list of {@link StatisticsInfoColumn}s from all the {@link StatisticalSubmissionInfo}-objects adding a column generated by each * {@link SubmInfoColumnGenerator} registered to this instance. * * @return {@link List} of {@link StatisticsInfoColumn}s generated by registered {@link SubmInfoColumnGenerator}s from unfiltered data */ public List<StatisticsInfoColumn<?>> exportUnfilteredTabularData() { List<StatisticsInfoColumn<?>> toExport = new ArrayList<StatisticsInfoColumn<?>>(); for (int i = 0, n = colGenerators.size(); i < n; i++) { toExport.add(toStatInfoColumn(colGenerators.get(i))); } return toExport; } /** * <p> * This method does the same as {@link #exportUnfilteredTabularData()} with the exception that it adds {@link StatisticsInfoColumn}s only for certain * columns. * </p> * <p> * Only columns generated by {@link SubmInfoColumnGenerator} for which {@link SubmInfoColumnGenerator #getColumnHeader(Localizer) getColumnHeader()} is in * list of accepted titles are added to the generated list. * </p> * <p> * This method can be useful when certain exercise-type has some uses {@link StatSubmInfoFilterTable} to show a more customized view on submissions made to * it and wants to also return some or all of these custom columns from its {@link SubmissionStatisticsGiver #getAsTabularData()}-method. * </p> * * @param acceptedTitles * list of names that identify columns to export * @return list of {@link StatisticsInfoColumn}s generated from accepted columns */ public List<StatisticsInfoColumn<?>> exportByColGenTitle( List<String> acceptedTitles) { List<StatisticsInfoColumn<?>> toExport = new ArrayList<StatisticsInfoColumn<?>>(); for (int i = 0, n = colGenerators.size(); i < n; i++) { if (acceptedTitles.contains(colGenerators.get(i).getColumnHeader( localizer))) { toExport.add(toStatInfoColumn(colGenerators.get(i))); } } return toExport; } private <X> StatisticsInfoColumn<X> toStatInfoColumn( SubmInfoColumnGenerator<X, S> colGen) { String header = colGen.getColumnHeader(localizer); String desc = colGen.getColumnDescription(localizer); Class<X> type = colGen.getColumnDataType(); List<X> colData = new ArrayList<X>(); for (int j = 0; j < allStatInfos.size(); j++) { colData.add(colGen.getColValueFor(allStatInfos.get(j), localizer)); } return new StatisticsInfoColumn<X>(header, desc, type, colData, colGen.isExportable()); } /** * Implementor of this interface can generate a column for the {@link StatSubmInfoFilterTable} and correct values for that column from * {@link StatisticalSubmissionInfo}-objects. * * @author Riku Haavisto * * @param <T> * type of data shown in the column * @param <A> * accepted {@link SubmissionInfo}-type */ public interface SubmInfoColumnGenerator<T, A extends SubmissionInfo> { /** * This method tells whether it makes sense (or whether it is even possible) to export data generated by this {@link SubmInfoColumnGenerator} outside of * Vaadin and ViLLE. An example of when to return false, is the 'visualize'-column. * * @return whether this column's data can be exported outside ViLLE */ boolean isExportable(); /** * @param localizer * {@link Localizer} for localizing the ui * @return {@link String} to use as header for the generated column */ String getColumnHeader(Localizer localizer); /** * @param localizer * {@link Localizer} for localizing the ui * @return localized text telling what is shown in the column (eg. score) */ String getColumnDescription(Localizer localizer); /** * * @return default value for the column */ T getDefaultValue(); /** * @return {@link Class}-file of the type used for values of this column */ Class<T> getColumnDataType(); /** * Return the value for this column for a {@link StatisticalSubmissionInfo}-object in a localized format. * * @param statSubmInfo * {@link StatisticalSubmissionInfo}-object for which to generate a value * @param localizer * {@link Localizer} for localizing UI * @return value for the column for row corresponding to given statSubmInfo */ T getColValueFor(StatisticalSubmissionInfo<A> statSubmInfo, Localizer localizer); } /* * * Default implementations for ColumnGenerators for the general properties * (done-time, evaluation, time-on-task) start here. */ /** * {@link SubmInfoColumnGenerator}-implementor for generating a 'done-time'-column. Added to {@link StatSubmInfoFilterTable} by default. * * @author Riku Haavisto * * @param <S> * accepted {@link SubmissionInfo}-type */ public static class DoneTimeColumnGen<S extends SubmissionInfo> implements SubmInfoColumnGenerator<Date, S> { @Override public String getColumnHeader(Localizer localizer) { return localizer.getUIText(StandardUIConstants.TIME); } @Override public Date getDefaultValue() { return null; } @Override public Class<Date> getColumnDataType() { return Date.class; } @Override public Date getColValueFor(StatisticalSubmissionInfo<S> statSubmInfo, Localizer localizer) { return new Date(statSubmInfo.getDoneTime()); } @Override public boolean isExportable() { return true; } @Override public String getColumnDescription(Localizer localizer) { return localizer .getUIText(StandardUIConstants.STATS_COL_DESC_DONE_TIME); } } /** * {@link SubmInfoColumnGenerator}-implementor for generating a 'evaluation'-column. Added to {@link StatSubmInfoFilterTable} by default. * * @author Riku Haavisto * * @param <S> * accepted {@link SubmissionInfo}-type */ public static class EvalColumnGen<S extends SubmissionInfo> implements SubmInfoColumnGenerator<Double, S> { @Override public String getColumnHeader(Localizer localizer) { return localizer.getUIText(StandardUIConstants.SCORE); } @Override public Double getDefaultValue() { return null; } @Override public Class<Double> getColumnDataType() { return Double.class; } @Override public Double getColValueFor(StatisticalSubmissionInfo<S> statSubmInfo, Localizer localizer) { return statSubmInfo.getEvalution(); } @Override public boolean isExportable() { return true; } @Override public String getColumnDescription(Localizer localizer) { return localizer .getUIText(StandardUIConstants.STATS_COL_DESC_SCORE); } } /** * {@link SubmInfoColumnGenerator}-implementor for generating a 'time-on-task'-column. Added to {@link StatSubmInfoFilterTable} by default. * * @author Riku Haavisto * * @param <S> * accepted {@link SubmissionInfo}-type */ public static class TimeOnTaskColumnGen<S extends SubmissionInfo> implements SubmInfoColumnGenerator<Integer, S> { @Override public String getColumnHeader(Localizer localizer) { return localizer.getUIText(StandardUIConstants.TIME_USED); } @Override public Integer getDefaultValue() { return null; } @Override public Class<Integer> getColumnDataType() { return Integer.class; } @Override public Integer getColValueFor( StatisticalSubmissionInfo<S> statSubmInfo, Localizer localizer) { return statSubmInfo.getTimeOnTask(); } @Override public boolean isExportable() { return true; } @Override public String getColumnDescription(Localizer localizer) { return localizer .getUIText(StandardUIConstants.STATS_COL_DESC_TIME_ON_TASK); } } /** * <p> * Helper method for parsing a {@link List} of {@link SubmInfoColumnGenerator}s usable with {@link StatSubmInfoFilterTable} from a list of * {@link StatisticsInfoColumn}s. * </p> * <p> * This method is analogous to {@link StatSubmInfoFilterTable #exportByColGenTitle(List)} in that it converts columns between the format used in * {@link SubmissionStatisticsGiver #getAsTabularData()} and {@link StatSubmInfoFilterTable}. This time the conversion goes the other way around, meaning * that if you already have implemented generation for some custom {@link StatisticsInfoColumn}s you can get those columns displayed in a * {@link StatSubmInfoFilterTable} by using this method. * </p> * <p> * The returned {@link SubmInfoColumnGenerator}s do not really do any hard work, they just map the already generated data and info from * {@link StatisticsInfoColumn} in such a way that the resulting {@link TabularDataSetColumnGen} can act like a {@link SubmInfoColumnGenerator}. * </p> * * @param allSubmInfos * list of all {@link StatisticalSubmissionInfo}s (in correct order) * @param extraInfoCols * list of {@link StatisticsInfoColumn}s for which to generate wrappers * @return a list of {@link SubmInfoColumnGenerator} wrapping the {@link StatisticsInfoColumn}s */ public static <R extends SubmissionInfo> List<SubmInfoColumnGenerator<?, R>> parseTabularDataColGens( List<StatisticalSubmissionInfo<R>> allSubmInfos, List<StatisticsInfoColumn<?>> extraInfoCols) { List<SubmInfoColumnGenerator<?, R>> res = new ArrayList<SubmInfoColumnGenerator<?, R>>(); Map<StatisticalSubmissionInfo<R>, Integer> submInfosToInd = new HashMap<StatisticalSubmissionInfo<R>, Integer>(); for (int i = 0; i < allSubmInfos.size(); i++) { submInfosToInd.put(allSubmInfos.get(i), i); } for (StatisticsInfoColumn<?> sic : extraInfoCols) { res.add(parseTabularDataSetColGen(sic, submInfosToInd)); } return res; } private static <X, S extends SubmissionInfo> TabularDataSetColumnGen<X, S> parseTabularDataSetColGen( StatisticsInfoColumn<X> toWrap, Map<StatisticalSubmissionInfo<S>, Integer> submInfosToOrigIndex) { return new TabularDataSetColumnGen<X, S>(toWrap, submInfosToOrigIndex); } /** * A class for wrapping a {@link StatisticsInfoColumn} so that it can be used like {@link SubmInfoColumnGenerator} * * @author Riku Haavisto * * @param <A> * type of data shown in the column * @param <S> * accepted {@link SubmissionInfo}-type > * */ public static class TabularDataSetColumnGen<A extends Object, S extends SubmissionInfo> implements SubmInfoColumnGenerator<A, S> { private final StatisticsInfoColumn<A> toWrap; private final Map<StatisticalSubmissionInfo<S>, Integer> submInfosToOrigIndex; /** * Construct a new {@link TabularDataSetColumnGen} wrapping a {@link StatisticsInfoColumn} to {@link SubmInfoColumnGenerator}. * * @param toWrap * {@link StatisticsInfoColumn} to wrap * @param submInfosToOrigIndex * mapping from {@link StatisticalSubmissionInfo} to index the object in original data-set; used to select correct value from * {@link StatisticsInfoColumn}s value when "generating" columns to new rows */ public TabularDataSetColumnGen(StatisticsInfoColumn<A> toWrap, Map<StatisticalSubmissionInfo<S>, Integer> submInfosToOrigIndex) { this.toWrap = toWrap; this.submInfosToOrigIndex = submInfosToOrigIndex; } @Override public String getColumnHeader(Localizer localizer) { return toWrap.getName(); } @Override public A getDefaultValue() { return null; } @Override public Class<A> getColumnDataType() { return toWrap.getColDataType(); } @Override public A getColValueFor(StatisticalSubmissionInfo<S> statSubmInfo, Localizer localizer) { return toWrap.getDataObjects().get( submInfosToOrigIndex.get(statSubmInfo)); } @Override public boolean isExportable() { return toWrap.isExportable(); } @Override public String getColumnDescription(Localizer localizer) { return toWrap.getDescription(); } } /** * <p> * Implements {@link SubmInfoColumnGenerator} in such a way that it generates a column with a button for opening a popup with {@link SubmissionVisualizer} * for visualizing the {@link SubmissionInfo} corresponding to given row. * </p> * <p> * <b>For safely using this method the {@link SubmissionVisualizer} of this type as well as {@link SubmissionStatisticsGiver} must treat the * {@link ExerciseData}-object as immutable, as all the generated submission-viewers will share the same {@link ExerciseData}-instance.</b> * </p> * * @author rahaav * * @param <E> * accepted {@link ExerciseData} implementor * @param <S> * accepted {@link SubmissionInfo} implementor */ public static class ShowSubmissionColGenerator<E extends ExerciseData, S extends SubmissionInfo> implements SubmInfoColumnGenerator<Button, S> { private final ExerciseTypeDescriptor<E, S> typeDesc; private final E exerData; private final TempFilesManager tempMan; /** * Constructs a {@link ShowSubmissionColGenerator} that can be added to {@link StatSubmInfoFilterTable} to generate a column with a button for opening a * popup with {@link SubmissionVisualizer} for visualizing the {@link SubmissionInfo} corresponding to given row. * * @param typeDesc * {@link ExerciseTypeDescriptor} of the exercise-type for which the class is used * @param exerData * {@link ExerciseData}-object corresponding to the exercise-instance for which statistics are shown * @param tempMan * {@link TempFilesManager} in use; can be null if it is known that exercise-type does not use it */ public ShowSubmissionColGenerator( ExerciseTypeDescriptor<E, S> typeDesc, E exerData, TempFilesManager tempMan) { this.typeDesc = typeDesc; this.exerData = exerData; this.tempMan = tempMan; } @Override public String getColumnHeader(Localizer localizer) { // TODO FIXME: localize return "Show submission"; } @Override public Button getDefaultValue() { return null; } @Override public Class<Button> getColumnDataType() { return Button.class; } @Override public Button getColValueFor( final StatisticalSubmissionInfo<S> statSubmInfo, final Localizer localizer) { // TODO localize Button res = StandardUIFactory.getButton("Show", null); res.addClickListener(new Button.ClickListener() { /** * */ private static final long serialVersionUID = 7188121454165685244L; @Override public void buttonClick(ClickEvent event) { SubmissionVisualizer<E, S> vis = typeDesc .newSubmissionVisualizer(typeDesc.getTypeName(localizer),localizer); Window popup = new Window(); popup.setWidth("80%"); popup.setHeight("80%"); popup.setModal(true); popup.center(); VerticalLayout layout = new VerticalLayout(); layout.setWidth("100%"); layout.setMargin(true); try { vis.initialize(exerData, statSubmInfo.getSubmissionData(), localizer, tempMan); layout.addComponent(vis.getView()); } catch (ExerciseException e) { e.printStackTrace(); layout.addComponent(StandardUIFactory .getWarningPanel("Could not load visualization!")); } popup.setContent(layout); UI.getCurrent().addWindow(popup); } }); return res; } @Override public boolean isExportable() { // would not make sense to export button-column return false; } @Override public String getColumnDescription(Localizer localizer) { return null; } } }