From 3b21698eb0f354b551da50bbc0405646091c4513 Mon Sep 17 00:00:00 2001 From: plukasew Date: Mon, 13 Aug 2018 09:29:25 -0400 Subject: [PATCH] SAK-40430: Allow SiteStats aggregator job to compensate for sakai_event data stored in a different timezone (#5866) * SAK-40430: Allow SiteStats aggregator job to compensate for sakai_event data stored in a different timezone * SAK-40430: replacing TimeService with UserTimeService and standardizing display of dates within the tool * SAK-40430: calendar mock fixup --- .../DbCalendarServiceSerializationTest.java | 18 ++- .../time/api/UserTimeService.java | 32 ++++- .../conversion/ConversionTimeService.java | 16 +++ .../time/impl/BasicTimeService.java | 16 +++ .../time/impl/UserTimeServiceImpl.java | 27 ++++ .../serialize/impl/test/MockTimeService.java | 16 +++ .../sitestats/api/StatsManager.java | 3 + .../sitestats/api/report/Report.java | 6 +- .../sitestats/api/report/ReportParams.java | 18 +-- .../src/resources/Messages.properties | 10 +- .../sitestats/impl/StatsAggregateJobImpl.java | 23 +++- .../sitestats/impl/StatsManagerImpl.java | 8 ++ .../impl/report/ReportManagerImpl.java | 44 ++++--- .../impl/report/fop/ReportXMLReader.java | 13 +- .../src/webapp/WEB-INF/components.xml | 2 +- .../sitestats/tool/facade/SakaiFacade.java | 7 +- .../tool/facade/SakaiFacadeImpl.java | 14 +- .../tool/wicket/SiteStatsApplication.java | 6 +- .../tool/wicket/components/LastJobRun.java | 30 +++-- .../wicket/components/SakaiDateTimeField.java | 124 ++++++++++++++++++ .../sitestats/tool/wicket/pages/BasePage.java | 3 - .../tool/wicket/pages/ReportDataPage.java | 24 +++- .../tool/wicket/pages/ReportsEditPage.java | 44 ++++--- .../tool/wicket/widget/VisitsWidget.java | 2 +- .../tool/wicket/widget/WidgetTabTemplate.java | 22 +++- .../tool/wicket/widget/WidgetTabs.java | 2 +- .../src/webapp/WEB-INF/applicationContext.xml | 2 +- .../src/webapp/css/sitestats.css | 13 +- .../webapp/html/components/LastJobRun.html | 8 +- .../src/webapp/html/pages/ReportDataPage.html | 2 +- .../webapp/html/pages/ReportsEditPage.html | 1 + .../webapp/html/widget/WidgetTabTemplate.html | 2 +- .../src/webapp/html/widget/WidgetTabs.html | 3 +- .../src/webapp/script/common.js | 2 +- .../src/webapp/script/reports.js | 11 -- .../src/webapp/script/sakaidatetimefield.js | 25 ++++ 36 files changed, 486 insertions(+), 113 deletions(-) create mode 100644 sitestats/sitestats-tool/src/java/org/sakaiproject/sitestats/tool/wicket/components/SakaiDateTimeField.java create mode 100644 sitestats/sitestats-tool/src/webapp/script/sakaidatetimefield.js diff --git a/calendar/calendar-impl/impl/src/test/org/sakaiproject/calendar/impl/DbCalendarServiceSerializationTest.java b/calendar/calendar-impl/impl/src/test/org/sakaiproject/calendar/impl/DbCalendarServiceSerializationTest.java index e299dec25495..97cdf1873430 100644 --- a/calendar/calendar-impl/impl/src/test/org/sakaiproject/calendar/impl/DbCalendarServiceSerializationTest.java +++ b/calendar/calendar-impl/impl/src/test/org/sakaiproject/calendar/impl/DbCalendarServiceSerializationTest.java @@ -27,6 +27,8 @@ import java.io.InputStreamReader; import java.sql.Connection; import java.sql.SQLException; +import java.time.Instant; +import java.time.LocalDate; import java.util.ArrayList; import java.util.Date; import java.util.GregorianCalendar; @@ -439,7 +441,21 @@ public String dateFormatLong(Date date, Locale locale) { public String dateTimeFormatLong(Date date, Locale locale) { return null; } - + + @Override + public String shortLocalizedTimestamp(Instant instant, TimeZone timezone, Locale locale) { + return null; + } + + @Override + public String shortLocalizedTimestamp(Instant instant, Locale locale) { + return null; + } + + @Override + public String shortLocalizedDate(LocalDate date, Locale locale) { + return null; + } }; services = new HashMap(); services.put("sqlservice", sqlService); diff --git a/kernel/api/src/main/java/org/sakaiproject/time/api/UserTimeService.java b/kernel/api/src/main/java/org/sakaiproject/time/api/UserTimeService.java index a723377d67f8..8fcb8d90088b 100644 --- a/kernel/api/src/main/java/org/sakaiproject/time/api/UserTimeService.java +++ b/kernel/api/src/main/java/org/sakaiproject/time/api/UserTimeService.java @@ -1,5 +1,7 @@ package org.sakaiproject.time.api; +import java.time.Instant; +import java.time.LocalDate; import java.util.Date; import java.util.Locale; import java.util.TimeZone; @@ -42,5 +44,33 @@ public interface UserTimeService { */ public String dateTimeFormatLong(Date date, Locale locale); - + /** + * Formats a point in time, in the given time zone, for display to the user in a concise way that still presents all relevant information + * including date, time, and time zone. + * + * @param instant the instant in time + * @param timezone the time zone to use when displaying the date + * @param locale the locale to use when formatting the date for display + * @return a formatted date/time for presentation to the user + */ + public String shortLocalizedTimestamp(Instant instant, TimeZone timezone, Locale locale); + + /** + * Formats a point in time, in the user's time zone, for display to the user in a concise way that still presents all relevant information + * including date, time, and time zone. + * + * @param instant the instant in time + * @param locale the locale to use when formatting the date for display + * @return a formatted date/time for presentation to the user + */ + public String shortLocalizedTimestamp(Instant instant, Locale locale); + + /** + * Formats a date (month/day/year) in a concise but easily understood format for the given locale. + * Typically presents unambiguous month/day/year values as opposed to a purely 2-digit value for each. + * @param date month/day/year value + * @param locale the locale for use when formatting the date for display + * @return a formatted date for presentation to the user + */ + public String shortLocalizedDate(LocalDate date, Locale locale); } diff --git a/kernel/kernel-impl/src/main/java/org/sakaiproject/content/impl/serialize/impl/conversion/ConversionTimeService.java b/kernel/kernel-impl/src/main/java/org/sakaiproject/content/impl/serialize/impl/conversion/ConversionTimeService.java index 290ca83b42ed..70cc2461c05b 100644 --- a/kernel/kernel-impl/src/main/java/org/sakaiproject/content/impl/serialize/impl/conversion/ConversionTimeService.java +++ b/kernel/kernel-impl/src/main/java/org/sakaiproject/content/impl/serialize/impl/conversion/ConversionTimeService.java @@ -21,6 +21,8 @@ package org.sakaiproject.content.impl.serialize.impl.conversion; +import java.time.Instant; +import java.time.LocalDate; import java.util.Date; import java.util.GregorianCalendar; import java.util.Locale; @@ -197,4 +199,18 @@ public String dateTimeFormatLong(Date date, Locale locale) { throw new UnsupportedOperationException("This class is only to be used for conversion purposes"); } + @Override + public String shortLocalizedTimestamp(Instant instant, TimeZone timezone, Locale locale) { + throw new UnsupportedOperationException("This class is only to be used for conversion purposes"); + } + + @Override + public String shortLocalizedTimestamp(Instant instant, Locale locale) { + throw new UnsupportedOperationException("This class is only to be used for conversion purposes"); + } + + @Override + public String shortLocalizedDate(LocalDate date, Locale locale) { + throw new UnsupportedOperationException("This class is only to be used for conversion purposes"); + } } diff --git a/kernel/kernel-impl/src/main/java/org/sakaiproject/time/impl/BasicTimeService.java b/kernel/kernel-impl/src/main/java/org/sakaiproject/time/impl/BasicTimeService.java index ad31f67e7e8b..6cda6495390a 100644 --- a/kernel/kernel-impl/src/main/java/org/sakaiproject/time/impl/BasicTimeService.java +++ b/kernel/kernel-impl/src/main/java/org/sakaiproject/time/impl/BasicTimeService.java @@ -24,6 +24,8 @@ import java.text.DateFormat; import java.text.SimpleDateFormat; import java.time.Clock; +import java.time.Instant; +import java.time.LocalDate; import java.util.Date; import java.util.GregorianCalendar; import java.util.Hashtable; @@ -1021,4 +1023,18 @@ public String dateTimeFormatLong(Date date, Locale locale) { return userTimeService.dateTimeFormatLong(date, locale); } + @Override + public String shortLocalizedTimestamp(Instant instant, TimeZone timezone, Locale locale) { + return userTimeService.shortLocalizedTimestamp(instant, timezone, locale); + } + + @Override + public String shortLocalizedTimestamp(Instant instant, Locale locale) { + return userTimeService.shortLocalizedTimestamp(instant, locale); + } + + @Override + public String shortLocalizedDate(LocalDate date, Locale locale) { + return userTimeService.shortLocalizedDate(date, locale); + } } diff --git a/kernel/kernel-impl/src/main/java/org/sakaiproject/time/impl/UserTimeServiceImpl.java b/kernel/kernel-impl/src/main/java/org/sakaiproject/time/impl/UserTimeServiceImpl.java index f6717dd27e40..9479d7d38f47 100644 --- a/kernel/kernel-impl/src/main/java/org/sakaiproject/time/impl/UserTimeServiceImpl.java +++ b/kernel/kernel-impl/src/main/java/org/sakaiproject/time/impl/UserTimeServiceImpl.java @@ -1,6 +1,13 @@ package org.sakaiproject.time.impl; import java.text.DateFormat; +import java.time.Instant; +import java.time.LocalDate; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.time.format.FormatStyle; +import java.time.format.TextStyle; import java.util.Date; import java.util.Locale; import java.util.TimeZone; @@ -116,4 +123,24 @@ public String dateTimeFormatLong(Date date, Locale locale) { return d; } + @Override + public String shortLocalizedTimestamp(Instant instant, TimeZone timezone, Locale locale) { + ZonedDateTime userDate = ZonedDateTime.ofInstant(instant, timezone.toZoneId()); + DateTimeFormatter userFormatter = new DateTimeFormatterBuilder() + .appendLocalized(FormatStyle.MEDIUM, FormatStyle.SHORT) + .appendLiteral(" ").appendZoneText(TextStyle.SHORT) + .toFormatter(locale); + return userDate.format(userFormatter); + } + + @Override + public String shortLocalizedTimestamp(Instant instant, Locale locale) { + return shortLocalizedTimestamp(instant, getLocalTimeZone(), locale); + } + + @Override + public String shortLocalizedDate(LocalDate date, Locale locale) { + DateTimeFormatter df = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM).withLocale(locale); + return date.format(df); + } } diff --git a/kernel/kernel-impl/src/test/java/org/sakaiproject/content/impl/serialize/impl/test/MockTimeService.java b/kernel/kernel-impl/src/test/java/org/sakaiproject/content/impl/serialize/impl/test/MockTimeService.java index 0d5666e756b1..5390ffd94cae 100644 --- a/kernel/kernel-impl/src/test/java/org/sakaiproject/content/impl/serialize/impl/test/MockTimeService.java +++ b/kernel/kernel-impl/src/test/java/org/sakaiproject/content/impl/serialize/impl/test/MockTimeService.java @@ -21,6 +21,8 @@ package org.sakaiproject.content.impl.serialize.impl.test; +import java.time.Instant; +import java.time.LocalDate; import java.util.Date; import java.util.GregorianCalendar; import java.util.Locale; @@ -216,4 +218,18 @@ public String dateTimeFormatLong(Date date, Locale locale) { return null; } + @Override + public String shortLocalizedTimestamp(Instant instant, TimeZone timezone, Locale locale) { + return null; + } + + @Override + public String shortLocalizedTimestamp(Instant instant, Locale locale) { + return null; + } + + @Override + public String shortLocalizedDate(LocalDate date, Locale locale) { + return null; + } } diff --git a/sitestats/sitestats-api/src/java/org/sakaiproject/sitestats/api/StatsManager.java b/sitestats/sitestats-api/src/java/org/sakaiproject/sitestats/api/StatsManager.java index 3c4d241fd02d..e276f02d825e 100644 --- a/sitestats/sitestats-api/src/java/org/sakaiproject/sitestats/api/StatsManager.java +++ b/sitestats/sitestats-api/src/java/org/sakaiproject/sitestats/api/StatsManager.java @@ -643,4 +643,7 @@ public int getResourceStatsRowCount( /** Logs an event using EventTrackingService. */ public void logEvent(Object object, String logAction, String siteId, boolean oncePerSession); + /** Get the local sakai name (from ui.service property) */ + public String getLocalSakaiName(); + } diff --git a/sitestats/sitestats-api/src/java/org/sakaiproject/sitestats/api/report/Report.java b/sitestats/sitestats-api/src/java/org/sakaiproject/sitestats/api/report/Report.java index 5ade801e4ec7..0204ea1fd940 100644 --- a/sitestats/sitestats-api/src/java/org/sakaiproject/sitestats/api/report/Report.java +++ b/sitestats/sitestats-api/src/java/org/sakaiproject/sitestats/api/report/Report.java @@ -25,7 +25,6 @@ import org.sakaiproject.sitestats.api.EventStat; import org.sakaiproject.sitestats.api.ResourceStat; import org.sakaiproject.sitestats.api.Stat; -import org.sakaiproject.time.cover.TimeService; public class Report implements Serializable { @@ -59,10 +58,7 @@ public void setReportDefinition(ReportDef reportDef) { public Date getReportGenerationDate() { return reportGenerationDate; } - /** Get the localized date the report was generated. */ - public String getLocalizedReportGenerationDate() { - return TimeService.newTime(reportGenerationDate.getTime()).toStringLocalFull(); - } + /** Set the localized date the report was generated. */ public void setReportGenerationDate(Date reportGenerationDate) { this.reportGenerationDate = reportGenerationDate; diff --git a/sitestats/sitestats-api/src/java/org/sakaiproject/sitestats/api/report/ReportParams.java b/sitestats/sitestats-api/src/java/org/sakaiproject/sitestats/api/report/ReportParams.java index b1e9bf6f2b06..95202c6cda83 100644 --- a/sitestats/sitestats-api/src/java/org/sakaiproject/sitestats/api/report/ReportParams.java +++ b/sitestats/sitestats-api/src/java/org/sakaiproject/sitestats/api/report/ReportParams.java @@ -19,8 +19,10 @@ package org.sakaiproject.sitestats.api.report; import java.io.Serializable; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; import java.util.ArrayList; -import java.util.Calendar; import java.util.Date; import java.util.List; @@ -65,14 +67,12 @@ public ReportParams(){ public ReportParams(String siteId){ this.siteId = siteId; whatToolIds.add(ReportManager.WHAT_EVENTS_ALLTOOLS); - whenFrom = new Date(); - Calendar c = Calendar.getInstance(); - c.add(Calendar.DAY_OF_MONTH, -1); - c.set(Calendar.HOUR_OF_DAY, 0); - c.set(Calendar.MINUTE, 0); - c.set(Calendar.SECOND, 0); - whenFrom = c.getTime(); - whenTo = new Date(); + // events are counted against a particular date using the server time zone, so initialize the date + // range based on the current date in that time zone + ZonedDateTime today = ZonedDateTime.now(ZoneId.systemDefault()).truncatedTo(ChronoUnit.DAYS); + ZonedDateTime yesterday = today.minusDays(1); + whenFrom = Date.from(yesterday.toInstant()); + whenTo = Date.from(today.toInstant()); } public ReportParams(String siteId, String what, List whatToolIds, List whatEventIds, String whatResourceAction, List whatResourceIds, String when, Date whenFrom, Date whenTo, String who, String whoRoleId, String whoGroupId, List whoUserIds) { diff --git a/sitestats/sitestats-bundle/src/resources/Messages.properties b/sitestats/sitestats-bundle/src/resources/Messages.properties index c476031b72f5..b798958d31ae 100644 --- a/sitestats/sitestats-bundle/src/resources/Messages.properties +++ b/sitestats/sitestats-bundle/src/resources/Messages.properties @@ -234,13 +234,13 @@ reportres_title=Report reportres_title_detailed=Report: '${title}' reportres_summ_description=Description: reportres_summ_site=Site: -reportres_summ_generatedon=Report date: +reportres_summ_generatedon=Report generated: reportres_summ_act_basedon=Activity type: reportres_summ_act_tools_selected=Tools selected: reportres_summ_act_events_selected=Events selected: reportres_summ_act_rsrc_action=Resources action: reportres_summ_act_rsrc_selected=Resources selected: -reportres_summ_timeperiod=Time period: +reportres_summ_timeperiod=Date range: reportres_summ_usr_selectiontype=User selection type: reportres_summ_usr_group_selected=Group selected: reportres_summ_usr_role_selected=Role selected: @@ -325,3 +325,9 @@ predefined_report5_description = Show users who have never visited the site. # predefined_report6_title = Users with no activity predefined_report6_description = Show users with no activity in site. + +# Dates +lastJobRun_server_time=({0} server time: {1}) +widget_server_time_msg=All dates use the {0} server time zone. +report_server_time_zone=({0} server time zone) + diff --git a/sitestats/sitestats-impl/src/java/org/sakaiproject/sitestats/impl/StatsAggregateJobImpl.java b/sitestats/sitestats-impl/src/java/org/sakaiproject/sitestats/impl/StatsAggregateJobImpl.java index 873603a6b029..8027bb59d852 100644 --- a/sitestats/sitestats-impl/src/java/org/sakaiproject/sitestats/impl/StatsAggregateJobImpl.java +++ b/sitestats/sitestats-impl/src/java/org/sakaiproject/sitestats/impl/StatsAggregateJobImpl.java @@ -25,10 +25,13 @@ import java.sql.SQLException; import java.sql.Statement; import java.util.ArrayList; +import java.util.Calendar; import java.util.Date; import java.util.List; +import java.util.TimeZone; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import org.quartz.SchedulerException; @@ -47,6 +50,7 @@ public class StatsAggregateJobImpl implements StatefulJob { private int sqlBlockSize = 1000; private long startEventId = -1; private long lastEventIdInTable = -1; + private String sakaiEventTimeZone = ""; private String driverClassName = null; private String url = null; @@ -281,7 +285,16 @@ private String startJob() throws SQLException { String sessionId = null; try{ //If an exception is launched, iteration is not aborted but no event is added to event queue - date = new Date(rs.getTimestamp("EVENT_DATE").getTime()); + + // Daily events can only be counted relative to a single time zone (server time). The sakai_event table + // may be storing dates in a time zone different than this. Adjust for the sakai_event time zone if provided. + if (StringUtils.isNotBlank(sakaiEventTimeZone)) { + Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone(sakaiEventTimeZone)); + date = new Date(rs.getTimestamp("EVENT_DATE", calendar).getTime()); + } else { + date = new Date(rs.getTimestamp("EVENT_DATE").getTime()); + } + event = rs.getString("EVENT"); ref = rs.getString("REF"); sessionUser = rs.getString("SESSION_USER"); @@ -633,6 +646,14 @@ public void setStartEventId(long startEventId) { this.startEventId = startEventId; } + public String getSakaiEventTimeZone() { + return sakaiEventTimeZone; + } + + public void setSakaiEventTimeZone(String timeZone) { + sakaiEventTimeZone = timeZone; + } + public String getDriverClassName() { return driverClassName; } diff --git a/sitestats/sitestats-impl/src/java/org/sakaiproject/sitestats/impl/StatsManagerImpl.java b/sitestats/sitestats-impl/src/java/org/sakaiproject/sitestats/impl/StatsManagerImpl.java index 2ef271471e4f..52aa093ec63d 100644 --- a/sitestats/sitestats-impl/src/java/org/sakaiproject/sitestats/impl/StatsManagerImpl.java +++ b/sitestats/sitestats-impl/src/java/org/sakaiproject/sitestats/impl/StatsManagerImpl.java @@ -3885,6 +3885,14 @@ public void logEvent(Object object, String logAction, String siteId, boolean onc } } + /* (non-Javadoc) + * @see org.sakaiproject.sitestats.api.StatsManager#getLocalSakaiName() + */ + @Override + public String getLocalSakaiName() { + return M_scs.getString("ui.service", "Sakai"); + } + private void checkForEventContextSupport() { try{ Event.class.getMethod("getContext", null); diff --git a/sitestats/sitestats-impl/src/java/org/sakaiproject/sitestats/impl/report/ReportManagerImpl.java b/sitestats/sitestats-impl/src/java/org/sakaiproject/sitestats/impl/report/ReportManagerImpl.java index 448f350b48de..23df64779940 100644 --- a/sitestats/sitestats-impl/src/java/org/sakaiproject/sitestats/impl/report/ReportManagerImpl.java +++ b/sitestats/sitestats-impl/src/java/org/sakaiproject/sitestats/impl/report/ReportManagerImpl.java @@ -22,6 +22,9 @@ import java.io.IOException; import java.io.InputStream; import java.text.SimpleDateFormat; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; @@ -96,8 +99,7 @@ import org.sakaiproject.sitestats.impl.report.fop.LibraryURIResolver; import org.sakaiproject.sitestats.impl.report.fop.ReportInputSource; import org.sakaiproject.sitestats.impl.report.fop.ReportXMLReader; -import org.sakaiproject.time.api.Time; -import org.sakaiproject.time.api.TimeService; +import org.sakaiproject.time.api.UserTimeService; import org.sakaiproject.tool.api.Placement; import org.sakaiproject.tool.api.ToolManager; import org.sakaiproject.user.api.User; @@ -133,7 +135,7 @@ public class ReportManagerImpl extends HibernateDaoSupport implements ReportMana private UserDirectoryService M_uds; private ContentHostingService M_chs; private ToolManager M_tm; - private TimeService M_ts; + private UserTimeService M_uts; private EventTrackingService M_ets; private MemoryService M_ms; @@ -171,11 +173,11 @@ public void setContentService(ContentHostingService contentService) { public void setToolManager(ToolManager toolManager) { this.M_tm = toolManager; } - - public void setTimeService(TimeService timeService) { - this.M_ts = timeService; + + public void setUserTimeService(UserTimeService timeService) { + M_uts = timeService; } - + public void setEventTrackingService(EventTrackingService eventTrackingService) { this.M_ets = eventTrackingService; } @@ -804,7 +806,8 @@ public byte[] getReportAsExcel(Report report, String sheetName) { row.createCell(ix++).setCellValue(rs.getResourceAction()); } if(isReportColumnAvailable(report.getReportDefinition().getReportParams(), StatsManager.T_DATE)) { - row.createCell(ix++).setCellValue(se.getDate().toString()); + java.sql.Date sqlDate = (java.sql.Date) se.getDate(); + row.createCell(ix++).setCellValue(M_uts.shortLocalizedDate(sqlDate.toLocalDate(), msgs.getLocale())); } if(isReportColumnAvailable(report.getReportDefinition().getReportParams(), StatsManager.T_DATEMONTH)) { row.createCell(ix++).setCellValue(dateMonthFrmt.format(se.getDate())); @@ -813,7 +816,8 @@ public byte[] getReportAsExcel(Report report, String sheetName) { row.createCell(ix++).setCellValue(dateYearFrmt.format(se.getDate())); } if(isReportColumnAvailable(report.getReportDefinition().getReportParams(), StatsManager.T_LASTDATE)) { - row.createCell(ix++).setCellValue(se.getDate().toString()); + java.sql.Date sqlDate = (java.sql.Date) se.getDate(); + row.createCell(ix++).setCellValue(M_uts.shortLocalizedDate(sqlDate.toLocalDate(), msgs.getLocale())); } if(report.getReportDefinition().getReportParams().getSiteId() != null && !"".equals(report.getReportDefinition().getReportParams().getSiteId())) { } @@ -1040,7 +1044,8 @@ public String getReportAsCsv(Report report) { if(!isFirst) { sb.append(","); } - appendQuoted(sb, se.getDate().toString()); + java.sql.Date sqlDate = (java.sql.Date) se.getDate(); + appendQuoted(sb, M_uts.shortLocalizedDate(sqlDate.toLocalDate(), msgs.getLocale())); isFirst = false; } // date (year-month) @@ -1064,7 +1069,8 @@ public String getReportAsCsv(Report report) { if(!isFirst) { sb.append(","); } - appendQuoted(sb, se.getDate().toString()); + java.sql.Date sqlDate = (java.sql.Date) se.getDate(); + appendQuoted(sb, M_uts.shortLocalizedDate(sqlDate.toLocalDate(), msgs.getLocale())); isFirst = false; } // total @@ -1347,9 +1353,11 @@ public boolean isStringLocalized(String string) { * @see org.sakaiproject.sitestats.api.report.ReportFormattedParams#getReportGenerationDate(org.sakaiproject.sitestats.api.report.Report) */ public String getReportGenerationDate(Report report) { - if(report.getReportGenerationDate() == null) + if(report.getReportGenerationDate() == null) { report.setReportGenerationDate(new Date()); - return report.getLocalizedReportGenerationDate(); + } + Instant time = report.getReportGenerationDate().toInstant(); + return M_uts.shortLocalizedTimestamp(time, msgs.getLocale()); } /* (non-Javadoc) @@ -1532,9 +1540,13 @@ public String getReportTimePeriod(Report report) { if(report.getReportDefinition().getReportParams().getWhen().equals(ReportManager.WHEN_ALL)){ return msgs.getString("report_when_all"); }else{ - Time from = M_ts.newTime(report.getReportDefinition().getReportParams().getWhenFrom().getTime()); - Time to = M_ts.newTime(report.getReportDefinition().getReportParams().getWhenTo().getTime()); - return from.toStringLocalFull() + " - " + to.toStringLocalFull(); + ReportParams rp = report.getReportDefinition().getReportParams(); + ZonedDateTime from = ZonedDateTime.ofInstant(rp.getWhenFrom().toInstant(), ZoneId.systemDefault()); + ZonedDateTime to = ZonedDateTime.ofInstant(rp.getWhenTo().toInstant(), ZoneId.systemDefault()); + String timeZoneMsg = msgs.getFormattedMessage("report_server_time_zone", M_sm.getLocalSakaiName()); + + return M_uts.shortLocalizedDate(from.toLocalDate(), msgs.getLocale()) + + " - " + M_uts.shortLocalizedDate(to.toLocalDate(), msgs.getLocale()) + " " + timeZoneMsg; } } diff --git a/sitestats/sitestats-impl/src/java/org/sakaiproject/sitestats/impl/report/fop/ReportXMLReader.java b/sitestats/sitestats-impl/src/java/org/sakaiproject/sitestats/impl/report/fop/ReportXMLReader.java index e0d3613c23a0..93565898a6a0 100644 --- a/sitestats/sitestats-impl/src/java/org/sakaiproject/sitestats/impl/report/fop/ReportXMLReader.java +++ b/sitestats/sitestats-impl/src/java/org/sakaiproject/sitestats/impl/report/fop/ReportXMLReader.java @@ -41,7 +41,7 @@ import org.sakaiproject.sitestats.api.report.Report; import org.sakaiproject.sitestats.api.report.ReportManager; import org.sakaiproject.sitestats.api.report.ReportParams; -import org.sakaiproject.time.api.TimeService; +import org.sakaiproject.time.api.UserTimeService; import org.sakaiproject.user.api.User; import org.sakaiproject.user.api.UserDirectoryService; import org.sakaiproject.user.api.UserNotDefinedException; @@ -58,7 +58,7 @@ public class ReportXMLReader extends AbstractObjectReader { private SimpleDateFormat dateYearFrmt = new SimpleDateFormat("yyyy"); /** Sakai services */ - private TimeService M_ts = (TimeService) ComponentManager.get(TimeService.class.getName()); + private UserTimeService M_uts = (UserTimeService) ComponentManager.get(UserTimeService.class.getName()); private SiteService M_ss = (SiteService) ComponentManager.get(SiteService.class.getName()); private UserDirectoryService M_uds = (UserDirectoryService) ComponentManager.get(UserDirectoryService.class.getName()); private StatsManager M_sm = (StatsManager) ComponentManager.get(StatsManager.class.getName()); @@ -361,7 +361,9 @@ private void generateReportTable(List data, ReportParams params) if(showDate) { java.util.Date date = cs.getDate(); if(M_rm.isReportColumnAvailable(params, StatsManager.T_DATE)) { - handler.element("date", date == null? "" :M_ts.newTime(date.getTime()).toStringLocalDate()); + // under the hood this is an sql.Date and has no time component + java.sql.Date sqlDate = (java.sql.Date) date; + handler.element("date", date == null ? "" : M_uts.shortLocalizedDate(sqlDate.toLocalDate(), msgs.getLocale())); }else if(M_rm.isReportColumnAvailable(params, StatsManager.T_DATEMONTH)) { handler.element("date", date == null? "" :dateMonthFrmt.format(date)); }else if(M_rm.isReportColumnAvailable(params, StatsManager.T_DATEYEAR)) { @@ -369,8 +371,9 @@ private void generateReportTable(List data, ReportParams params) } } if(showLastDate) { - java.util.Date date = cs.getDate(); - handler.element("lastdate", date == null? "" :M_ts.newTime(date.getTime()).toStringLocalDate()); + // under the hood this is an sql.Date and has no time component + java.sql.Date sqlDate = (java.sql.Date) cs.getDate(); + handler.element("lastdate", sqlDate == null? "" : M_uts.shortLocalizedDate(sqlDate.toLocalDate(), msgs.getLocale())); } if(showTotal) { handler.element("total", String.valueOf(cs.getCount())); diff --git a/sitestats/sitestats-impl/src/webapp/WEB-INF/components.xml b/sitestats/sitestats-impl/src/webapp/WEB-INF/components.xml index 1068663b8be8..b0c937506ad0 100644 --- a/sitestats/sitestats-impl/src/webapp/WEB-INF/components.xml +++ b/sitestats/sitestats-impl/src/webapp/WEB-INF/components.xml @@ -154,7 +154,7 @@ - + diff --git a/sitestats/sitestats-tool/src/java/org/sakaiproject/sitestats/tool/facade/SakaiFacade.java b/sitestats/sitestats-tool/src/java/org/sakaiproject/sitestats/tool/facade/SakaiFacade.java index 61369bd7970f..775aa5320818 100644 --- a/sitestats/sitestats-tool/src/java/org/sakaiproject/sitestats/tool/facade/SakaiFacade.java +++ b/sitestats/sitestats-tool/src/java/org/sakaiproject/sitestats/tool/facade/SakaiFacade.java @@ -28,7 +28,7 @@ import org.sakaiproject.sitestats.api.chart.ChartService; import org.sakaiproject.sitestats.api.event.EventRegistryService; import org.sakaiproject.sitestats.api.report.ReportManager; -import org.sakaiproject.time.api.TimeService; +import org.sakaiproject.time.api.UserTimeService; import org.sakaiproject.tool.api.SessionManager; import org.sakaiproject.tool.api.ToolManager; import org.sakaiproject.user.api.UserDirectoryService; @@ -54,7 +54,6 @@ public interface SakaiFacade { public AuthzGroupService getAuthzGroupService(); public UserDirectoryService getUserDirectoryService(); public ContentHostingService getContentHostingService(); - public TimeService getTimeService(); - - + public UserTimeService getUserTimeService(); + } diff --git a/sitestats/sitestats-tool/src/java/org/sakaiproject/sitestats/tool/facade/SakaiFacadeImpl.java b/sitestats/sitestats-tool/src/java/org/sakaiproject/sitestats/tool/facade/SakaiFacadeImpl.java index 139a41e2d234..9640e06abbfc 100644 --- a/sitestats/sitestats-tool/src/java/org/sakaiproject/sitestats/tool/facade/SakaiFacadeImpl.java +++ b/sitestats/sitestats-tool/src/java/org/sakaiproject/sitestats/tool/facade/SakaiFacadeImpl.java @@ -28,7 +28,7 @@ import org.sakaiproject.sitestats.api.chart.ChartService; import org.sakaiproject.sitestats.api.event.EventRegistryService; import org.sakaiproject.sitestats.api.report.ReportManager; -import org.sakaiproject.time.api.TimeService; +import org.sakaiproject.time.api.UserTimeService; import org.sakaiproject.tool.api.SessionManager; import org.sakaiproject.tool.api.ToolManager; import org.sakaiproject.user.api.UserDirectoryService; @@ -52,7 +52,7 @@ public class SakaiFacadeImpl implements SakaiFacade { private transient AuthzGroupService authzGroupService; private transient UserDirectoryService userDirectoryService; private transient ContentHostingService contentHostingService; - private transient TimeService timeService; + private transient UserTimeService userTimeService; public final StatsManager getStatsManager() { return statsManager; @@ -158,12 +158,12 @@ public final void setContentHostingService(ContentHostingService contentHostingS this.contentHostingService = contentHostingService; } - public final TimeService getTimeService() { - return timeService; + @Override + public final UserTimeService getUserTimeService() { + return userTimeService; } - public final void setTimeService(TimeService timeService) { - this.timeService = timeService; + public final void setUserTimeService(UserTimeService timeService) { + this.userTimeService = timeService; } - } diff --git a/sitestats/sitestats-tool/src/java/org/sakaiproject/sitestats/tool/wicket/SiteStatsApplication.java b/sitestats/sitestats-tool/src/java/org/sakaiproject/sitestats/tool/wicket/SiteStatsApplication.java index 824912b12134..020ccb0bf961 100644 --- a/sitestats/sitestats-tool/src/java/org/sakaiproject/sitestats/tool/wicket/SiteStatsApplication.java +++ b/sitestats/sitestats-tool/src/java/org/sakaiproject/sitestats/tool/wicket/SiteStatsApplication.java @@ -42,6 +42,8 @@ import org.sakaiproject.component.cover.ServerConfigurationService; import org.sakaiproject.sitestats.tool.facade.SakaiFacade; import org.sakaiproject.sitestats.tool.wicket.pages.OverviewPage; +import org.sakaiproject.sitestats.tool.wicket.pages.PreferencesPage; +import org.sakaiproject.sitestats.tool.wicket.pages.ReportsPage; import org.sakaiproject.util.ResourceLoader; @@ -63,8 +65,10 @@ protected void init() { getResourceSettings().setResourceStreamLocator(new SiteStatsResourceStreamLocator()); getDebugSettings().setAjaxDebugModeEnabled(debug); - // Home page + // Mount pages mountPage("/home", OverviewPage.class); + mountPage("/reports", ReportsPage.class); + mountPage("/preferences", PreferencesPage.class); // On wicket session timeout, redirect to main page getApplicationSettings().setPageExpiredErrorPage(OverviewPage.class); diff --git a/sitestats/sitestats-tool/src/java/org/sakaiproject/sitestats/tool/wicket/components/LastJobRun.java b/sitestats/sitestats-tool/src/java/org/sakaiproject/sitestats/tool/wicket/components/LastJobRun.java index 915ec1e960b8..3ec146d847a0 100644 --- a/sitestats/sitestats-tool/src/java/org/sakaiproject/sitestats/tool/wicket/components/LastJobRun.java +++ b/sitestats/sitestats-tool/src/java/org/sakaiproject/sitestats/tool/wicket/components/LastJobRun.java @@ -19,15 +19,18 @@ package org.sakaiproject.sitestats.tool.wicket.components; import java.util.Date; +import java.util.TimeZone; import org.apache.wicket.markup.html.WebMarkupContainer; import org.apache.wicket.markup.html.basic.Label; import org.apache.wicket.markup.html.panel.Panel; import org.apache.wicket.model.Model; +import org.apache.wicket.model.StringResourceModel; import org.sakaiproject.sitestats.api.StatsManager; import org.sakaiproject.sitestats.api.StatsUpdateManager; import org.sakaiproject.sitestats.tool.facade.Locator; import org.sakaiproject.sitestats.tool.wicket.pages.NotAuthorizedPage; +import org.sakaiproject.time.api.UserTimeService; /** @@ -37,8 +40,6 @@ public class LastJobRun extends Panel { private static final long serialVersionUID = 1L; private String realSiteId; - private String siteId; - private String siteTitle; public LastJobRun(String id) { this(id, null); @@ -51,13 +52,19 @@ public LastJobRun(String id, String siteId) { siteId = realSiteId; } boolean allowed = Locator.getFacade().getStatsAuthz().isUserAbleToViewSiteStats(siteId); - if(allowed) { - renderBody(); - }else{ + if(!allowed) { setResponsePage(NotAuthorizedPage.class); } } + @Override + protected void onInitialize() + { + super.onInitialize(); + + renderBody(); + } + private void renderBody() { StatsManager statsManager = Locator.getFacade().getStatsManager(); StatsUpdateManager statsUpdateManager = Locator.getFacade().getStatsUpdateManager(); @@ -69,18 +76,25 @@ private void renderBody() { lastJobRun.setVisible(lastJobRunVisible); add(lastJobRun); final Label lastJobRunDate = new Label("lastJobRunDate"); + final Label lastJobRunServerDate = new Label("lastJobRunServerDate"); if(lastJobRunVisible) { try{ Date d = statsUpdateManager.getEventDateFromLatestJobRun(); - String dStr = Locator.getFacade().getTimeService().newTime(d.getTime()).toStringLocalFull(); + UserTimeService timeServ = Locator.getFacade().getUserTimeService(); + String dStr = timeServ.shortLocalizedTimestamp(d.toInstant(), getSession().getLocale()); + String serverDateStr = timeServ.shortLocalizedTimestamp(d.toInstant(), TimeZone.getDefault(), getSession().getLocale()); lastJobRunDate.setDefaultModel(new Model(dStr)); - }catch(RuntimeException e) { - lastJobRunDate.setDefaultModel(new Model()); + String localSakaiName = Locator.getFacade().getStatsManager().getLocalSakaiName(); + StringResourceModel model = new StringResourceModel("lastJobRun_server_time", getPage(), null, + new Object[] {localSakaiName, serverDateStr}); + lastJobRunServerDate.setDefaultModel(model); }catch(Exception e){ lastJobRunDate.setDefaultModel(new Model()); + lastJobRunServerDate.setDefaultModel(new Model()); } } lastJobRun.add(lastJobRunDate); + lastJobRun.add(lastJobRunServerDate); } } diff --git a/sitestats/sitestats-tool/src/java/org/sakaiproject/sitestats/tool/wicket/components/SakaiDateTimeField.java b/sitestats/sitestats-tool/src/java/org/sakaiproject/sitestats/tool/wicket/components/SakaiDateTimeField.java new file mode 100644 index 000000000000..61f30e132534 --- /dev/null +++ b/sitestats/sitestats-tool/src/java/org/sakaiproject/sitestats/tool/wicket/components/SakaiDateTimeField.java @@ -0,0 +1,124 @@ +package org.sakaiproject.sitestats.tool.wicket.components; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Locale; +import java.util.Objects; +import org.apache.wicket.AttributeModifier; +import org.apache.wicket.markup.head.IHeaderResponse; +import org.apache.wicket.markup.head.JavaScriptHeaderItem; +import org.apache.wicket.markup.head.OnDomReadyHeaderItem; +import org.apache.wicket.markup.html.form.TextField; +import org.apache.wicket.model.IModel; +import org.apache.wicket.util.convert.ConversionException; +import org.apache.wicket.util.convert.IConverter; +import org.sakaiproject.sitestats.api.StatsManager; +import static org.sakaiproject.sitestats.tool.wicket.pages.BasePage.DATEPICKERSCRIPT; +import static org.sakaiproject.sitestats.tool.wicket.pages.BasePage.JQUERYSCRIPT; +import static org.sakaiproject.sitestats.tool.wicket.pages.BasePage.JQUERYUISCRIPT; +import org.sakaiproject.util.DateFormatterUtil; + +/** + * A TextField equipped with a standard Sakai datepicker. + * + * @author plukasew + */ +public class SakaiDateTimeField extends TextField +{ + private IConverter dateConverter; + private static final String DATEPICKER_FORMAT = "yyyy-MM-dd HH:mm:ss"; + private static final String DATEPICKER_FORMAT_DATE_ONLY = "yyyy-MM-dd"; + private static final String FIELD_JS = StatsManager.SITESTATS_WEBAPP + "/script/sakaidatetimefield.js"; + + private boolean useTime = true; + private final ZoneId zoneId; + + /** + * @param id wicket id + * @param model a ZonedDateTime. Can be null if no time is set. + * @param timeZoneId timezone for this time, cannot be null. Should match the model's timezone, if model is non-null. + */ + public SakaiDateTimeField(String id, IModel model, ZoneId timeZoneId) + { + super(id, model); + Objects.requireNonNull(timeZoneId); + zoneId = timeZoneId; + } + + @Override + protected void onInitialize() + { + super.onInitialize(); + + setOutputMarkupId(true); + dateConverter = new SakaiIsoDateConverter(getMarkupId()); + + add(AttributeModifier.append("class", "sakai-datetimefield")); + } + + @Override + public IConverter getConverter(Class type) + { + return (IConverter) dateConverter; + } + + @Override + public void renderHead(IHeaderResponse response) + { + super.renderHead(response); + + response.render(JavaScriptHeaderItem.forUrl(JQUERYSCRIPT)); + response.render(JavaScriptHeaderItem.forUrl(JQUERYUISCRIPT)); + response.render(JavaScriptHeaderItem.forUrl(DATEPICKERSCRIPT)); + response.render(JavaScriptHeaderItem.forUrl(FIELD_JS)); + String pattern = useTime ? DATEPICKER_FORMAT : DATEPICKER_FORMAT_DATE_ONLY; + String formattedDate = getModelObject().format(DateTimeFormatter.ofPattern(pattern, getSession().getLocale())); + String loadFunction = useTime ? "loadJQueryDatePicker" : "loadJQueryDateOnlyPicker"; + String script = String.format("%s('%s','%s');", loadFunction, getMarkupId(), formattedDate); + response.render(OnDomReadyHeaderItem.forScript(script)); + } + + public void setUseTime(boolean value) + { + useTime = value; + } + + private class SakaiIsoDateConverter implements IConverter + { + private final String componentMarkupId; + + public SakaiIsoDateConverter(String componentMarkupId) + { + this.componentMarkupId = componentMarkupId; + } + + @Override + public ZonedDateTime convertToObject(String string, Locale locale) throws ConversionException + { + // ignore actual input string and read in iso date from hidden input instead + String isoParam = componentMarkupId + "ISO8601"; + String isoDateStr = getRequest().getRequestParameters().getParameterValue(isoParam).toString(""); + // format is like '2017-12-03T10:15:30-04:00' + if (!DateFormatterUtil.isValidISODate(isoDateStr)) + { + throw new ConversionException("Invalid ISO date: " + isoDateStr); + } + + // The string we get back from the datepicker has an offset which may or may not match the desired timezone. + // Since the user cannot choose a timezone using the datepicker, we just ignore the datepicker-provided offset + // and set the timezone from the stored zone. + LocalDateTime local = LocalDateTime.parse(isoDateStr, DateTimeFormatter.ISO_OFFSET_DATE_TIME); + return ZonedDateTime.of(local, zoneId); + } + + @Override + public String convertToString(ZonedDateTime c, Locale locale) + { + // ignore model date, it will be set via javascript on page load + return ""; + } + + } +} diff --git a/sitestats/sitestats-tool/src/java/org/sakaiproject/sitestats/tool/wicket/pages/BasePage.java b/sitestats/sitestats-tool/src/java/org/sakaiproject/sitestats/tool/wicket/pages/BasePage.java index 21d59aafa910..938214a2125e 100644 --- a/sitestats/sitestats-tool/src/java/org/sakaiproject/sitestats/tool/wicket/pages/BasePage.java +++ b/sitestats/sitestats-tool/src/java/org/sakaiproject/sitestats/tool/wicket/pages/BasePage.java @@ -31,7 +31,6 @@ import org.apache.wicket.model.StringResourceModel; import org.apache.wicket.markup.head.CssHeaderItem; import org.apache.wicket.markup.head.JavaScriptHeaderItem; -import org.apache.wicket.markup.head.OnLoadHeaderItem; import org.apache.wicket.markup.head.StringHeaderItem; import org.apache.wicket.devutils.debugbar.DebugBar; @@ -44,7 +43,6 @@ public class BasePage extends WebPage implements IHeaderContributor { private static final long serialVersionUID = 1L; public static final String COMMONSCRIPT = StatsManager.SITESTATS_WEBAPP+"/script/common.js"; public static final String JQUERYSCRIPT = "/library/webjars/jquery/1.12.4/jquery.min.js"; - public static final String BODY_ONLOAD_ADDTL = "setMainFrameHeightNoScroll(window.name, 0, 400)"; public static final String LAST_PAGE = "lastSiteStatsPage"; public static final String DATEPICKERSCRIPT = "/library/js/lang-datepicker/lang-datepicker.js"; public static final String JQUERYUISCRIPT = "/library/webjars/jquery-ui/1.12.1/jquery-ui.min.js"; @@ -71,7 +69,6 @@ public void renderHead(IHeaderResponse response) { //get the Sakai skin header fragment from the request attribute HttpServletRequest request = (HttpServletRequest) getRequest().getContainerRequest(); response.render(StringHeaderItem.forString(request.getAttribute("sakai.html.head").toString())); - response.render(OnLoadHeaderItem.forScript(BODY_ONLOAD_ADDTL)); response.render(JavaScriptHeaderItem.forUrl(COMMONSCRIPT)); // include (this) tool style (CSS) diff --git a/sitestats/sitestats-tool/src/java/org/sakaiproject/sitestats/tool/wicket/pages/ReportDataPage.java b/sitestats/sitestats-tool/src/java/org/sakaiproject/sitestats/tool/wicket/pages/ReportDataPage.java index 1c59ea4f5df9..c2ed4951fb03 100644 --- a/sitestats/sitestats-tool/src/java/org/sakaiproject/sitestats/tool/wicket/pages/ReportDataPage.java +++ b/sitestats/sitestats-tool/src/java/org/sakaiproject/sitestats/tool/wicket/pages/ReportDataPage.java @@ -29,6 +29,7 @@ import lombok.extern.slf4j.Slf4j; import org.apache.wicket.AttributeModifier; import org.apache.wicket.Component; +import org.apache.wicket.Session; import org.apache.wicket.request.mapper.parameter.PageParameters; import org.apache.wicket.request.cycle.RequestCycle; import org.apache.wicket.extensions.markup.html.repeater.data.table.IColumn; @@ -74,6 +75,7 @@ import org.sakaiproject.sitestats.tool.wicket.components.SakaiDataTable; import org.sakaiproject.sitestats.tool.wicket.models.ReportDefModel; import org.sakaiproject.sitestats.tool.wicket.providers.ReportsDataProvider; +import org.sakaiproject.time.api.UserTimeService; import org.sakaiproject.user.api.UserNotDefinedException; /** @@ -256,7 +258,7 @@ public byte[] getImageData(int width, int height) { trReportUserSelection.add(new Label("reportUserSelection")); add(trReportUserSelection); - add(new Label("report.localizedReportGenerationDate")); + add(new Label("reportGenerationDate")); // buttons @@ -484,7 +486,12 @@ public void populateItem(Item item, String componentId, IModel model) { }); } if(Locator.getFacade().getReportManager().isReportColumnAvailable(reportParams, StatsManager.T_DATE)) { - columns.add(new PropertyColumn(new ResourceModel("th_date"), columnsSortable ? ReportsDataProvider.COL_DATE : null, ReportsDataProvider.COL_DATE)); + columns.add(new PropertyColumn(new ResourceModel("th_date"), columnsSortable ? ReportsDataProvider.COL_DATE : null, ReportsDataProvider.COL_DATE) { + @Override + public void populateItem(Item item, String componentId, IModel model) { + item.add(new Label(componentId, getLocalizedDate((Stat) model.getObject()))); + } + }); } if(Locator.getFacade().getReportManager().isReportColumnAvailable(reportParams, StatsManager.T_DATEMONTH)) { columns.add(new PropertyColumn(new ResourceModel("th_date"), columnsSortable ? ReportsDataProvider.COL_DATE : null, ReportsDataProvider.COL_DATE) { @@ -507,7 +514,12 @@ public void populateItem(Item item, String componentId, IModel model) { }); } if(Locator.getFacade().getReportManager().isReportColumnAvailable(reportParams, StatsManager.T_LASTDATE)) { - columns.add(new PropertyColumn(new ResourceModel("th_lastdate"), columnsSortable ? ReportsDataProvider.COL_DATE : null, ReportsDataProvider.COL_DATE)); + columns.add(new PropertyColumn(new ResourceModel("th_lastdate"), columnsSortable ? ReportsDataProvider.COL_DATE : null, ReportsDataProvider.COL_DATE) { + @Override + public void populateItem(Item item, String componentId, IModel model) { + item.add(new Label(componentId, getLocalizedDate((Stat) model.getObject()))); + } + }); } if(Locator.getFacade().getReportManager().isReportColumnAvailable(reportParams, StatsManager.T_TOTAL)) { columns.add(new PropertyColumn(new ResourceModel("th_total"), columnsSortable ? ReportsDataProvider.COL_TOTAL : null, "count")); @@ -533,6 +545,12 @@ public void populateItem(Item item, String componentId, IModel model) { } return columns; } + + private static String getLocalizedDate(Stat stat) { + java.sql.Date sqlDate = (java.sql.Date) stat.getDate(); + UserTimeService timeServ = Locator.getFacade().getUserTimeService(); + return timeServ.shortLocalizedDate(sqlDate.toLocalDate(), Session.get().getLocale()); + } private byte[] getChartImage() { if(chartImage == null) { diff --git a/sitestats/sitestats-tool/src/java/org/sakaiproject/sitestats/tool/wicket/pages/ReportsEditPage.java b/sitestats/sitestats-tool/src/java/org/sakaiproject/sitestats/tool/wicket/pages/ReportsEditPage.java index 1691ed3cb2e8..f52959498591 100644 --- a/sitestats/sitestats-tool/src/java/org/sakaiproject/sitestats/tool/wicket/pages/ReportsEditPage.java +++ b/sitestats/sitestats-tool/src/java/org/sakaiproject/sitestats/tool/wicket/pages/ReportsEditPage.java @@ -21,17 +21,19 @@ import java.text.Collator; import java.text.ParseException; import java.text.RuleBasedCollator; +import java.time.ZoneId; +import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; +import java.util.Date; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.concurrent.locks.ReentrantLock; -import java.util.Locale; import lombok.extern.slf4j.Slf4j; import org.apache.wicket.AttributeModifier; @@ -63,8 +65,6 @@ import org.apache.wicket.model.PropertyModel; import org.apache.wicket.model.ResourceModel; import org.apache.wicket.model.StringResourceModel; -import org.apache.wicket.util.convert.IConverter; -import org.apache.wicket.util.convert.converter.IntegerConverter; import org.sakaiproject.authz.api.Role; import org.sakaiproject.event.api.EventTrackingService; @@ -88,12 +88,12 @@ import org.sakaiproject.sitestats.tool.wicket.components.Menus; import org.sakaiproject.sitestats.tool.wicket.components.StylableSelectOptions; import org.sakaiproject.sitestats.tool.wicket.components.StylableSelectOptionsGroup; +import org.sakaiproject.sitestats.tool.wicket.components.SakaiDateTimeField; import org.sakaiproject.sitestats.tool.wicket.models.EventModel; import org.sakaiproject.sitestats.tool.wicket.models.ReportDefModel; import org.sakaiproject.sitestats.tool.wicket.models.ToolModel; import org.sakaiproject.user.api.User; import org.sakaiproject.user.api.UserNotDefinedException; -import org.sakaiproject.util.DateFormatterUtil; import org.sakaiproject.util.Web; /** @@ -128,10 +128,8 @@ public class ReportsEditPage extends BasePage { private boolean usersLoaded = false; private transient Collator collator = Collator.getInstance(); - - private static String HIDDEN_WHENFROM_ISO8601 = "whenFromISO8601"; - private static String HIDDEN_WHENTO_ISO8601 = "whenToISO8601"; - private static String DATEPICKER_FORMAT = "yyyy-MM-dd HH:mm:ss"; + + private ZonedDateTime startDate, endDate; { try{ @@ -210,8 +208,6 @@ public void renderHead(IHeaderResponse response) { onDomReady.append("checkHowSelection();"); onDomReady.append("checkReportDetails();"); onDomReady.append("checkHowChartSelection();"); - onDomReady.append(String.format("loadJQueryDatePicker('%s','%s');", "whenFrom", DateFormatterUtil.format(getReportParams().getWhenFrom(), DATEPICKER_FORMAT, getSession().getLocale()))); - onDomReady.append(String.format("loadJQueryDatePicker('%s','%s');", "whenTo", DateFormatterUtil.format(getReportParams().getWhenTo(), DATEPICKER_FORMAT, getSession().getLocale()))); response.render(OnDomReadyHeaderItem.forScript(onDomReady.toString())); } @@ -553,10 +549,23 @@ public String getIdValue(Object object, int index) { when.setMarkupId("when"); when.setOutputMarkupId(true); form.add(when); + + String localSakaiName = Locator.getFacade().getStatsManager().getLocalSakaiName(); + StringResourceModel model = new StringResourceModel("report_server_time_zone", getPage(), null, + new Object[] {localSakaiName}); + form.add(new Label("reportParams.when.serverTimeZone", model)); // custom dates - form.add(new TextField("whenFrom", Model.of(""))); - form.add(new TextField("whenTo", Model.of(""))); + // date range for reports uses the server time zone to match how the events are counted + ZoneId sys = ZoneId.systemDefault(); + startDate = ZonedDateTime.ofInstant(getReportParams().getWhenFrom().toInstant(), sys); + endDate = ZonedDateTime.ofInstant(getReportParams().getWhenTo().toInstant(), sys); + SakaiDateTimeField startDateField = new SakaiDateTimeField("whenFrom", new PropertyModel<>(this, "startDate"), sys); + startDateField.setUseTime(false); + form.add(startDateField); + SakaiDateTimeField endDateField = new SakaiDateTimeField("whenTo", new PropertyModel<>(this, "endDate"), sys); + endDateField.setUseTime(false); + form.add(endDateField); } @@ -1364,15 +1373,8 @@ public void updateModel() { } private void setISODates(){ - String whenFrom = getRequest().getRequestParameters().getParameterValue(HIDDEN_WHENFROM_ISO8601).toString(""); - String whenTo = getRequest().getRequestParameters().getParameterValue(HIDDEN_WHENTO_ISO8601).toString(""); - if(DateFormatterUtil.isValidISODate(whenFrom)){ - getReportParams().setWhenFrom(DateFormatterUtil.parseISODate(whenFrom)); - } - - if(DateFormatterUtil.isValidISODate(whenTo)){ - getReportParams().setWhenTo(DateFormatterUtil.parseISODate(whenTo)); - } + getReportParams().setWhenFrom(Date.from(startDate.toInstant())); + getReportParams().setWhenTo(Date.from(endDate.toInstant())); } } diff --git a/sitestats/sitestats-tool/src/java/org/sakaiproject/sitestats/tool/wicket/widget/VisitsWidget.java b/sitestats/sitestats-tool/src/java/org/sakaiproject/sitestats/tool/wicket/widget/VisitsWidget.java index 3baced6d9331..5d6acf142aa6 100644 --- a/sitestats/sitestats-tool/src/java/org/sakaiproject/sitestats/tool/wicket/widget/VisitsWidget.java +++ b/sitestats/sitestats-tool/src/java/org/sakaiproject/sitestats/tool/wicket/widget/VisitsWidget.java @@ -576,7 +576,7 @@ public ReportDef getTableReportDefinition() { r.setReportParams(rp); return r; - } + } }; return wTab; } diff --git a/sitestats/sitestats-tool/src/java/org/sakaiproject/sitestats/tool/wicket/widget/WidgetTabTemplate.java b/sitestats/sitestats-tool/src/java/org/sakaiproject/sitestats/tool/wicket/widget/WidgetTabTemplate.java index da9140258d72..d31df1b65c7c 100644 --- a/sitestats/sitestats-tool/src/java/org/sakaiproject/sitestats/tool/wicket/widget/WidgetTabTemplate.java +++ b/sitestats/sitestats-tool/src/java/org/sakaiproject/sitestats/tool/wicket/widget/WidgetTabTemplate.java @@ -22,6 +22,7 @@ import java.util.Arrays; import java.util.Iterator; import java.util.List; +import java.util.Optional; import java.util.Set; import lombok.extern.slf4j.Slf4j; @@ -33,12 +34,16 @@ import org.apache.wicket.markup.ComponentTag; import org.apache.wicket.markup.MarkupStream; import org.apache.wicket.markup.html.WebMarkupContainer; +import org.apache.wicket.markup.html.basic.Label; import org.apache.wicket.markup.html.form.IChoiceRenderer; import org.apache.wicket.markup.html.link.Link; import org.apache.wicket.markup.html.link.StatelessLink; import org.apache.wicket.markup.html.panel.Panel; import org.apache.wicket.model.CompoundPropertyModel; +import org.apache.wicket.model.IModel; +import org.apache.wicket.model.Model; import org.apache.wicket.model.ResourceModel; +import org.apache.wicket.model.StringResourceModel; import org.sakaiproject.authz.api.Role; import org.sakaiproject.exception.IdUnusedException; @@ -110,7 +115,19 @@ public WidgetTabTemplate(String id, String siteId) { public abstract boolean useChartReportDefinitionForTable(); public abstract List getFilters(); - + + /** + * Gets an optional message that will be displayed at the bottom of the tab. + * @return a model containing the message string + */ + protected Optional> getFooterMsg() + { + String localSakaiName = Locator.getFacade().getStatsManager().getLocalSakaiName(); + StringResourceModel model = new StringResourceModel("widget_server_time_msg", getPage(), null, + new Object[] {localSakaiName}); + return Optional.of(model); + } + @Override protected void onBeforeRender() { // update data @@ -138,6 +155,9 @@ protected void onBeforeRender() { renderTable(); tabTemplateRendered = true; + Optional> footerModel = getFooterMsg(); + add(new Label("widgetFooterMsg", footerModel.orElseGet(() -> Model.of(""))).setVisible(footerModel.isPresent())); + super.onBeforeRender(); } diff --git a/sitestats/sitestats-tool/src/java/org/sakaiproject/sitestats/tool/wicket/widget/WidgetTabs.java b/sitestats/sitestats-tool/src/java/org/sakaiproject/sitestats/tool/wicket/widget/WidgetTabs.java index b625787fda0c..04312f219e84 100644 --- a/sitestats/sitestats-tool/src/java/org/sakaiproject/sitestats/tool/wicket/widget/WidgetTabs.java +++ b/sitestats/sitestats-tool/src/java/org/sakaiproject/sitestats/tool/wicket/widget/WidgetTabs.java @@ -81,7 +81,7 @@ protected void populateItem(LoopItem item) { add(loadSelectedTabBehavior); // select initial tab - setSelectedTab(selectedTab, false); + setSelectedTab(selectedTab, false); } protected void onBeforeRender() { diff --git a/sitestats/sitestats-tool/src/webapp/WEB-INF/applicationContext.xml b/sitestats/sitestats-tool/src/webapp/WEB-INF/applicationContext.xml index c23b95b765a6..b2382b2ae0ef 100644 --- a/sitestats/sitestats-tool/src/webapp/WEB-INF/applicationContext.xml +++ b/sitestats/sitestats-tool/src/webapp/WEB-INF/applicationContext.xml @@ -21,7 +21,7 @@ - + diff --git a/sitestats/sitestats-tool/src/webapp/css/sitestats.css b/sitestats/sitestats-tool/src/webapp/css/sitestats.css index 79d37b9d2ade..2cffd42a115f 100644 --- a/sitestats/sitestats-tool/src/webapp/css/sitestats.css +++ b/sitestats/sitestats-tool/src/webapp/css/sitestats.css @@ -326,7 +326,7 @@ table.formContainer { font-weight: bold; text-align: right; width: 150px; - vertical-align:top; + vertical-align:middle; } .formContainer select{ margin: 0.1em 0.3em; @@ -529,3 +529,14 @@ label.indnt1 { text-align: right; width: 49%; } +button.ui-datepicker-trigger { + margin: 0; + padding: 0; +} +.sakai-datetimefield { + min-width: 200px; +} +.widget .sitestats-widgetFooterMsg { + float: none; +} + diff --git a/sitestats/sitestats-tool/src/webapp/html/components/LastJobRun.html b/sitestats/sitestats-tool/src/webapp/html/components/LastJobRun.html index 1ba6ca1c60b3..15ea782b7219 100644 --- a/sitestats/sitestats-tool/src/webapp/html/components/LastJobRun.html +++ b/sitestats/sitestats-tool/src/webapp/html/components/LastJobRun.html @@ -23,10 +23,10 @@
- -   - [last_job_run_date] - [date] + + [last_job_run_date] + [date] + [date]
diff --git a/sitestats/sitestats-tool/src/webapp/html/pages/ReportDataPage.html b/sitestats/sitestats-tool/src/webapp/html/pages/ReportDataPage.html index 61dc8757dd8a..58d7344df0fa 100644 --- a/sitestats/sitestats-tool/src/webapp/html/pages/ReportDataPage.html +++ b/sitestats/sitestats-tool/src/webapp/html/pages/ReportDataPage.html @@ -154,7 +154,7 @@

- + diff --git a/sitestats/sitestats-tool/src/webapp/html/pages/ReportsEditPage.html b/sitestats/sitestats-tool/src/webapp/html/pages/ReportsEditPage.html index d29fab719657..65bf793a86d6 100644 --- a/sitestats/sitestats-tool/src/webapp/html/pages/ReportsEditPage.html +++ b/sitestats/sitestats-tool/src/webapp/html/pages/ReportsEditPage.html @@ -184,6 +184,7 @@