• +43 660 1453541
  • contact@germaniumhq.com

Automating Browser Timesheet Filling With Germanium




Automating Browser Timesheet Filling With Germanium

We all hate to fill in those meaningless timesheet reports. What if there would be a way to automate this? Since Germanium is a web automation API, it becomes quite simple.

I personally hate all the things I need to manually do over, and over, and over again. For example at the end of the day, I need to log whatever is that I done. In order to do that, I need to:

  1. Login into the Outlook 365 account,
  2. create the first part of the day (in Austria where I live is forbidden to work more than 6 hours without a 30 minute break in between),
  3. pick the starting and end times,
  4. pick the project from a combo box with 100 entries, even if I work on the same project all the time,
  5. write the description, on what is that I did that day,
  6. create the entry,
    and again:
  7. select the second part of the day,
  8. pick the starting and end times,
  9. pick the project from the endless combo box,
  10. write the description.

What I actually want to do:

  1. Write the description.

So without further due, I wrote a small script that allows me to literally do all the steps automatically:

1
timesheet "IE9 Fixes, integration testing; Create docker images."

Then:

Animation showing Germanium automating the timesheet fill.

Script Overview

The core of the script looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
open_browser_and_login()

double_click('tr.ms-acal-hour00 td')
wait_dialog_to_load()
fill_dialog_with_data(current_date.day, current_date.month, current_date.year, #current date
"%02d" % start_time.hour, "%02d" % start_time.minute, # start time
"14", "00", # end time
message_morning) # message

double_click('tr.ms-acal-hour00 td')
wait_dialog_to_load()
fill_dialog_with_data(current_date.day, current_date.month, current_date.year, #current date
"14", "30", # start time
"%02d" % end_time.hour, "%02d" % end_time.minute, # end time
message_evening) # message

Obviously there is some parsing first going on of the input data. This is straightforward. First I go and open_browser_and_login(), then I create the first timesheet entry, then the second.

Open Browser and Login

1
2
3
4
5
6
7
8
9
10
11
12
def open_browser_and_login():
open_browser("chrome", iframe_selector=iframe_selector)
get_germanium().maximize_window()
go_to("https://company.sharepoint.com/sites/timesheet/_layouts/15/start.aspx#/Lists/user/calendar.aspx")

type_keys('user@company.com<tab>')
wait(Css('div.progress').not_exists) # wait for the domain check

type_keys('MYUSERPASSWORD')
type_keys('<cr>')

wait(Text('Calendars in View'))

Since we do need to open a browser, I decided to open Chrome, and just maximize the window. Since maximize_window() is not a Germanium function, it will be transparently sent to the underlying WebDriver object.

Then we just load the page.

After the page is loaded, the focus is already set in the user section, so I just start typing my user.

Outlook then starts doing some AJAX magic validating the login server and if they know about the server, so the more esoteric line:

1
wait(Css('div.progress').not_exists) # wait for the domain check

is just doing that, waiting for the div with the progress class to not exist or be visible. Of course then I type the password, and a big <cr>. Germanium has amazing typing support, so we can just do things like <tab> and <cr> with ease.

Finally we wait for the 'Calendars in View' text to appear, that signals the completion of the loading of the calendars. Now we can start adding entries.

Creating Entries

In order to create the entry we just double click the first editable cell from the calendar to fire up the adding of the dialog.

But here is where things start getting a bit interesting. The dialog itself is actually just a container for an <iframe> where the Dialog will reside. So in order to do things in the Dialog, we need to wait not only for the action (double click) to complete, or for the dialog to open, but also for the content of the dialog to load:

1
2
3
4
5
6
7
def wait_dialog_to_load():
@iframe("dialog")
def wait_iframe_dialog_to_load():
wait(Text('Project name'))

wait(Text("user.name - New Item"))
wait_iframe_dialog_to_load()

All the code does is waiting for the text "user.name - New Item" to appear, that signifies the dialog is how shown, then switch to the iframe of the dialog, using the iframe decorator, and wait in that context for the text 'Project name' to appear, that tells us the content of the iframe is there.

When all this is done, we can fill in the form inside the iframe of the dialog:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@iframe("dialog")
def fill_dialog_with_data(day, month, year, start_hour, start_minute, end_hour, end_minute, comment):
click(Input().right_of(Text('Comment')))

type_keys(comment) # Comment
type_keys('<c-a><del>%s/%s/%s' % (month, day, year), Input().right_of(Text('Start Time')))
select(Element("select").right_of(Text('Start Time')).element_list(0), "%s:" % start_hour)
select(Element("select").right_of(Text('Start Time')).element_list(1), round_minute(start_minute))

type_keys('<c-a><del>%s/%s/%s' % (month, day, year), Input().right_of(Text('End Time')))
select(Element("select").right_of(Text('End Time')).element_list(0), "%s:" % end_hour)
select(Element("select").right_of(Text('End Time')).element_list(1), round_minute(end_minute))

select(Element("select").right_of(Text('Project name')), "PROJECT_NAME")

click(Button("Save"))

This is in a sense a lot like what we’ve seen previously, except that now we also use the positional filtering capabilities of Germanium.

IFrame Selector

The IFrame Selector that was used is really straightforward:

1
2
3
4
5
6
def iframe_selector(germanium, iframe_name):
if iframe_name == 'dialog':
dialog = Element('iframe', css_classes='ms-dlgFrame').element()
germanium.switch_to_frame(dialog)
else:
germanium.switch_to_default_content()

and so is the

Launcher Shell Script

The launcher shell script just reads the current date, and starting and the current time, in order to get the time entries.

1
2
3
4
5
6
7
8
9
10
#!/usr/bin/env bash

start_time=$(last | grep reboot | head -n 1 | tr -s " " | cut -f8 -d\ )
end_time=$(last | grep reboot | head -n 1 | tr -s " " | cut -f10 -d\ )

current_date=$(date +%Y-%m-%d)

echo "Times: $current_date $start_time $end_time $@"

$(dirname $(readlink -f $0))/timesheet.py $current_date $start_time $end_time $@

It’s also possible to call the timesheet.py manually for custom dates an times:

1
python timesheet.py 2016-05-11 09:02 18:14 "IE9 Fixes, integration testing; Create docker images."