001 /**
002 * Copyright 2004-2013 The Kuali Foundation
003 *
004 * Licensed under the Educational Community License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.opensource.org/licenses/ecl2.php
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016 package org.kuali.hr.time.test;
017
018 import java.math.BigDecimal;
019 import java.sql.Date;
020 import java.sql.Timestamp;
021 import java.util.ArrayList;
022 import java.util.Arrays;
023 import java.util.Calendar;
024 import java.util.GregorianCalendar;
025 import java.util.HashMap;
026 import java.util.Iterator;
027 import java.util.LinkedList;
028 import java.util.List;
029 import java.util.Map;
030 import java.util.Set;
031
032 import org.apache.commons.lang3.StringUtils;
033 import org.apache.log4j.Logger;
034 import org.joda.time.DateTime;
035 import org.joda.time.DateTimeZone;
036 import org.joda.time.Days;
037 import org.joda.time.Duration;
038 import org.junit.Assert;
039 import org.kuali.hr.job.Job;
040 import org.kuali.hr.lm.accrual.AccrualCategory;
041 import org.kuali.hr.lm.accrual.service.AccrualService;
042 import org.kuali.hr.lm.leaveblock.LeaveBlock;
043 import org.kuali.hr.lm.leaveblock.service.LeaveBlockService;
044 import org.kuali.hr.lm.leavecalendar.LeaveCalendarDocument;
045 import org.kuali.hr.time.assignment.Assignment;
046 import org.kuali.hr.time.flsa.FlsaDay;
047 import org.kuali.hr.time.flsa.FlsaWeek;
048 import org.kuali.hr.time.service.base.TkServiceLocator;
049 import org.kuali.hr.time.timeblock.TimeBlock;
050 import org.kuali.hr.time.timeblock.TimeHourDetail;
051 import org.kuali.hr.time.timeblock.service.TimeBlockService;
052 import org.kuali.hr.time.timesheet.TimesheetDocument;
053 import org.kuali.hr.time.util.*;
054 import org.kuali.rice.kew.api.exception.WorkflowException;
055 import org.w3c.dom.NamedNodeMap;
056 import org.w3c.dom.Node;
057
058 import com.gargoylesoftware.htmlunit.html.HtmlCheckBoxInput;
059 import com.gargoylesoftware.htmlunit.html.HtmlElement;
060 import com.gargoylesoftware.htmlunit.html.HtmlForm;
061 import com.gargoylesoftware.htmlunit.html.HtmlInput;
062 import com.gargoylesoftware.htmlunit.html.HtmlPage;
063 import com.gargoylesoftware.htmlunit.html.HtmlSelect;
064 import com.gargoylesoftware.htmlunit.html.HtmlSubmitInput;
065 import com.gargoylesoftware.htmlunit.html.HtmlTextArea;
066
067 public class TkTestUtils {
068
069 private static final Logger LOG = Logger.getLogger(TkTestUtils.class);
070
071 public static TimesheetDocument populateBlankTimesheetDocument(Date calDate) {
072 try {
073 TimesheetDocument timesheet = TkServiceLocator.getTimesheetService().openTimesheetDocument(TKUser.getCurrentTargetPerson().getPrincipalId(),
074 TkServiceLocator.getCalendarService().getCurrentCalendarDates(TKUser.getCurrentTargetPerson().getPrincipalId(),
075 calDate));
076 for(TimeBlock timeBlock : timesheet.getTimeBlocks()){
077 TkServiceLocator.getTimeBlockService().deleteTimeBlock(timeBlock);
078 }
079
080 return timesheet;
081 } catch (WorkflowException e) {
082 throw new RuntimeException("Problem fetching document");
083 }
084 }
085
086 public static TimesheetDocument populateTimesheetDocument(Date calDate) {
087 try {
088 TimesheetDocument timesheet = TkServiceLocator.getTimesheetService().openTimesheetDocument(TKUser.getCurrentTargetPerson().getPrincipalId(),
089 TkServiceLocator.getCalendarService().getCurrentCalendarDates(TKUser.getCurrentTargetPerson().getPrincipalId(),
090 calDate));
091 for(TimeBlock timeBlock : timesheet.getTimeBlocks()){
092 TkServiceLocator.getTimeBlockService().deleteTimeBlock(timeBlock);
093 }
094
095 //refetch clean document
096 timesheet = TkServiceLocator.getTimesheetService().openTimesheetDocument(TKUser.getCurrentTargetPerson().getPrincipalId(),
097 TkServiceLocator.getCalendarService().getCurrentCalendarDates(TKUser.getCurrentTargetPerson().getPrincipalId(),
098 calDate));
099 List<TimeBlock> timeBlocks = new LinkedList<TimeBlock>();
100 for(int i = 0;i<5;i++){
101 TimeBlock timeBlock = createTimeBlock(timesheet, i+1, 10);
102 timeBlocks.add(timeBlock);
103 timesheet.setTimeBlocks(timeBlocks);
104 }
105
106 //Add a TEX accrual earn code
107 TimeBlock timeBlock = createTimeBlock(timesheet, 1, 4,"TEX");
108 timeBlocks.add(timeBlock);
109 timesheet.setTimeBlocks(timeBlocks);
110 return timesheet;
111
112 } catch (WorkflowException e) {
113 throw new RuntimeException("Problem fetching document");
114 }
115 }
116
117 //TODO populate this
118 public static void createEarnGroup(String earnGroup, List<String> earnCodes, Date asOfDate){
119
120 }
121
122 /**
123 * Helper method to create regular time blocks for use in testing.
124 *
125 * @param start
126 * @param days
127 * @param hours
128 * @param earnCode
129 * @param jobNumber
130 * @param workArea
131 * @return
132 */
133 public static List<TimeBlock> createUniformTimeBlocks(DateTime start, int days, BigDecimal hours, String earnCode, Long jobNumber, Long workArea) {
134 return TkTestUtils.createUniformTimeBlocks(start, days, hours, earnCode, jobNumber, workArea, null);
135 }
136
137 public static List<TimeBlock> createUniformTimeBlocks(DateTime start, int days, BigDecimal hours, String earnCode, Long jobNumber, Long workArea, Long task) {
138 List<TimeBlock> blocks = new ArrayList<TimeBlock>();
139
140 for (int i=0; i<days; i++) {
141 DateTime ci = start.plusDays(i);
142 DateTime co = ci.plusHours(hours.intValue());
143 TimeBlock block = TkTestUtils.createDummyTimeBlock(ci, co, hours, earnCode, jobNumber, workArea);
144 block.setTask(task);
145 blocks.add(block);
146 }
147
148 return blocks;
149 }
150
151 public static TimeBlock createDummyTimeBlock(DateTime clockIn, DateTime clockOut, BigDecimal hours, String earnCode) {
152 return TkTestUtils.createDummyTimeBlock(clockIn, clockOut, hours, earnCode, -1L, -1L);
153 }
154
155 public static TimeBlock createDummyTimeBlock(DateTime clockIn, DateTime clockOut, BigDecimal hours, String earnCode, Long jobNumber, Long workArea) {
156 TimeBlock block = new TimeBlock();
157 Timestamp ci = new Timestamp(clockIn.getMillis());
158 Timestamp co = new Timestamp(clockOut.getMillis());
159 block.setBeginTimestamp(ci);
160 block.setEndTimestamp(co);
161 block.setHours(hours);
162 block.setBeginTimeDisplay(new DateTime(ci.getTime()));
163 block.setEndTimeDisplay(new DateTime(co.getTime()));
164
165 block.setEarnCode(earnCode);
166 block.setJobNumber(jobNumber);
167 block.setWorkArea(workArea);
168
169 TimeHourDetail thd = new TimeHourDetail();
170 thd.setHours(hours);
171 thd.setEarnCode(earnCode);
172 List<TimeHourDetail> timeHourDetails = new ArrayList<TimeHourDetail>();
173 timeHourDetails.add(thd);
174 block.setTimeHourDetails(timeHourDetails);
175
176 return block;
177 }
178
179 public static TimeBlock createTimeBlock(TimesheetDocument timesheetDocument, int dayInPeriod, int numHours){
180 return createTimeBlock(timesheetDocument, dayInPeriod, numHours,"RGN");
181 }
182 public static TimeBlock createTimeBlock(TimesheetDocument timesheetDocument, int dayInPeriod, int numHours, String earnCode){
183 TimeBlock timeBlock = new TimeBlock();
184 Calendar cal = GregorianCalendar.getInstance();
185 cal.setTimeInMillis(timesheetDocument.getCalendarEntry().getBeginPeriodDateTime().getTime());
186 for(int i = 1; i< dayInPeriod;i++){
187 cal.add(Calendar.DAY_OF_MONTH, 1);
188 }
189 cal.set(Calendar.HOUR, 8);
190 cal.set(Calendar.MINUTE, 0);
191
192 timeBlock.setDocumentId(timesheetDocument.getDocumentId());
193 timeBlock.setBeginTimeDisplay(new DateTime(cal.getTimeInMillis()));
194
195 timeBlock.setBeginTimestamp(new Timestamp(cal.getTimeInMillis()));
196 timeBlock.setBeginTimestampTimezone("EST");
197 timeBlock.setEarnCode(earnCode);
198 timeBlock.setJobNumber(1L);
199 timeBlock.setWorkArea(1234L);
200 timeBlock.setTask(1L);
201 timeBlock.setHours((new BigDecimal(numHours)).setScale(TkConstants.BIG_DECIMAL_SCALE, TkConstants.BIG_DECIMAL_SCALE_ROUNDING));
202 cal.add(Calendar.HOUR, numHours);
203 timeBlock.setEndTimestamp(new Timestamp(cal.getTimeInMillis()));
204 timeBlock.setEndTimeDisplay(new DateTime(cal.getTimeInMillis()));
205
206 return timeBlock;
207 }
208
209 public static List<Job> getJobs(Date calDate){
210 return TkServiceLocator.getJobService().getJobs(TKContext.getPrincipalId(), calDate);
211 }
212 /**
213 *
214 * @param page: current html page
215 * @param criteria: The key is the field name and the value is a string array which contains the field value and the field type which can be chosen from TkTestConstants
216 * @return HtmlPage resultPage
217 * @throws Exception
218 */
219 public static HtmlPage fillOutForm(HtmlPage page, Map<String, Object> criteria) throws Exception {
220 HtmlForm lookupForm = HtmlUnitUtil.getDefaultForm(page);
221 String formFieldPrefix = "";
222 HtmlPage resultPage = null;
223 HtmlSelect select = null;
224 HtmlInput input = null;
225 HtmlCheckBoxInput checkBox = null;
226 HtmlTextArea textArea = null;
227
228
229 // Common class of both HtmlInput and HtmlTextArea -- since either of these can appear
230 // on a web-form.
231 HtmlElement htmlBasicInput = null;
232
233 Set<Map.Entry<String, Object>> entries = criteria.entrySet();
234 Iterator<Map.Entry<String, Object>> it = entries.iterator();
235
236 while (it.hasNext()) {
237 Map.Entry<String,Object> entry = it.next();
238
239 // if the field type is not <input>
240 if(entry.getValue() instanceof String[]) {
241 String key = Arrays.asList((String[])entry.getValue()).get(0).toString();
242 String value = Arrays.asList((String[])entry.getValue()).get(1).toString();
243
244 // drop-down
245 if(key.equals(TkTestConstants.FormElementTypes.DROPDOWN)) {
246
247 try {
248 select = (HtmlSelect) lookupForm.getSelectByName(formFieldPrefix + entry.getKey());
249 } catch (Exception e) {
250 select = (HtmlSelect) lookupForm.getElementById(formFieldPrefix + entry.getKey());
251 }
252 // try to get a useful option, other than the typical blank default, by getting the last option
253 // if the size of the options list is zero, then there is a problem. might as well error here with an array out of bounds.
254 resultPage = (HtmlPage) select.getOption(select.getOptionSize()-1).setSelected(true);
255 HtmlUnitUtil.createTempFile(resultPage);
256 }
257 // check box
258 else if(key.equals(TkTestConstants.FormElementTypes.CHECKBOX)) {
259 try {
260 checkBox = page.getHtmlElementById(formFieldPrefix + entry.getKey());
261 }
262 catch(Exception e) {
263 checkBox = page.getElementByName(formFieldPrefix + entry.getKey());
264 }
265 resultPage = (HtmlPage) checkBox.setChecked(Boolean.parseBoolean(value));
266 }
267 // text area
268 else if(key.equals(TkTestConstants.FormElementTypes.TEXTAREA)) {
269 try {
270 textArea = page.getHtmlElementById(formFieldPrefix + entry.getKey());
271 } catch (Exception e){
272 textArea = page.getElementByName(formFieldPrefix + entry.getKey());
273 }
274 textArea.setText(Arrays.asList((String[])entry.getValue()).get(1).toString());
275 }
276 } else {
277 try {
278 htmlBasicInput = page.getHtmlElementById(formFieldPrefix + entry.getKey());
279 if (htmlBasicInput instanceof HtmlTextArea) {
280 textArea = (HtmlTextArea)htmlBasicInput;
281 textArea.setText(entry.getValue().toString());
282 resultPage = (HtmlPage) textArea.getPage();
283 } else if (htmlBasicInput instanceof HtmlInput) {
284 input = (HtmlInput)htmlBasicInput;
285 resultPage = (HtmlPage) input.setValueAttribute(entry.getValue().toString());
286 } else {
287 LOG.error("Request to populate a non-input html form field.");
288 }
289 } catch (Exception e) {
290 htmlBasicInput = page.getElementByName(formFieldPrefix + entry.getKey());
291
292 if (htmlBasicInput instanceof HtmlTextArea) {
293 textArea = (HtmlTextArea)htmlBasicInput;
294 textArea.setText(entry.getValue().toString());
295 resultPage = (HtmlPage) textArea.getPage();
296 } else if (htmlBasicInput instanceof HtmlInput) {
297 input = (HtmlInput)htmlBasicInput;
298 resultPage = (HtmlPage) input.setValueAttribute(entry.getValue().toString());
299 } else {
300 LOG.error("Request to populate a non-input html form field.");
301 }
302 }
303 }
304 }
305 HtmlUnitUtil.createTempFile(resultPage);
306 return resultPage;
307 }
308
309 /**
310 *
311 * @param page: current html page //NOTE doesnt seem to work currently for js setting of form variables
312 * @param name: the button name
313 * @return
314 * @throws Exception
315 */
316 public static HtmlPage clickButton(HtmlPage page, String name) throws Exception {
317 HtmlForm form = HtmlUnitUtil.getDefaultForm(page);
318 HtmlSubmitInput input = form.getInputByName(name);
319 return (HtmlPage) input.click();
320 }
321
322 public static HtmlPage clickClockInOrOutButton(HtmlPage page) throws Exception {
323 HtmlForm form = HtmlUnitUtil.getDefaultForm(page);
324
325 HtmlSubmitInput input = form.getInputByName("clockAction");
326 form.getInputByName("methodToCall").setValueAttribute("clockAction");
327 return (HtmlPage) input.click();
328 }
329
330 public static HtmlPage clickLunchInOrOutButton(HtmlPage page, String lunchAction) throws Exception {
331 HtmlForm form = HtmlUnitUtil.getDefaultForm(page);
332
333 HtmlSubmitInput input = form.getInputByName("clockAction");
334 form.getInputByName("methodToCall").setValueAttribute("clockAction");
335 form.getInputByName("currentClockAction").setValueAttribute(lunchAction);
336 return (HtmlPage) input.click();
337 }
338
339 @SuppressWarnings("serial")
340 public static void verifyAggregateHourSumsFlatList(String msg, final Map<String,BigDecimal> ecToHoursMap, TkTimeBlockAggregate aggregate) {
341 // Initializes sum map to zeros, since we only care about the entires
342 // that were passed in.
343 Map<String,BigDecimal> ecToSumMap = new HashMap<String,BigDecimal>() {{ for (String ec : ecToHoursMap.keySet()) { put(ec, BigDecimal.ZERO); }}};
344
345 for (TimeBlock bl : aggregate.getFlattenedTimeBlockList()) {
346 for (TimeHourDetail thd : bl.getTimeHourDetails()) {
347 if (ecToSumMap.containsKey(thd.getEarnCode())) {
348 ecToSumMap.put(thd.getEarnCode(), ecToSumMap.get(thd.getEarnCode()).add(thd.getHours()));
349 }
350 }
351 }
352
353 // Assert that our values are correct.
354 for (String key : ecToHoursMap.keySet()) {
355 Assert.assertEquals(
356 msg + " >> ("+key+") Wrong number of hours expected: " + ecToHoursMap.get(key) + " found: " + ecToSumMap.get(key) + " :: ",
357 0,
358 ecToHoursMap.get(key).compareTo(ecToSumMap.get(key)));
359 }
360 }
361
362 /**
363 * Helper method to verify that the aggregate contains the correct sums as
364 * indicated in the ecToHoursMapping, on a SINGLE given flsaWeek.
365 *
366 * Warning! Contains Assertions, use only with Test Cases.
367 *
368 * @param ecToHoursMap ex: { 'REG' => 40, 'OVT' => 10 }
369 * @param aggregate An aggregate object containing the time blocks
370 * @param flsaWeek 0 indexed start week (pulling from aggregate)
371 */
372 @SuppressWarnings("serial")
373 public static void verifyAggregateHourSums(String msg, final Map<String,BigDecimal> ecToHoursMap, TkTimeBlockAggregate aggregate, int flsaWeek) {
374 // Initializes sum map to zeros, since we only care about the entires
375 // that were passed in.
376 Map<String,BigDecimal> ecToSumMap = new HashMap<String,BigDecimal>() {{ for (String ec : ecToHoursMap.keySet()) { put(ec, BigDecimal.ZERO); }}};
377
378 List<FlsaWeek> flsaWeeks = aggregate.getFlsaWeeks(DateTimeZone.forID(TKUtils.getSystemTimeZone()));
379 Assert.assertTrue(msg + " >> Not enough FLSA weeks to verify aggregate hours, max: " + (flsaWeeks.size() - 1), flsaWeeks.size() > flsaWeek);
380
381 // Build our Sum Map.
382 FlsaWeek week = flsaWeeks.get(flsaWeek);
383 List<FlsaDay> flsaDays = week.getFlsaDays();
384 for (FlsaDay day : flsaDays) {
385 for (TimeBlock bl : day.getAppliedTimeBlocks()) {
386 for (TimeHourDetail thd : bl.getTimeHourDetails()) {
387 if (ecToSumMap.containsKey(thd.getEarnCode())) {
388 ecToSumMap.put(thd.getEarnCode(), ecToSumMap.get(thd.getEarnCode()).add(thd.getHours()));
389 }
390 }
391 }
392 }
393
394
395 // Assert that our values are correct.
396 for (String key : ecToHoursMap.keySet()) {
397 Assert.assertEquals(
398 msg + " >> ("+key+") Wrong number of hours expected: " + ecToHoursMap.get(key) + " found: " + ecToSumMap.get(key) + " :: ",
399 0,
400 ecToHoursMap.get(key).compareTo(ecToSumMap.get(key)));
401 }
402 }
403
404
405 public static void verifyAggregateHourSums(final Map<String,BigDecimal> ecToHoursMap, TkTimeBlockAggregate aggregate, int flsaWeek) {
406 TkTestUtils.verifyAggregateHourSums("", ecToHoursMap, aggregate, flsaWeek);
407 }
408
409
410 /**
411 * Helper method to generate time blocks suitable for db persistence in
412 * unit tests.
413 */
414 public static List<TimeBlock> createUniformActualTimeBlocks(TimesheetDocument timesheetDocument, Assignment assignment, String earnCode, DateTime start, int days, BigDecimal hours, BigDecimal amount) {
415 TimeBlockService service = TkServiceLocator.getTimeBlockService();
416 List<TimeBlock> blocks = new ArrayList<TimeBlock>();
417
418 for (int i=0; i<days; i++) {
419 DateTime ci = start.plusDays(i);
420 DateTime co = ci.plusHours(hours.intValue());
421 Timestamp tsin = new Timestamp(ci.getMillis());
422 Timestamp tsout = new Timestamp(co.getMillis());
423
424 blocks.addAll(service.buildTimeBlocks(assignment, earnCode, timesheetDocument, tsin, tsout, hours, amount, false, false, TKContext.getPrincipalId()));
425 }
426
427 return blocks;
428 }
429
430 public static Map<Timestamp, BigDecimal> getDateToHoursMap(TimeBlock timeBlock, TimeHourDetail timeHourDetail) {
431 Map<Timestamp, BigDecimal> dateToHoursMap = new HashMap<Timestamp, BigDecimal>();
432 DateTime beginTime = new DateTime(timeBlock.getBeginTimestamp());
433 DateTime endTime = new DateTime(timeBlock.getEndTimestamp());
434
435 Days d = Days.daysBetween(beginTime, endTime);
436 int numberOfDays = d.getDays();
437 if (numberOfDays < 1) {
438 dateToHoursMap.put(timeBlock.getBeginTimestamp(), timeHourDetail.getHours());
439 return dateToHoursMap;
440 }
441 DateTime currentTime = beginTime;
442 for (int i = 0; i < numberOfDays; i++) {
443 DateTime nextDayAtMidnight = new DateTime(currentTime.plusDays(1).getMillis());
444 nextDayAtMidnight = nextDayAtMidnight.hourOfDay().setCopy(12);
445 nextDayAtMidnight = nextDayAtMidnight.minuteOfDay().setCopy(0);
446 nextDayAtMidnight = nextDayAtMidnight.secondOfDay().setCopy(0);
447 nextDayAtMidnight = nextDayAtMidnight.millisOfSecond().setCopy(0);
448 Duration dur = new Duration(currentTime, nextDayAtMidnight);
449 long duration = dur.getStandardSeconds();
450 BigDecimal hrs = new BigDecimal(duration / 3600, TkConstants.MATH_CONTEXT);
451 dateToHoursMap.put(new Timestamp(currentTime.getMillis()), hrs);
452 currentTime = nextDayAtMidnight;
453 }
454 Duration dur = new Duration(currentTime, endTime);
455 long duration = dur.getStandardSeconds();
456 BigDecimal hrs = new BigDecimal(duration / 3600, TkConstants.MATH_CONTEXT);
457 dateToHoursMap.put(new Timestamp(currentTime.getMillis()), hrs);
458
459 return dateToHoursMap;
460 }
461
462 public static Date createDate(int month, int day, int year, int hours, int minutes, int seconds){
463 DateTime dt = new DateTime(year, month, day, hours, minutes, seconds, 0);
464 return new Date(dt.getMillis());
465 }
466
467
468 /**
469 * Method to obtain the HREF onclick='' value from the button when
470 * the client side typically processes the request.
471 * @param button
472 */
473 public static String getOnClickHref(HtmlElement button) {
474 NamedNodeMap attributes = button.getAttributes();
475 Node node = attributes.getNamedItem("onclick");
476
477 //location.href='TimesheetSubmit.do?action=R&documentId=2000&methodToCall=approveTimesheet'
478 String hrefRaw = node.getNodeValue();
479 int sidx = hrefRaw.indexOf("='");
480
481 return hrefRaw.substring(sidx+2, hrefRaw.length() - 1);
482 }
483
484 }