diff --git a/django/website/chn_spreadsheet/test_files/sample_excel.xlsx b/django/website/chn_spreadsheet/test_files/sample_excel.xlsx
new file mode 100644
index 0000000000000000000000000000000000000000..c1d7b5faee1fe3e5f8d44e4c3fad192fbb8a4689
Binary files /dev/null and b/django/website/chn_spreadsheet/test_files/sample_excel.xlsx differ
diff --git a/django/website/chn_spreadsheet/tests.py b/django/website/chn_spreadsheet/tests.py
index 7ce503c2dd97ba78597f6ff6e4393132753573f6..9da30add0754f7a5ef704c0c6bec6548a682fe03 100644
--- a/django/website/chn_spreadsheet/tests.py
+++ b/django/website/chn_spreadsheet/tests.py
@@ -1,3 +1,215 @@
-from django.test import TestCase
+import datetime
+import decimal
+from os import path
+import pytest
 
-# Create your tests here.
+from django.utils.translation import ugettext as _
+
+from .utils import (
+    get_profile, get_columns_map, order_columns, get_fields_and_types,
+    parse_date, normalize_row, get_rows_iterator, convert_row, process_rows,
+    SheetProfile, SheetImportException
+)
+
+
+TEST_BASE_DIR = path.abspath(path.dirname(__file__))
+TEST_DIR = path.join(TEST_BASE_DIR, 'test_files')
+
+
+COLUMN_LIST = [
+    {
+        'name': 'Province',
+        'type': 'location',
+        'field': 'message.location',
+    },
+    {
+        'name': 'Message',
+        'type': 'text',
+        'field': 'message.content',
+    },
+]
+
+
+@pytest.mark.django_db
+def test_get_profile_returns_profile():
+    label = "unknownpoll"
+    profile = {'name': 'Empty profile'}
+
+    SheetProfile.objects.create(label=label, profile=profile)
+
+    sprofile = get_profile(label)
+    assert sprofile == profile
+
+
+@pytest.mark.django_db
+def test_get_profile_raises_on_unknown_label():
+    with pytest.raises(SheetImportException) as excinfo:
+        get_profile('unknownlabel')
+    assert excinfo.value.message == _('Misconfigured service. Source "unknownlabel" does not exist')
+
+
+def test_get_columns_map():
+    expected_result = {
+        'Province': {
+            'type': 'location',
+            'field': 'message.location'
+        },
+        'Message': {
+            'type': 'text',
+            'field': 'message.content'
+        },
+    }
+    result = get_columns_map(COLUMN_LIST)
+    assert result == expected_result
+
+
+def test_get_rows_iterator_raises_on_non_excel_files():
+    with pytest.raises(SheetImportException) as excinfo:
+        get_rows_iterator('not_a_file', 'excel')
+    assert excinfo.value.message == _('Expected excel file. Received file in an unrecognized format.')
+
+    with pytest.raises(SheetImportException) as excinfo:
+        get_rows_iterator(None, 'pdf')
+    assert excinfo.value.message == _('Unsupported file format: pdf')
+
+
+def test_get_rows_iterator_works_on_excel_files():
+    file_path = path.join(TEST_DIR, 'sample_excel.xlsx')
+    f = open(file_path, 'rb')
+    rows = list(get_rows_iterator(f, 'excel'))
+
+    # 2x2 spreadsheet
+    assert len(rows) == 2
+    assert len(rows[0]) == 2
+    assert len(rows[1]) == 2
+
+
+def _make_columns_row(column_list):
+    row = [d.copy() for d in column_list]
+    for col in row:
+        del col['name']  # Unify with first row version
+    return row
+
+
+def test_order_columns_with_no_first_row_returns_original_order():
+    expected = _make_columns_row(COLUMN_LIST)
+    ordered = order_columns(COLUMN_LIST)
+    assert ordered == expected
+
+
+def test_order_columns_with_first_row_return_first_row_order():
+    cleaned = _make_columns_row(COLUMN_LIST)
+
+    first_row = ['Message', 'Province']
+    ordered = order_columns(COLUMN_LIST, first_row)
+    assert ordered == [cleaned[1], cleaned[0]]
+
+
+def test_get_fields_and_types():
+    fields, types = get_fields_and_types(COLUMN_LIST)
+    expected_types = ['location', 'text']
+    expected_fields = ['message.location', 'message.content']
+
+    assert fields == expected_fields
+    assert types == expected_types
+
+
+def test_successful_runs_of_parse_date():
+    dates = (
+        '05/01/2015',
+        '5.1.2015',
+        '5/1/15',
+        '05-01-2015',
+        datetime.datetime(2015, 1, 5, 0, 0)
+    )
+    expected = datetime.date(2015, 1, 5)
+    for date in dates:
+        assert parse_date(date) == expected
+
+
+def test_exception_raised_on_faulty_dates():
+    bad_date = '05x01-2015'
+    with pytest.raises(ValueError):
+        parse_date(bad_date)
+
+
+def test_convert_row():
+    row = ['Short message', '5', '10.4', '1.5.2015', 'Something else']
+    types = ('text', 'integer', 'number', 'date', 'ignore')
+
+    number = decimal.Decimal('10.4')
+    date = datetime.date(2015, 5, 1)
+
+    converted = convert_row(row, types, 4)
+    assert converted == ['Short message', 5, number, date]
+
+
+def test_convert_row_raises_on_unknown_type():
+    row = ['Short message']
+    types = ['location']
+
+    with pytest.raises(SheetImportException) as excinfo:
+        convert_row(row, types, 5)
+    assert excinfo.value.message == _(u"Unknown data type 'location' on row 5 ")
+
+
+def test_convert_row_raises_on_malformed_value():
+    row = ['not_integer']
+    types = ['integer']
+
+    with pytest.raises(SheetImportException) as excinfo:
+        convert_row(row, types, 3)
+    assert excinfo.value.message == _(u"Can not process value 'not_integer' of type 'integer' on row 3 ")
+
+
+def test_normalize_row_differences():
+    class Cell(object):
+        def __init__(self, value):
+            self.value = value
+
+    row = [5, 'London', Cell('1.1.2015')]
+    result = normalize_row(row)
+    assert result == [5, 'London', '1.1.2015']
+
+
+def test_normalize_row_works_with_none():
+    assert normalize_row(None) is None
+
+
+def __test_process_rows_without_or_with_header(with_header):
+    def _rows_generator():
+        rows = [
+            ('Province', 'Message'),
+            ('London', 'Short message'),
+            ('Cambridge', 'What?'),
+        ]
+        if not with_header:
+            rows = rows[1:]
+        for row in rows:
+            yield row
+
+    columns = [d.copy() for d in COLUMN_LIST]
+    columns[0]['type'] = 'text'
+    rows = _rows_generator()
+
+    objects = process_rows(rows, columns, with_header)
+    expected_objects = [
+        {
+            'message.location': 'London',
+            'message.content': 'Short message'
+        },
+        {
+            'message.location': 'Cambridge',
+            'message.content': 'What?'
+        },
+    ]
+
+    assert objects == expected_objects
+
+
+def test_process_rows_without_header():
+    __test_process_rows_without_or_with_header(False)
+
+
+def test_process_rows_with_header():
+    __test_process_rows_without_or_with_header(True)
diff --git a/django/website/chn_spreadsheet/utils.py b/django/website/chn_spreadsheet/utils.py
index 1e137406f3c94d79ec9c67c05dc8caff82419c81..0b716679f6423e4abcabf09106baba35584afea1 100644
--- a/django/website/chn_spreadsheet/utils.py
+++ b/django/website/chn_spreadsheet/utils.py
@@ -60,7 +60,9 @@ def order_columns(profile_columns, first_row=None):
                 error_msg = _('Unknown column: %s') % label
                 raise SheetImportException(error_msg)
     else:
-        columns = profile_columns[:]
+        columns = [d.copy() for d in profile_columns]
+        for col in columns:
+            del col['name']  # Unify with first row version
 
     return columns
 
@@ -72,7 +74,12 @@ def get_fields_and_types(columns):
 
 
 def parse_date(value):
-    return dateutil.parser.parse(value, dayfirst=True).date()
+    if isinstance(value, basestring):
+        date_time = dateutil.parser.parse(value, dayfirst=True)
+    else:
+        date_time = value
+
+    return date_time.date()
 
 
 def convert_row(orig_values, types, row_number):
@@ -102,21 +109,20 @@ def convert_row(orig_values, types, row_number):
 
 def normalize_row(raw_row):
     # Unify difference between CSV and openpyxl cells
-    row = []
-    for val in raw_row:
-        value = val.value if hasattr(val, "value") else val
-        row.append(value)
-    return row
+    if raw_row:
+        row = []
+        for val in raw_row:
+            value = val.value if hasattr(val, "value") else val
+            row.append(value)
+        return row
+    return None
 
 
-def process_rows(spreadsheet, profile):
-    file_format = profile.get('format')
-    rows = get_rows_iterator(spreadsheet, file_format)
-
-    # If skip_header, then use profile's order of columns, otherwise
-    # use header line to check mapping and define order
-    first_row = rows.next() if profile['skip_header'] else None
-    columns = order_columns(profile['columns'], normalize_row(first_row))
+def process_rows(rows, profile_columns, skip_header=False):
+    # If there is no header (skip_header=False), then use profile's order of
+    # columns, otherwise use header line to check mapping and define order
+    first_row = rows.next() if skip_header else None
+    columns = order_columns(profile_columns, normalize_row(first_row))
 
     fields, types = get_fields_and_types(columns)
 
@@ -138,5 +144,10 @@ def save_rows(objects, data_type):
 
 def store_spreadsheet(label, fobject):
     profile = get_profile(label)
-    items = process_rows(fobject, profile)
+
+    file_format = profile.get('format')
+    skip_header = profile.get('skip_header', False)
+
+    rows = get_rows_iterator(fobject, file_format)
+    items = process_rows(rows, profile['columns'], skip_header)
     return save_rows(items, 'message')
diff --git a/django/website/hid/templates/hid/sources.html b/django/website/hid/templates/hid/sources.html
index 3ed1ea58916558d70efc2b6d8572350ff63deb18..6944649b901aa6447f1c929d14cd2428c1e899bd 100644
--- a/django/website/hid/templates/hid/sources.html
+++ b/django/website/hid/templates/hid/sources.html
@@ -13,7 +13,7 @@
 
             <form action="{% url "sources-upload" %}" method="post" enctype="multipart/form-data" class="item-source-actions pull-right">
                 {% csrf_token %}
-                <a class="btn btn-primary btn-block" value="View/Edit data" type="button" href="{% url "sources-edit" source.src %}">View/Edit data</a>
+                <a class="btn btn-primary btn-block" value="View/Edit data" type="button" href="{% url "data-view" %}">View/Edit data</a>
                 {% bootstrap_form source.form show_label=False %}
                 {% bootstrap_button "Upload" button_type="submit" value="Upload" button_class="item-source-upload btn-block btn-primary" %}
             </form>
diff --git a/django/website/hid/templates/hid/view.html b/django/website/hid/templates/hid/view.html
new file mode 100644
index 0000000000000000000000000000000000000000..ac20d756409996bd6d59500b619d59ac8591bc4b
--- /dev/null
+++ b/django/website/hid/templates/hid/view.html
@@ -0,0 +1,8 @@
+{% extends "base.html" %}
+{% load i18n %}
+
+{% block maincontent %}
+<h2>View data</h2>
+<p>{% trans "This is the view data page." %}</p>
+
+{% endblock maincontent %}
diff --git a/django/website/hid/urls.py b/django/website/hid/urls.py
index eb4ab3515ec3714e48c17ef3db837b525c5f4573..34b0e36002c8521b45de022bffcdf120a8e5b5a4 100644
--- a/django/website/hid/urls.py
+++ b/django/website/hid/urls.py
@@ -10,5 +10,6 @@ urlpatterns = patterns('',
     url(r'^sources/upload/$', login_required(UploadSpreadsheetView.as_view()), name='sources-upload'),
     url(r'^sources/(?P<label>\w+)/$', login_required(ListSources.as_view()), name='sources-edit'),
     url(r'^sources/$', login_required(ListSources.as_view()), name='sources'),
+    url(r'^view/$', login_required(TemplateView.as_view(template_name='hid/view.html')), name="data-view"),
     url(r'^$', login_required(TemplateView.as_view(template_name='hid/dashboard.html')), name="dashboard"),
 )
diff --git a/django/website/hid/views.py b/django/website/hid/views.py
index 83aef1720ef9582cb0b74df40ae20b41b476987e..275f5e6bf5c811be350d5debf2e974aa5ab44cb3 100644
--- a/django/website/hid/views.py
+++ b/django/website/hid/views.py
@@ -33,7 +33,7 @@ class UploadSpreadsheetView(FormView):
     template_name = 'hid/upload.html'
 
     def get_success_url(self):
-        return reverse("sources")
+        return reverse("data-view")
 
     def form_valid(self, form):
         data = form.cleaned_data
diff --git a/django/website/media/images/favicon.ico b/django/website/media/images/favicon.ico
index 61b6eb304e5f45d11289fe4fbc5c78daaac886a9..6945e4856b67dc0f20cf114df510c7cca6d1dafa 100644
Binary files a/django/website/media/images/favicon.ico and b/django/website/media/images/favicon.ico differ
diff --git a/django/website/templates/base.html b/django/website/templates/base.html
index ac298b9a4ee9f70411ad57b76ce6127ed2c34894..08661ec7a1ef4a195019c3f5b0866eb852136728 100644
--- a/django/website/templates/base.html
+++ b/django/website/templates/base.html
@@ -17,10 +17,11 @@
   <![endif]-->
   {% block bootstrap3_extra_head %}{% endblock %}
   <link href="{{ STATIC_URL }}css/styles.css" media="all" rel="stylesheet" />
+  <link rel="shortcut icon" href="{{STATIC_URL}}images/favicon.ico?v=1"/>
 </head>
 
 <body>
-    
+
     {% block content %}
     <meta name="viewport" content="width=device-width,initial-scale=1">
     <div class="container-fluid">
@@ -51,7 +52,7 @@
         </div>
     </div>
     {% endblock content %}
-    
+
     {% block bootstrap3_extra_js %}
 {% bootstrap_javascript jquery=True %}
     {% endblock %}