diff --git a/smash/web/api_views/appointment.py b/smash/web/api_views/appointment.py
index 227d8353e60b80d3c2bb9a51f115889c86fd78ba..a51ddfc058c410f8f65bef590804cf9e793ef35e 100644
--- a/smash/web/api_views/appointment.py
+++ b/smash/web/api_views/appointment.py
@@ -1,5 +1,4 @@
 import logging
-import traceback
 from datetime import datetime
 
 from django.contrib.auth.decorators import login_required
@@ -87,7 +86,7 @@ def serialize_appointment(appointment):
     subject_string = ""
     nd_number = screening_number = phone_numbers = appointment_types = None
     if appointment.visit is not None:
-        title = appointment.visit.follow_up_title()
+        title = "Visit " + str(appointment.visit.visit_number)
         subject = appointment.visit.subject
         subject_string = subject.last_name + " " + subject.first_name
         nd_number = subject.nd_number
diff --git a/smash/web/api_views/subject.py b/smash/web/api_views/subject.py
index c1394a0dc93124e12d5f168e905989c574167baa..6d1bb1f598bf7371d94cb3e5a4e12a085f125306 100644
--- a/smash/web/api_views/subject.py
+++ b/smash/web/api_views/subject.py
@@ -1,12 +1,14 @@
 import logging
 
 from django.contrib.auth.decorators import login_required
+from django.db.models import Count, Case, When, Min
+from django.db.models import Q
 from django.http import JsonResponse
 
-from web.models import Subject
+from web.models import Subject, Visit, Appointment
 from web.models.constants import SUBJECT_TYPE_CHOICES
 from web.views import e500_error
-from web.views.notifications import get_subjects_with_no_visit, get_subjects_with_reminder
+from web.views.notifications import get_subjects_with_no_visit, get_subjects_with_reminder, get_today_midnight_date
 from web.views.subject import SUBJECT_LIST_GENERIC, SUBJECT_LIST_NO_VISIT, SUBJECT_LIST_REQUIRE_CONTACT
 
 logger = logging.getLogger(__name__)
@@ -48,37 +50,126 @@ def get_subjects(request, type):
         raise TypeError("Unknown query type: " + type)
 
 
-def get_subjects_order(subjects, order_column, order_direction):
-    result = subjects
+def order_by_visit(subjects_to_be_ordered, order_direction, visit_number):
+    return subjects_to_be_ordered.annotate(
+        sort_visit_date=Min(Case(When(visit__visit_number=visit_number, then='visit__datetime_begin')))).order_by(
+        order_direction + 'sort_visit_date')
+
+
+def get_subjects_order(subjects_to_be_ordered, order_column, order_direction):
+    result = subjects_to_be_ordered
     if order_direction == "asc":
         order_direction = ""
     else:
         order_direction = "-"
     if order_column == "first_name":
-        result = subjects.order_by(order_direction + 'first_name')
+        result = subjects_to_be_ordered.order_by(order_direction + 'first_name')
     elif order_column == "last_name":
-        result = subjects.order_by(order_direction + 'last_name')
+        result = subjects_to_be_ordered.order_by(order_direction + 'last_name')
     elif order_column == "nd_number":
-        result = subjects.order_by(order_direction + 'nd_number')
+        result = subjects_to_be_ordered.order_by(order_direction + 'nd_number')
     elif order_column == "screening_number":
-        result = subjects.order_by(order_direction + 'screening_number')
+        result = subjects_to_be_ordered.order_by(order_direction + 'screening_number')
     elif order_column == "default_location":
-        result = subjects.order_by(order_direction + 'default_location')
+        result = subjects_to_be_ordered.order_by(order_direction + 'default_location')
     elif order_column == "dead":
-        result = subjects.order_by(order_direction + 'dead')
+        result = subjects_to_be_ordered.order_by(order_direction + 'dead')
     elif order_column == "resigned":
-        result = subjects.order_by(order_direction + 'resigned')
+        result = subjects_to_be_ordered.order_by(order_direction + 'resigned')
     elif order_column == "information_sent":
-        result = subjects.order_by(order_direction + 'information_sent')
+        result = subjects_to_be_ordered.order_by(order_direction + 'information_sent')
     elif order_column == "postponed":
-        result = subjects.order_by(order_direction + 'postponed')
+        result = subjects_to_be_ordered.order_by(order_direction + 'postponed')
     elif order_column == "type":
-        result = subjects.order_by(order_direction + 'type')
+        result = subjects_to_be_ordered.order_by(order_direction + 'type')
+    elif order_column == "id":
+        result = subjects_to_be_ordered.order_by(order_direction + 'id')
+    elif order_column == "date_born":
+        result = subjects_to_be_ordered.order_by(order_direction + 'date_born')
+    elif order_column == "visit_1":
+        result = order_by_visit(subjects_to_be_ordered, order_direction, 1)
+    elif order_column == "visit_2":
+        result = order_by_visit(subjects_to_be_ordered, order_direction, 2)
+    elif order_column == "visit_3":
+        result = order_by_visit(subjects_to_be_ordered, order_direction, 3)
+    elif order_column == "visit_4":
+        result = order_by_visit(subjects_to_be_ordered, order_direction, 4)
+    elif order_column == "visit_5":
+        result = order_by_visit(subjects_to_be_ordered, order_direction, 5)
+    elif order_column == "visit_6":
+        result = order_by_visit(subjects_to_be_ordered, order_direction, 6)
+    elif order_column == "visit_7":
+        result = order_by_visit(subjects_to_be_ordered, order_direction, 7)
+    elif order_column == "visit_8":
+        result = order_by_visit(subjects_to_be_ordered, order_direction, 8)
+    else:
+        logger.warn("Unknown sort column: "+order_column)
+    return result
+
+
+def filter_by_visit(result, visit_number, visit_type):
+    # we need to give custom names for filtering params that contain visit_number in it
+    # because we might want to filter by few visits  and they shouldn't collide
+    datetime_begin_filter = 'visit_' + str(visit_number) + '_datetime_begin'
+    datetime_end_filter = 'visit_' + str(visit_number) + '_datetime_end'
+    is_finished_filter = 'visit_' + str(visit_number) + '_is_finished'
+    finished_appointments_filter = 'visit_' + str(visit_number) + '_finished_appointments'
+    scheduled_appointments_filter = 'visit_' + str(visit_number) + '_scheduled_appointments'
+
+    # this is hack... instead of providing True/False value this field contain 1/0 value, the problem is that we need
+    # to provide aggregate function for the interacting parameter
+    # If we try to assign it with pure Case(When...) (without Count/Min/Max/Avg) we obtain duplicates
+    # of the results which are later on messing up with the subject list
+    result = result.annotate(**{
+        is_finished_filter: Count(Case(When(Q(visit__is_finished=True) & Q(visit__visit_number=visit_number), then=1)))
+    })
+
+    # number of finished appointments
+    result = result.annotate(**{finished_appointments_filter: Count(Case(When(
+        Q(visit__appointment__status=Appointment.APPOINTMENT_STATUS_FINISHED) & Q(visit__visit_number=visit_number),
+        then=1)))})
+    # number of scheduled appointments
+    result = result.annotate(**{scheduled_appointments_filter: Count(Case(When(
+        Q(visit__appointment__status=Appointment.APPOINTMENT_STATUS_SCHEDULED) & Q(visit__visit_number=visit_number),
+        then=1)))})
+
+    # when visit starts
+    result = result.annotate(
+        **{datetime_begin_filter: Min(Case(When(visit__visit_number=visit_number, then='visit__datetime_begin')))})
+    # when visit finish
+    result = result.annotate(
+        **{datetime_end_filter: Min(Case(When(visit__visit_number=visit_number, then='visit__datetime_end')))})
+
+    if visit_type == "DONE":
+        result = result.filter(**{datetime_begin_filter + "__lt": get_today_midnight_date()}). \
+            filter(**{is_finished_filter + "__gt": 0}). \
+            filter(**{finished_appointments_filter + "__gt": 0})
+    elif visit_type == "MISSED":
+        result = result.filter(**{datetime_begin_filter + "__lt": get_today_midnight_date()}). \
+            filter(**{is_finished_filter + "__gt": 0}). \
+            filter(**{finished_appointments_filter: 0})
+    elif visit_type == "EXCEED":
+        result = result.filter(**{datetime_begin_filter + "__lt": get_today_midnight_date()}). \
+            filter(**{is_finished_filter: 0}). \
+            filter(**{datetime_end_filter + "__lt": get_today_midnight_date()})
+    elif visit_type == "IN_PROGRESS":
+        result = result.filter(**{datetime_begin_filter + "__lt": get_today_midnight_date()}). \
+            filter(**{is_finished_filter: 0}). \
+            filter(**{datetime_end_filter + "__gt": get_today_midnight_date()}). \
+            filter(**{scheduled_appointments_filter + "__gt": 0})
+    elif visit_type == "SHOULD_BE_IN_PROGRESS":
+        result = result.filter(**{datetime_begin_filter + "__lt": get_today_midnight_date()}). \
+            filter(**{is_finished_filter: 0}). \
+            filter(**{datetime_end_filter + "__gt": get_today_midnight_date()}). \
+            filter(**{scheduled_appointments_filter: 0})
+    elif visit_type == "UPCOMING":
+        result = result.filter(**{datetime_begin_filter + "__gt": get_today_midnight_date()})
+
     return result
 
 
-def get_subjects_filtered(subjects, filters):
-    result = subjects
+def get_subjects_filtered(subjects_to_be_filtered, filters):
+    result = subjects_to_be_filtered
     for row in filters:
         column = row[0]
         value = row[1]
@@ -102,6 +193,22 @@ def get_subjects_filtered(subjects, filters):
             result = result.filter(default_location=value)
         elif column == "type":
             result = result.filter(type=value)
+        elif column == "visit_1":
+            result = filter_by_visit(result, 1, value)
+        elif column == "visit_2":
+            result = filter_by_visit(result, 2, value)
+        elif column == "visit_3":
+            result = filter_by_visit(result, 3, value)
+        elif column == "visit_4":
+            result = filter_by_visit(result, 4, value)
+        elif column == "visit_5":
+            result = filter_by_visit(result, 5, value)
+        elif column == "visit_6":
+            result = filter_by_visit(result, 6, value)
+        elif column == "visit_7":
+            result = filter_by_visit(result, 7, value)
+        elif column == "visit_8":
+            result = filter_by_visit(result, 8, value)
         elif column == "":
             pass
         else:
@@ -177,10 +284,43 @@ def get_yes_no(val):
         return "NO"
 
 
+def serialize_subject_visit(visit):
+    status = "---"
+    appointments = visit.appointment_set.filter()
+    pass
+
+
 def serialize_subject(subject):
     location = ""
     if subject.default_location is not None:
         location = subject.default_location.name
+    visits = Visit.objects.filter(subject=subject).order_by('visit_number')
+    serialized_visits = []
+    for visit in visits:
+        if visit.datetime_begin < get_today_midnight_date():
+            if visit.is_finished:
+                finished_appointments_count = visit.appointment_set.filter(
+                    status=Appointment.APPOINTMENT_STATUS_FINISHED).count()
+                if finished_appointments_count > 0:
+                    status = "DONE"
+                else:
+                    status = "MISSED"
+            elif visit.datetime_end < get_today_midnight_date():
+                status = "EXCEEDED"
+            else:
+                scheduled_appointments_count = visit.appointment_set.filter(
+                    status=Appointment.APPOINTMENT_STATUS_SCHEDULED).count()
+                if scheduled_appointments_count > 0:
+                    status = "IN_PROGRESS"
+                else:
+                    status = "SHOULD_BE_IN_PROGRESS"
+        else:
+            status = "UPCOMING"
+        serialized_visits.append({
+            "status": status,
+            "datetime_start": visit.datetime_begin.strftime('%Y-%m-%d'),
+            "datetime_end": visit.datetime_end.strftime('%Y-%m-%d'),
+        })
 
     result = {
         "first_name": subject.first_name,
@@ -195,5 +335,6 @@ def serialize_subject(subject):
         "information_sent": get_yes_no(subject.information_sent),
         "type": subject.get_type_display(),
         "id": subject.id,
+        "visits": serialized_visits,
     }
     return result
diff --git a/smash/web/api_views/worker.py b/smash/web/api_views/worker.py
index ece5fc05954e7a2423e2c71164b237143efc1f5e..5183e3cd3fa5ae515102ed8e8d860f27f52c0c7f 100644
--- a/smash/web/api_views/worker.py
+++ b/smash/web/api_views/worker.py
@@ -24,7 +24,6 @@ def units(request):
 @login_required
 def workers_for_daily_planning(request):
     workers = get_workers_for_daily_planning(request)
-    print workers
     workers_list_for_json = []
     for worker in workers:
         worker_dict_for_json = {
diff --git a/smash/web/forms.py b/smash/web/forms.py
index 10b925e99020bc6fcb3443316e8df7f4c3b9c00a..e4e4c2bb8b0594b62ea16fba51132eb68282f19b 100644
--- a/smash/web/forms.py
+++ b/smash/web/forms.py
@@ -320,7 +320,7 @@ class VisitDetailForm(ModelForm):
 
     class Meta:
         model = Visit
-        exclude = ['is_finished']
+        exclude = ['is_finished', 'visit_number']
 
 
 class VisitAddForm(ModelForm):
@@ -336,7 +336,7 @@ class VisitAddForm(ModelForm):
 
     class Meta:
         model = Visit
-        exclude = ['is_finished']
+        exclude = ['is_finished', 'visit_number']
 
     def clean(self):
         super(VisitAddForm, self).clean()
diff --git a/smash/web/migrations/0056_visit_visit_number.py b/smash/web/migrations/0056_visit_visit_number.py
new file mode 100644
index 0000000000000000000000000000000000000000..3be1cb193f27909dfbb70c8cddbd9156540ba146
--- /dev/null
+++ b/smash/web/migrations/0056_visit_visit_number.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.7 on 2017-10-27 10:22
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('web', '0055_auto_20170925_0905'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='visit',
+            name='visit_number',
+            field=models.IntegerField(default=1, verbose_name=b'Visit number'),
+        ),
+    ]
diff --git a/smash/web/migrations/0057_update_visit_number_for_existing_visits.py b/smash/web/migrations/0057_update_visit_number_for_existing_visits.py
new file mode 100644
index 0000000000000000000000000000000000000000..6697af45d131464e03ef50b75f4a4820dff48143
--- /dev/null
+++ b/smash/web/migrations/0057_update_visit_number_for_existing_visits.py
@@ -0,0 +1,17 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.7 on 2017-10-27 10:22
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ('web', '0056_visit_visit_number'),
+    ]
+
+    operations = [
+        migrations.RunSQL(
+            "update web_visit s set visit_number= (select count(*) from web_visit t " +
+            "where t.subject_id=s.subject_id and t.datetime_begin<= s.datetime_begin);")
+    ]
diff --git a/smash/web/models/subject.py b/smash/web/models/subject.py
index c0a0b4b86d55c2054a3e2ddd6d6839ffddd2cb04..756cb960ea2bbf9ec58382fefe724bce9d4c8c7e 100644
--- a/smash/web/models/subject.py
+++ b/smash/web/models/subject.py
@@ -168,16 +168,6 @@ class Subject(models.Model):
         editable=True
     )
 
-    def latest_visit(self):
-        visits = self.visit_set.all()
-        if len(visits) == 0:
-            return None
-        result = visits[0]
-        for visit in visits:
-            if visit.datetime_begin > result.datetime_begin:
-                result = visit
-        return result
-
     def __str__(self):
         return "%s %s" % (self.first_name, self.last_name)
 
diff --git a/smash/web/models/visit.py b/smash/web/models/visit.py
index 7554e3984307d09fd55ccaae81c12e8547fc8aef..e11b52d74a7b54d2e837369b839e68a87276be31 100644
--- a/smash/web/models/visit.py
+++ b/smash/web/models/visit.py
@@ -2,6 +2,8 @@
 import datetime
 
 from django.db import models
+from django.db.models.signals import post_save
+from django.dispatch import receiver
 
 from constants import BOOL_CHOICES, SUBJECT_TYPE_CHOICES_CONTROL
 
@@ -33,16 +35,18 @@ class Visit(models.Model):
                                                blank=True,
                                                )
 
+    # this value is automatically computed by signal handled by update_visit_number method
+    visit_number = models.IntegerField(
+        verbose_name='Visit number',
+        default=1
+    )
+
     def __unicode__(self):
         return "%s %s" % (self.subject.first_name, self.subject.last_name)
 
     def __str__(self):
         return "%s %s" % (self.subject.first_name, self.subject.last_name)
 
-    def follow_up_title(self):
-        count = Visit.objects.filter(subject=self.subject, datetime_begin__lt=self.datetime_begin).count()
-        return "Visit " + str(count + 1)
-
     def mark_as_finished(self):
         self.is_finished = True
         self.save()
@@ -59,3 +63,16 @@ class Visit(models.Model):
                 datetime_begin=visit_started + time_to_next_visit,
                 datetime_end=visit_started + time_to_next_visit + datetime.timedelta(days=93)
             )
+
+
+@receiver(post_save, sender=Visit)
+def update_visit_number(sender, instance, **kwargs):
+    visit = instance
+    if visit.subject is not None:
+        count = Visit.objects.filter(subject=visit.subject).filter(datetime_begin__lte=visit.datetime_begin).count()
+        if count != visit.visit_number:
+            visit.visit_number = count
+            visit.save()
+            subject_visits = Visit.objects.filter(subject=visit.subject).all()
+            for subject_visit in subject_visits:
+                update_visit_number(sender, subject_visit)
diff --git a/smash/web/static/js/redcap.js b/smash/web/static/js/redcap.js
index 4cdb49ae41ebb824ec9af9b79a494c61e05e5869..75efd59ee27e17650932c357a9b5ccbb94a9ce6c 100644
--- a/smash/web/static/js/redcap.js
+++ b/smash/web/static/js/redcap.js
@@ -2,8 +2,8 @@ function ignore_missing_subject(id, url) {
     $.ajax({
         url: url,
         success: function () {
-            $("#ignore_" + id).hide()
-            $("#unignore_" + id).show()
+            $("#ignore_" + id).hide();
+            $("#unignore_" + id).show();
             $("#row_" + id).find('td').addClass("ignore-row");
         }
     });
@@ -14,8 +14,8 @@ function unignore_missing_subject(id, url) {
     $.ajax({
         url: url,
         success: function () {
-            $("#unignore_" + id).hide()
-            $("#ignore_" + id).show()
+            $("#unignore_" + id).hide();
+            $("#ignore_" + id).show();
             $("#row_" + id).find('td').removeClass("ignore-row");
         }
     });
diff --git a/smash/web/static/js/subject.js b/smash/web/static/js/subject.js
new file mode 100644
index 0000000000000000000000000000000000000000..bfee2f14996e3bbf51dce37beb3796e0304d7993
--- /dev/null
+++ b/smash/web/static/js/subject.js
@@ -0,0 +1,241 @@
+function createColumn(dataType, name, filter, visible, renderFunction) {
+    if (renderFunction === undefined) {
+        renderFunction = function (data, type, row, meta) {
+            return row[dataType];
+        }
+    }
+    return {
+        "type": dataType,
+        "name": name,
+        "filter": filter,
+        "visible": visible,
+        "render": renderFunction
+    };
+}
+
+function getColumns(type) {
+    var result = [];
+    // don't confuse end user
+    // result.push(createColumn("id", "Id", null, false));
+    result.push(createColumn("nd_number", "ND", "string_filter", true));
+    result.push(createColumn("screening_number", "Screening", "string_filter", true));
+    result.push(createColumn("first_name", "First name", "string_filter", true));
+    result.push(createColumn("last_name", "Last name", "string_filter", true));
+    result.push(createColumn("date_born", "Date of birth", null, false));
+    result.push(createColumn("default_location", "Location", "location_filter", true));
+    result.push(createColumn("dead", "Deceased", "yes_no_filter", true));
+    result.push(createColumn("resigned", "Resigned", "yes_no_filter", true));
+    result.push(createColumn("postponed", "Postponed", "yes_no_filter", true));
+    result.push(createColumn("information_sent", "Info sent", "yes_no_filter", true));
+    result.push(createColumn("type", "Type", "type_filter", true));
+    result.push(createColumn("id", "Edit", null, true, function (data, type, row, meta) {
+        return '<a href="#" type="button" class="btn btn-block btn-default">Edit</a>';
+    }));
+    for (var i = 1; i <= 8; i++) {
+        var renderFunction = (function () {
+            var x = i;
+            return function (data, type, row, meta) {
+                return create_visit_row(row.visits[x - 1]);
+            };
+        })();
+
+        result.push(createColumn("visit_" + i, "Visit " + i, "visit_filter", true, renderFunction));
+
+    }
+    return result;
+
+}
+
+function createHeader(columnsDefinition) {
+    var header = document.createElement("thead");
+    var headerRow = document.createElement("tr");
+    header.appendChild(headerRow);
+    for (var i = 0; i < columnsDefinition.length; i++) {
+        var column = columnsDefinition[i];
+        var element = document.createElement("th");
+        element.innerHTML = column.name;
+        headerRow.appendChild(element);
+    }
+    return header;
+}
+
+function createFilter(columnsDefinition) {
+    var footer = document.createElement("tfoot");
+    footer.style.display = "table-header-group";
+    var footerRow = document.createElement("tr");
+    footer.appendChild(footerRow);
+    for (var i = 0; i < columnsDefinition.length; i++) {
+        var column = columnsDefinition[i];
+        var element = document.createElement("th");
+        if (column.filter !== null) {
+            element.innerHTML = "<div name='" + column.filter + "'>" + column.name + "</div>";
+        }
+        footerRow.appendChild(element);
+    }
+    return footer;
+}
+
+function create_visit_row(visit) {
+    var color = "white";
+    var text = "---";
+    if (visit !== undefined && visit !== null) {
+        if (visit.status === "DONE") {
+            color = "green";
+            text = "OK";
+        } else if (visit.status === "MISSED") {
+            color = "pink";
+            text = "MISSED";
+        } else if (visit.status === "UPCOMING") {
+            color = "#00ffff";
+            text = "UPCOMING";
+        } else if (visit.status === "EXCEEDED") {
+            color = "orange";
+            text = "EXCEEDED";
+        } else if (visit.status === "SHOULD_BE_IN_PROGRESS") {
+            color = "orange";
+            text = "IN PROGRESS (NO APPOINTMENTS)";
+        } else if (visit.status === "IN_PROGRESS") {
+            color = "lightgreen";
+            text = "IN PROGRESS";
+        }
+        text += "<br/>" + visit.datetime_start + " - " + visit.datetime_end;
+    }
+    return "<div style='background-color:" + color + "';width:100%;height:100%>" + text + "</div>";
+}
+
+function createVisibilityCheckboxes(checkboxesElement, columns) {
+    var row = null;
+    for (var i = 0; i < columns.length; i++) {
+        if (i % 10 === 0) {
+            row = document.createElement("div");
+            row.style.display = "table-row";
+            checkboxesElement.appendChild(row);
+        }
+        var column = columns[i];
+        var element = document.createElement("div");
+        element.style.display = "table-cell";
+        var checked = "";
+        if (column.visible) {
+            checked = "checked";
+        }
+        element.innerHTML = "<input type='checkbox' " + checked + " data-column='" + i + "' name='" + column.type + "'/>" + column.name;
+        row.appendChild(element);
+    }
+
+}
+
+function createSubjectsTable(params) {
+    var tableElement = params.tableElement;
+    var worker_locations = params.worker_locations;
+    var getSubjectEditUrl = params.getSubjectEditUrl;
+    var subject_types_url = params.subject_types_url;
+    var locations_url = params.locations_url;
+    var subjects_url = params.subjects_url;
+    var columnsDefinition = params.columns;
+
+    tableElement.appendChild(createHeader(columnsDefinition));
+    tableElement.appendChild(createFilter(columnsDefinition));
+    tableElement.appendChild(document.createElement("tbody"));
+    createVisibilityCheckboxes(params.checkboxesElement, columnsDefinition);
+
+    var table;
+    $(tableElement).find('tfoot div[name="string_filter"]').each(function () {
+        var title = $(this).text();
+        $(this).html('<input type="text" style="width:80px" placeholder="' + title + '" />');
+    });
+
+    $(tableElement).find('tfoot div[name="yes_no_filter"]').each(function () {
+        $(this).html('<select style="width:60px" ><option value selected="selected">---</option><option value="true">YES</option><option value="false">NO</option></select>');
+    });
+
+    $(tableElement).find('tfoot div[name="visit_filter"]').each(function () {
+        $(this).html('<select style="width:60px" >' +
+            '<option value selected="selected">---</option>' +
+            '<option value="MISSED">MISSED</option>' +
+            '<option value="DONE">DONE</option>' +
+            '<option value="EXCEED">EXCEED</option>' +
+            '<option value="IN_PROGRESS">IN PROGRESS</option>' +
+            '<option value="SHOULD_BE_IN_PROGRESS">SHOULD BE IN PROGRESS</option>' +
+            '<option value="UPCOMING">UPCOMING</option>' +
+            '</select>');
+    });
+
+    $(tableElement).find('tfoot div[name="location_filter"]').each(function () {
+        var obj = $(this);
+        obj.html('<select style="width:80px"><option value selected="selected">---</option></select>');
+        var select = $('select', obj);
+        $.get(locations_url, function (data) {
+            $.each(data.locations, function (index, location) {
+                select.append('<option value="' + location.id + '">' + location.name + '</option>');
+            });
+            if (worker_locations.length === 1) {
+                select.val(worker_locations[0].id);
+            }
+        });
+
+    });
+    $(tableElement).find('tfoot div[name="type_filter"]').each(function () {
+        var obj = $(this);
+        obj.html('<select style="width:80px"><option value selected="selected">---</option></select>');
+        var select = $('select', obj);
+        $.get(subject_types_url, function (data) {
+            $.each(data.types, function (index, type) {
+                select.append('<option value="' + type.id + '">' + type.name + '</option>');
+            });
+        });
+    });
+
+
+    $(function () {
+        var columns = [];
+        var columnDefs = [];
+        for (var i = 0; i < columnsDefinition.length; i++) {
+            var column = columnsDefinition[i];
+            columns.push({"data": column.type});
+            columnDefs.push({"targets": i, "render": column.render, visible: column.visible});
+        }
+
+
+        table = $('#table').DataTable({
+            pageLength: 25,
+            serverSide: true,
+            processing: true,
+            responsive: true,
+            ajax: subjects_url,
+            columns: columns,
+            columnDefs: columnDefs,
+            order: [[0, 'desc']]
+        });
+
+        $('#table tbody').on('click', 'a', function () {
+            var data = table.row($(this).parents('tr')).data();
+            window.location.href = getSubjectEditUrl(data.id.toString());
+        });
+
+        // Apply the search
+        table.columns().every(function () {
+            var that = this;
+
+            $('input', this.footer()).on('keyup change', function () {
+                if (that.search() !== this.value) {
+                    that.search(this.value).draw();
+                }
+            });
+            $('select', this.footer()).on('keyup change', function () {
+                if (that.search() !== this.value) {
+                    that.search(this.value).draw();
+                }
+            });
+        });
+        $('#table_filter').css("display", "null");
+    });
+    $('#visible-column-checkboxes input').on('click', function (e) {
+        var visible = $(this).is(":checked");
+
+        // Get the column API object
+        var column = table.column($(this).attr('data-column'));
+        console.log($(this).attr('data-column'));
+        // Toggle the visibility
+        column.visible(visible);
+    });
+}
\ No newline at end of file
diff --git a/smash/web/statistics.py b/smash/web/statistics.py
index f52f54307ee4c65dea5ee6b2869eb888500c976e..63a710dca60acf2a2269e7a27d41ff9318ca2e92 100644
--- a/smash/web/statistics.py
+++ b/smash/web/statistics.py
@@ -74,7 +74,7 @@ class StatisticsManager(object):
          - number of appointments,
          - number of visits ended,
          - number of visits started
-         - number of appointements per type and per status
+         - number of appointments per type and per status
         :param month: the month number [1;12]
         :type month: int
         :param year: the year (4 digits)
@@ -200,8 +200,8 @@ class StatisticsManager(object):
             subject_type_filter = Q(subject__type=subject_type)
             filters_month_year_visits_started.add(subject_type_filter, Q.AND)
             filters_month_year_visits_ended.add(subject_type_filter, Q.AND)
-            subject_type_filter_appointements = Q(visit__subject__type=subject_type)
-            filters_month_year_appointments.add(subject_type_filter_appointements, Q.AND)
+            subject_type_filter_appointments = Q(visit__subject__type=subject_type)
+            filters_month_year_appointments.add(subject_type_filter_appointments, Q.AND)
         return filters_month_year_appointments, filters_month_year_visits_ended, filters_month_year_visits_started
 
     @staticmethod
diff --git a/smash/web/templates/subjects/add.html b/smash/web/templates/subjects/add.html
index 0d22f5b169dc21f552d35d91a80075be3d141187..fc6931871cd604134379b9e519268ee3dc2a8472 100644
--- a/smash/web/templates/subjects/add.html
+++ b/smash/web/templates/subjects/add.html
@@ -94,7 +94,7 @@
                 var first_name = $("input[name='first_name']").val();
                 var last_name = $("input[name='last_name']").val();
                 if (last_name !== "") {
-                    var url = "{% url 'web.api.subjects' 'GENERIC' %}"
+                    var url = "{% url 'web.api.subjects' 'GENERIC' %}";
                     url += "?columns[0][data]=first_name&columns[0][search][value]=" + first_name;
                     url += "&columns[1][data]=last_name&columns[1][search][value]=" + last_name;
                     $.get(url, function (data) {
@@ -106,7 +106,7 @@
                         });
                         if (subjects.length > 0) {
                             $("#duplicate_warning").css("display", "block");
-                            var content = "There are possible duplicate(s) with the same name:<br/>"
+                            var content = "There are possible duplicate(s) with the same name:<br/>";
                             $.each(subjects, function (index, subject) {
                                 content += subject.first_name + " " + subject.last_name + ", born: " + subjects.date_born + ", screening number: " + subject.screening_number + "<br/>"
                             });
diff --git a/smash/web/templates/subjects/index.html b/smash/web/templates/subjects/index.html
index 99bc0553185ef5d02cbac7af0239a4eb60c5b80e..5d1b71485ed61161555b8cf9ce0b087ae1cf0865 100644
--- a/smash/web/templates/subjects/index.html
+++ b/smash/web/templates/subjects/index.html
@@ -28,61 +28,11 @@
 
     <div class="box-body">
         <table id="table" class="table table-bordered table-striped table-responsive">
-            <thead>
-            <tr>
-                <th>ND</th>
-                <th>Screening</th>
-                <th>First name
-                </th>
-                <th>Last name</th>
-                <th>Default location</th>
-                <th>Deceased</th>
-                <th>Resigned</th>
-                <th>Postponed</th>
-                <th>Info sent</th>
-                <th>Type</th>
-                <th>Edit</th>
-            </tr>
-            </thead>
-            <tfoot style="display: table-header-group;">
-            <tr>
-                <th>
-                    <div name="string_filter">ND</div>
-                </th>
-                <th>
-                    <div name="string_filter">Screening number</div>
-                </th>
-                <th>
-                    <div name="string_filter">Name</div>
-                </th>
-                <th>
-                    <div name="string_filter">Surname</div>
-                </th>
-                <th>
-                    <div name="location_filter">---</div>
-                </th>
-                <th>
-                    <div name="yes_no_filter">---</div>
-                </th>
-                <th>
-                    <div name="yes_no_filter">---</div>
-                </th>
-                <th>
-                    <div name="yes_no_filter">---</div>
-                </th>
-                <th>
-                    <div name="yes_no_filter">---</div>
-                </th>
-                <th>
-                    <div name="type_filter">---</div>
-                </th>
-            </tr>
-            </tfoot>
-
-            <tbody>
-            </tbody>
         </table>
     </div>
+    <h3>Visible columns</h3>
+    <div id="visible-column-checkboxes" style="display:table; width:100%">
+    </div>
 {% endblock maincontent %}
 
 {% block scripts %}
@@ -90,102 +40,29 @@
 
     <script src="{% static 'AdminLTE/plugins/datatables/jquery.dataTables.min.js' %}"></script>
     <script src="{% static 'AdminLTE/plugins/datatables/dataTables.bootstrap.min.js' %}"></script>
+    <script src="{% static 'js/subject.js' %}"></script>
 
     <script>
-        var table;
-        $('#table').find('tfoot div[name="string_filter"]').each(function () {
-            var title = $(this).text();
-            $(this).html('<input type="text" style="width:80px" placeholder="' + title + '" />');
-        });
-
-        $('#table').find('tfoot div[name="yes_no_filter"]').each(function () {
-            $(this).html('<select style="width:60px" ><option value selected="selected">---</option><option value="true">YES</option><option value="false">NO</option></select>');
-        });
-
-        $('#table').find('tfoot div[name="location_filter"]').each(function () {
-            var obj = $(this);
-            obj.html('<select style="width:80px"><option value selected="selected">---</option></select>');
-            var select = $('select', obj);
-            $.get("{% url 'web.api.locations' %}", function (data) {
-                $.each(data.locations, function (index, location) {
-                    select.append('<option value="' + location.id + '">' + location.name + '</option>');
-                });
-                {% if worker.locations.all.count == 1 %}
-                    select.val({{  worker.locations.first.id}});
-                {% endif %}
-            });
-
-        });
-        $('#table').find('tfoot div[name="type_filter"]').each(function () {
-            var obj = $(this);
-            obj.html('<select style="width:80px"><option value selected="selected">---</option></select>');
-            var select = $('select', obj);
-            $.get("{% url 'web.api.subject_types' %}", function (data) {
-                $.each(data.types, function (index, type) {
-                    select.append('<option value="' + type.id + '">' + type.name + '</option>');
-                });
-            });
-
-        });
-
-        $(function () {
-            table = $('#table').DataTable({
-                pageLength: 25,
-                serverSide: true,
-                processing: true,
-                responsive: true,
-                ajax: "{% url 'web.api.subjects' list_type %}",
-                columns: [
-                    {"data": "nd_number"},
-                    {"data": "screening_number"},
-                    {"data": "first_name"},
-                    {"data": "last_name"},
-                    {"data": "default_location"},
-                    {"data": "dead"},
-                    {"data": "resigned"},
-                    {"data": "postponed"},
-                    {"data": "information_sent"},
-                    {"data": "type"},
-                    {"data": null}
-                ],
-                columnDefs: [{
-                    "targets": 10,
-                    "data": "id",
-                    "defaultContent": '<a href="#" type="button" class="btn btn-block btn-default">Edit</a>'
-                }],
-                order: [[0, 'desc']]
-            });
-            {% if worker.locations.all.count == 1 %}
-                table.columns(4).search({{  worker.locations.first.id}}).draw();
-            {% endif %}
-
-            $('#table tbody').on('click', 'a', function () {
-                var data = table.row($(this).parents('tr')).data();
-                var url = "{% url 'web.views.subject_edit' 12345 %}".replace(/12345/, data.id.toString());
-                window.location.href = url;
-            });
-
-            // Apply the search
-            table.columns().every(function () {
-                var that = this;
-
-                $('input', this.footer()).on('keyup change', function () {
-                    if (that.search() !== this.value) {
-                        that
-                            .search(this.value)
-                            .draw();
-                    }
-                });
-                $('select', this.footer()).on('keyup change', function () {
-                    if (that.search() !== this.value) {
-                        that
-                            .search(this.value)
-                            .draw();
-                    }
-                });
-            });
-            $('#table_filter').css("display", "none");
-        });
+        function getSubjectEditUrl(id) {
+            return "{% url 'web.views.subject_edit' 12345 %}".replace(/12345/, id);
+        }
+
+        var worker_locations = [];
+        {% for location in worker.locations.all %}
+            worker_locations.push({id: location.id, name: location.name});
+        {% endfor %}
+
+        createSubjectsTable({
+            getSubjectEditUrl: getSubjectEditUrl,
+            worker_locations: worker_locations,
+            subject_types_url: "{% url 'web.api.subject_types' %}",
+            locations_url: "{% url 'web.api.locations' %}",
+            subjects_url: "{% url 'web.api.subjects' list_type %}",
+            tableElement: document.getElementById("table"),
+            columns: getColumns("{{ list_type }}"),
+            checkboxesElement: document.getElementById("visible-column-checkboxes")
+        })
+        ;
     </script>
 
 {% endblock scripts %}
diff --git a/smash/web/templates/subjects/visitdetails.html b/smash/web/templates/subjects/visit_details.html
similarity index 100%
rename from smash/web/templates/subjects/visitdetails.html
rename to smash/web/templates/subjects/visit_details.html
diff --git a/smash/web/templates/visits/details.html b/smash/web/templates/visits/details.html
index 6953e3a90265956863eb65986af8e6ddac669c95..f6c632b961d953120b79982937a94a509a5cde74 100644
--- a/smash/web/templates/visits/details.html
+++ b/smash/web/templates/visits/details.html
@@ -11,10 +11,10 @@
 {% endblock styles %}
 
 {% block ui_active_tab %}'visits'{% endblock ui_active_tab %}
-{% block page_header %}Details of the visit ({{ visit.follow_up_title }}) {% endblock page_header %}
+{% block page_header %}Details of the visit ({{ visit.visit_number }}) {% endblock page_header %}
 {% block page_description %}{% endblock page_description %}
 
-{% block title %}{{ block.super }} - Details of visit ({{ visit.follow_up_title }}) {% endblock %}
+{% block title %}{{ block.super }} - Details of visit ({{ visit.visit_number }}) {% endblock %}
 
 {% block breadcrumb %}
     {% include "subjects/breadcrumb.html" %}
diff --git a/smash/web/tests/test_api_appointment.py b/smash/web/tests/api_views/test_appointment.py
similarity index 100%
rename from smash/web/tests/test_api_appointment.py
rename to smash/web/tests/api_views/test_appointment.py
diff --git a/smash/web/tests/test_api_appointment_type.py b/smash/web/tests/api_views/test_appointment_type.py
similarity index 100%
rename from smash/web/tests/test_api_appointment_type.py
rename to smash/web/tests/api_views/test_appointment_type.py
diff --git a/smash/web/tests/test_api_configuration_item.py b/smash/web/tests/api_views/test_configuration_item.py
similarity index 98%
rename from smash/web/tests/test_api_configuration_item.py
rename to smash/web/tests/api_views/test_configuration_item.py
index 102e3a5b6d2451e3d9238400affa0034171526a7..d4becf7cf6bf7b12b93de0a747e21e903c0e4eed 100644
--- a/smash/web/tests/test_api_configuration_item.py
+++ b/smash/web/tests/api_views/test_configuration_item.py
@@ -4,9 +4,9 @@
 from django.urls import reverse
 
 from web.models import ConfigurationItem
-from web.tests.functions import create_configuration_item
-from . import LoggedInTestCase
 from web.models.constants import CANCELLED_APPOINTMENT_COLOR_CONFIGURATION_TYPE
+from web.tests import LoggedInTestCase
+from web.tests.functions import create_configuration_item
 
 
 class TestConfigurationItemApi(LoggedInTestCase):
diff --git a/smash/web/tests/test_api_location.py b/smash/web/tests/api_views/test_location.py
similarity index 100%
rename from smash/web/tests/test_api_location.py
rename to smash/web/tests/api_views/test_location.py
diff --git a/smash/web/tests/test_api_subject.py b/smash/web/tests/api_views/test_subject.py
similarity index 60%
rename from smash/web/tests/test_api_subject.py
rename to smash/web/tests/api_views/test_subject.py
index 8fbce03c8fd979bb726f96d305fd8e4fd4f57f9e..b4b074f3f031cdcc26d84a5c6890ad084f0aff6f 100644
--- a/smash/web/tests/test_api_subject.py
+++ b/smash/web/tests/api_views/test_subject.py
@@ -1,15 +1,17 @@
 # coding=utf-8
 import json
+import datetime
 
 from django.contrib.auth.models import User
 from django.test import Client
 from django.test import TestCase
 from django.urls import reverse
 
+from web.views.notifications import get_today_midnight_date
 from web.api_views.subject import get_subjects_order, get_subjects_filtered, serialize_subject, SUBJECT_LIST_GENERIC, \
     SUBJECT_LIST_NO_VISIT, SUBJECT_LIST_REQUIRE_CONTACT
-from web.models import Subject
-from web.tests.functions import create_subject, create_worker, create_get_suffix
+from web.models import Subject, Appointment, Visit
+from web.tests.functions import create_subject, create_worker, create_get_suffix, create_visit, create_appointment
 
 
 class TestApi(TestCase):
@@ -142,6 +144,24 @@ class TestApi(TestCase):
 
         self.check_subject_ordered("nd_number", [subject, subject2])
 
+    def test_subjects_sort_id(self):
+        subject = self.subject
+
+        subject2 = create_subject(2)
+
+        self.check_subject_ordered("id", [subject, subject2])
+
+    def test_subjects_sort_date_born(self):
+        subject = self.subject
+        subject.date_born = get_today_midnight_date()
+        subject.save()
+
+        subject2 = create_subject(2)
+        subject2.date_born = get_today_midnight_date() + datetime.timedelta(days=1)
+        subject2.save()
+
+        self.check_subject_ordered("date_born", [subject, subject2])
+
     def test_subjects_sort_default_location(self):
         subject = self.subject
 
@@ -289,3 +309,132 @@ class TestApi(TestCase):
 
         subject_json = serialize_subject(subject)
         self.assertEqual("YES", subject_json["dead"])
+
+    def test_subjects_filter_visit_1_DONE(self):
+        subject = self.subject
+        subject.dead = True
+        subject.save()
+
+        visit = create_visit(subject)
+        appointment = create_appointment(visit)
+        appointment.status = Appointment.APPOINTMENT_STATUS_FINISHED
+        appointment.save()
+        visit.mark_as_finished()
+
+        self.check_subject_filtered([["visit_1", "DONE"]], [subject])
+        self.check_subject_filtered([["visit_2", "DONE"]], [])
+
+        self.check_subject_filtered([["visit_1", "MISSED"]], [])
+        self.check_subject_filtered([["visit_1", "EXCEED"]], [])
+        self.check_subject_filtered([["visit_1", "IN_PROGRESS"]], [])
+        self.check_subject_filtered([["visit_1", "SHOULD_BE_IN_PROGRESS"]], [])
+        self.check_subject_filtered([["visit_1", "UPCOMING"]], [])
+
+    def test_subjects_filter_visit_1_MISSED(self):
+        subject = self.subject
+        subject.dead = True
+        subject.save()
+
+        visit = create_visit(subject)
+        appointment = create_appointment(visit)
+        appointment.status = Appointment.APPOINTMENT_STATUS_CANCELLED
+        appointment.save()
+        visit.mark_as_finished()
+
+        self.check_subject_filtered([["visit_1", "MISSED"]], [subject])
+        self.check_subject_filtered([["visit_2", "MISSED"]], [])
+
+        self.check_subject_filtered([["visit_1", "DONE"]], [])
+        self.check_subject_filtered([["visit_1", "EXCEED"]], [])
+        self.check_subject_filtered([["visit_1", "IN_PROGRESS"]], [])
+        self.check_subject_filtered([["visit_1", "SHOULD_BE_IN_PROGRESS"]], [])
+        self.check_subject_filtered([["visit_1", "UPCOMING"]], [])
+
+    def test_subjects_filter_visit_1_EXCEED(self):
+        subject = self.subject
+        subject.dead = True
+        subject.save()
+
+        visit = create_visit(subject)
+        visit.datetime_begin = get_today_midnight_date() + datetime.timedelta(days=-365 * 2)
+        visit.datetime_end = get_today_midnight_date() + datetime.timedelta(days=-365 * 1)
+        visit.save()
+
+        self.check_subject_filtered([["visit_1", "EXCEED"]], [subject])
+        self.check_subject_filtered([["visit_2", "EXCEED"]], [])
+
+        self.check_subject_filtered([["visit_1", "DONE"]], [])
+        self.check_subject_filtered([["visit_1", "MISSED"]], [])
+        self.check_subject_filtered([["visit_1", "IN_PROGRESS"]], [])
+        self.check_subject_filtered([["visit_1", "SHOULD_BE_IN_PROGRESS"]], [])
+        self.check_subject_filtered([["visit_1", "UPCOMING"]], [])
+
+    def test_subjects_filter_visit_1_IN_PROGRESS(self):
+        subject = self.subject
+        subject.dead = True
+        subject.save()
+
+        visit = create_visit(subject)
+        appointment = create_appointment(visit)
+        appointment.status = Appointment.APPOINTMENT_STATUS_SCHEDULED
+        appointment.save()
+
+        self.check_subject_filtered([["visit_1", "IN_PROGRESS"]], [subject])
+        self.check_subject_filtered([["visit_2", "IN_PROGRESS"]], [])
+
+        self.check_subject_filtered([["visit_1", "DONE"]], [])
+        self.check_subject_filtered([["visit_1", "MISSED"]], [])
+        self.check_subject_filtered([["visit_1", "EXCEED"]], [])
+        self.check_subject_filtered([["visit_1", "SHOULD_BE_IN_PROGRESS"]], [])
+        self.check_subject_filtered([["visit_1", "UPCOMING"]], [])
+
+    def test_subjects_filter_visit_1_SHOULD_BE_IN_PROGRESS(self):
+        subject = self.subject
+        subject.dead = True
+        subject.save()
+
+        visit = create_visit(subject)
+
+        self.check_subject_filtered([["visit_1", "SHOULD_BE_IN_PROGRESS"]], [subject])
+        self.check_subject_filtered([["visit_2", "SHOULD_BE_IN_PROGRESS"]], [])
+
+        self.check_subject_filtered([["visit_1", "DONE"]], [])
+        self.check_subject_filtered([["visit_1", "MISSED"]], [])
+        self.check_subject_filtered([["visit_1", "EXCEED"]], [])
+        self.check_subject_filtered([["visit_1", "IN_PROGRESS"]], [])
+        self.check_subject_filtered([["visit_1", "UPCOMING"]], [])
+
+    def test_subjects_filter_visit_1_UPCOMING(self):
+        subject = self.subject
+        subject.dead = True
+        subject.save()
+
+        visit = create_visit(subject)
+        visit.datetime_begin = get_today_midnight_date() + datetime.timedelta(days=2)
+        visit.datetime_end = get_today_midnight_date() + datetime.timedelta(days=20)
+        visit.save()
+
+        self.check_subject_filtered([["visit_1", "UPCOMING"]], [subject])
+        self.check_subject_filtered([["visit_2", "UPCOMING"]], [])
+
+        self.check_subject_filtered([["visit_1", "DONE"]], [])
+        self.check_subject_filtered([["visit_1", "MISSED"]], [])
+        self.check_subject_filtered([["visit_1", "EXCEED"]], [])
+        self.check_subject_filtered([["visit_1", "IN_PROGRESS"]], [])
+        self.check_subject_filtered([["visit_1", "SHOULD_BE_IN_PROGRESS"]], [])
+
+    def test_subjects_filter_visit_1_visit_2_combined(self):
+        subject = self.subject
+        subject.save()
+
+        visit = create_visit(subject)
+        appointment = create_appointment(visit)
+        appointment.status = Appointment.APPOINTMENT_STATUS_FINISHED
+        appointment.save()
+        visit.mark_as_finished()
+
+        self.check_subject_filtered([["visit_1", "DONE"]], [subject])
+        self.check_subject_filtered([["visit_2", "UPCOMING"]], [subject])
+
+        self.check_subject_filtered([["visit_1", "DONE"], ["visit_2", "UPCOMING"]], [subject])
+        self.check_subject_filtered([["visit_1", "UPCOMING"], ["visit_2", "DONE"]], [])
diff --git a/smash/web/tests/forms/__init__.py b/smash/web/tests/forms/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/smash/web/tests/test_AppointmentAddForm.py b/smash/web/tests/forms/test_AppointmentAddForm.py
similarity index 89%
rename from smash/web/tests/test_AppointmentAddForm.py
rename to smash/web/tests/forms/test_AppointmentAddForm.py
index 7861e12f9bb24e72c016e4e3a8101e9492cfdbb6..a63675e57ba5affc5d5e7b3c70a7dfda3d5074e7 100644
--- a/smash/web/tests/test_AppointmentAddForm.py
+++ b/smash/web/tests/forms/test_AppointmentAddForm.py
@@ -1,9 +1,9 @@
 from django.test import TestCase
 
-from functions import create_user, create_visit, create_location
-from functions import get_test_location
 from web.forms import AppointmentAddForm
 from web.models import Worker
+from web.tests.functions import create_user, create_visit, create_location
+from web.tests.functions import get_test_location
 
 
 class AppointmentAddFormTests(TestCase):
diff --git a/smash/web/tests/test_AppointmentEditForm.py b/smash/web/tests/forms/test_AppointmentEditForm.py
similarity index 94%
rename from smash/web/tests/test_AppointmentEditForm.py
rename to smash/web/tests/forms/test_AppointmentEditForm.py
index eba5c6d0970e19908dd90f34a5e224d3153f34e8..0a60144efbc9916be9c6eee7f3719a2f1fa937f7 100644
--- a/smash/web/tests/test_AppointmentEditForm.py
+++ b/smash/web/tests/forms/test_AppointmentEditForm.py
@@ -1,9 +1,9 @@
 from django.test import TestCase
 
-from functions import get_test_location, create_user, create_visit, create_location
 from web.forms import AppointmentAddForm
 from web.forms import AppointmentEditForm
 from web.models import Appointment, Worker
+from web.tests.functions import get_test_location, create_user, create_visit, create_location
 
 
 class AppointmentEditFormTests(TestCase):
diff --git a/smash/web/tests/test_AvailabilityAddForm.py b/smash/web/tests/forms/test_AvailabilityAddForm.py
similarity index 93%
rename from smash/web/tests/test_AvailabilityAddForm.py
rename to smash/web/tests/forms/test_AvailabilityAddForm.py
index 85987100244f414824869bec471a1e3d4aca0143..0d8f1beec26c159dc2d6807d359222c77b4a0618 100644
--- a/smash/web/tests/test_AvailabilityAddForm.py
+++ b/smash/web/tests/forms/test_AvailabilityAddForm.py
@@ -1,10 +1,10 @@
 from django.test import TestCase
 
-from functions import create_user, create_visit
-from functions import get_test_location
-from web.models.constants import MONDAY_AS_DAY_OF_WEEK
 from web.forms import AvailabilityAddForm
 from web.models import Worker
+from web.models.constants import MONDAY_AS_DAY_OF_WEEK
+from web.tests.functions import create_user, create_visit
+from web.tests.functions import get_test_location
 
 
 class AvailabilityAddFormTests(TestCase):
diff --git a/smash/web/tests/test_AvailabilityEditForm.py b/smash/web/tests/forms/test_AvailabilityEditForm.py
similarity index 89%
rename from smash/web/tests/test_AvailabilityEditForm.py
rename to smash/web/tests/forms/test_AvailabilityEditForm.py
index 9d2d3bd1106c50f5626e3ebaeca788ba94ce6232..0779eabecdfaf3c93c4206a7da65fb0bec05dd3d 100644
--- a/smash/web/tests/test_AvailabilityEditForm.py
+++ b/smash/web/tests/forms/test_AvailabilityEditForm.py
@@ -1,10 +1,10 @@
 from django.test import TestCase
 
-from functions import create_user, create_visit
-from functions import get_test_location
-from web.models.constants import MONDAY_AS_DAY_OF_WEEK
 from web.forms import AvailabilityEditForm, AvailabilityAddForm
 from web.models import Worker
+from web.models.constants import MONDAY_AS_DAY_OF_WEEK
+from web.tests.functions import create_user, create_visit
+from web.tests.functions import get_test_location
 
 
 class AvailabilityEditFormTests(TestCase):
diff --git a/smash/web/tests/test_SubjectAddForm.py b/smash/web/tests/forms/test_SubjectAddForm.py
similarity index 98%
rename from smash/web/tests/test_SubjectAddForm.py
rename to smash/web/tests/forms/test_SubjectAddForm.py
index 77e9c6d6044ed42cc320175d7ec6e9ba98db6938..c22fee47c48f9c39f4b4a4567b5f93cf0fe73673 100644
--- a/smash/web/tests/test_SubjectAddForm.py
+++ b/smash/web/tests/forms/test_SubjectAddForm.py
@@ -2,9 +2,9 @@ from django.contrib.auth.models import User
 from django.test import Client
 from django.test import TestCase
 
-from functions import get_test_location, create_subject, create_worker
 from web.forms import SubjectAddForm, get_new_screening_number
 from web.models.constants import SEX_CHOICES_MALE, SUBJECT_TYPE_CHOICES_CONTROL
+from web.tests.functions import get_test_location, create_subject, create_worker
 
 
 class SubjectAddFormTests(TestCase):
diff --git a/smash/web/tests/test_SubjectEditForm.py b/smash/web/tests/forms/test_SubjectEditForm.py
similarity index 97%
rename from smash/web/tests/test_SubjectEditForm.py
rename to smash/web/tests/forms/test_SubjectEditForm.py
index bf9fb7314eb8a5109b03ecf8efc90d249fc40663..25f2047ac34f8da86c28b130ace30aa68b3832e9 100644
--- a/smash/web/tests/test_SubjectEditForm.py
+++ b/smash/web/tests/forms/test_SubjectEditForm.py
@@ -1,12 +1,12 @@
 from django.contrib.auth.models import User
-from django.test import TestCase
 from django.test import Client
+from django.test import TestCase
 
-from functions import get_test_location, create_worker
 from web.forms import SubjectAddForm
 from web.forms import SubjectEditForm
 from web.models import Subject
 from web.models.constants import SEX_CHOICES_MALE, SUBJECT_TYPE_CHOICES_CONTROL
+from web.tests.functions import get_test_location, create_worker
 
 
 class SubjectEditFormTests(TestCase):
diff --git a/smash/web/tests/test_VisitAddForm.py b/smash/web/tests/forms/test_VisitAddForm.py
similarity index 90%
rename from smash/web/tests/test_VisitAddForm.py
rename to smash/web/tests/forms/test_VisitAddForm.py
index 2a2dfe0c6b6d85ca86fb47cd6946d76df0d32189..b6a0d817ad2b577e83bcbf9a03fbfd71b3baf6f7 100644
--- a/smash/web/tests/test_VisitAddForm.py
+++ b/smash/web/tests/forms/test_VisitAddForm.py
@@ -2,12 +2,12 @@ from __future__ import print_function
 
 from django.test import TestCase
 
-from functions import create_subject
-from functions import get_test_location
 from web.forms import VisitAddForm
+from web.tests.functions import create_subject
+from web.tests.functions import get_test_location
 
 
-class SubjectAddFormTests(TestCase):
+class VisitAddFormTests(TestCase):
     def setUp(self):
         get_test_location()
         self.subject = create_subject()
diff --git a/smash/web/tests/test_model_appointment.py b/smash/web/tests/models/test_appointment.py
similarity index 87%
rename from smash/web/tests/test_model_appointment.py
rename to smash/web/tests/models/test_appointment.py
index 36525f581b4dba2ebd12f93c49db85e1dbf4397d..a781e69c9ec36a99bd92e3cbf5616d16ee8d2b4d 100644
--- a/smash/web/tests/test_model_appointment.py
+++ b/smash/web/tests/models/test_appointment.py
@@ -1,6 +1,6 @@
 from django.test import TestCase
 
-from functions import create_appointment
+from web.tests.functions import create_appointment
 
 
 class AppointmentModelTests(TestCase):
diff --git a/smash/web/tests/test_model_configuration_item.py b/smash/web/tests/models/test_configuration_item.py
similarity index 100%
rename from smash/web/tests/test_model_configuration_item.py
rename to smash/web/tests/models/test_configuration_item.py
diff --git a/smash/web/tests/test_model_mail_template.py b/smash/web/tests/models/test_mail_template.py
similarity index 98%
rename from smash/web/tests/test_model_mail_template.py
rename to smash/web/tests/models/test_mail_template.py
index 0460314373a98c926c4dcabf9b725cf9949efd81..df6611fa60ba7e30d85dd82241d422b2508e3b0f 100644
--- a/smash/web/tests/test_model_mail_template.py
+++ b/smash/web/tests/models/test_mail_template.py
@@ -3,12 +3,12 @@ import StringIO
 from django.test import TestCase
 from docx import Document
 
-from functions import create_language, get_resource_path, create_appointment, create_user, create_subject, \
-    create_visit
 from web.models import MailTemplate
 from web.models.constants import MAIL_TEMPLATE_CONTEXT_APPOINTMENT, MAIL_TEMPLATE_CONTEXT_VISIT, \
     MAIL_TEMPLATE_CONTEXT_SUBJECT
 from web.models.mail_template import DATE_FORMAT_SHORT
+from web.tests.functions import create_language, get_resource_path, create_appointment, create_user, create_subject, \
+    create_visit
 
 
 class MailTemplateModelTests(TestCase):
diff --git a/smash/web/tests/test_model_visit.py b/smash/web/tests/models/test_visit.py
similarity index 83%
rename from smash/web/tests/test_model_visit.py
rename to smash/web/tests/models/test_visit.py
index 2a54bcb98df71668457065eefac420c2f0095071..835b7a66569ddbed84abf6212822e7dcf6ae8bee 100644
--- a/smash/web/tests/test_model_visit.py
+++ b/smash/web/tests/models/test_visit.py
@@ -1,7 +1,6 @@
 from django.test import TestCase
 
-from functions import create_subject
-from functions import create_visit
+from web.tests.functions import create_subject, create_visit
 from web.models import Visit
 
 
@@ -36,3 +35,7 @@ class VisitModelTests(TestCase):
 
         visit_count = Visit.objects.filter(subject=subject).count()
         self.assertEquals(1, visit_count)
+
+    def test_visit_to_string(self):
+        visit = create_visit(create_subject())
+        self.assertIsNotNone(str(visit))
diff --git a/smash/web/tests/models/test_worker.py b/smash/web/tests/models/test_worker.py
index be91b963bf4c9136f2432abf49f52a8489c1667b..9b193326f06a1cbbe7992c61ead7f00d6e8552e5 100644
--- a/smash/web/tests/models/test_worker.py
+++ b/smash/web/tests/models/test_worker.py
@@ -2,6 +2,7 @@ from django.contrib.auth.models import User
 from django.test import Client
 from django.test import TestCase
 
+from web.models import Worker
 from web.tests.functions import create_worker
 
 
@@ -29,3 +30,19 @@ class WorkerModelTests(TestCase):
     def test_is_active_for_invalid(self):
         worker = create_worker()
         self.assertFalse(worker.is_active())
+
+    def test_get_by_user_for_None(self):
+        worker = Worker.get_by_user(None)
+        self.assertIsNone(worker)
+
+    def test_get_by_user_for_Worker(self):
+        worker = create_worker()
+        worker_2 = Worker.get_by_user(worker)
+        self.assertEqual(worker, worker_2)
+
+    def test_get_by_user_for_invalid(self):
+        try:
+            Worker.get_by_user("invalid object")
+            self.fail("Exception expected")
+        except TypeError:
+            pass
diff --git a/smash/web/tests/test_view_configuration.py b/smash/web/tests/view/test_configuration.py
similarity index 87%
rename from smash/web/tests/test_view_configuration.py
rename to smash/web/tests/view/test_configuration.py
index 4b9bbccdc61c23d13edd9b6e2906314a5c995573..23a147796de28f012dcf257e1dac12313160bcb3 100644
--- a/smash/web/tests/test_view_configuration.py
+++ b/smash/web/tests/view/test_configuration.py
@@ -2,7 +2,7 @@ import datetime
 
 from django.urls import reverse
 
-from . import LoggedInTestCase
+from web.tests import LoggedInTestCase
 
 
 class ConfigurationViewTests(LoggedInTestCase):
diff --git a/smash/web/tests/test_view_contact_attempt.py b/smash/web/tests/view/test_contact_attempt.py
similarity index 96%
rename from smash/web/tests/test_view_contact_attempt.py
rename to smash/web/tests/view/test_contact_attempt.py
index 6a36fdaaaa2310d8e929e7e0440b05bd414a4197..9bb40d7191ef127995ec1a01f2a9618c91547971 100644
--- a/smash/web/tests/test_view_contact_attempt.py
+++ b/smash/web/tests/view/test_contact_attempt.py
@@ -4,10 +4,10 @@ from django.urls import reverse
 from django.utils import timezone
 
 from web.forms import ContactAttemptEditForm
-from functions import create_subject, create_contact_attempt, format_form_field
 from web.models import ContactAttempt
 from web.models.constants import CONTACT_TYPES_EMAIL
-from . import LoggedInWithWorkerTestCase
+from web.tests import LoggedInWithWorkerTestCase
+from web.tests.functions import create_subject, create_contact_attempt, format_form_field
 
 
 class ContactAttemptViewTests(LoggedInWithWorkerTestCase):
diff --git a/smash/web/tests/test_view_export.py b/smash/web/tests/view/test_export.py
similarity index 90%
rename from smash/web/tests/test_view_export.py
rename to smash/web/tests/view/test_export.py
index f15c2cf439a9012999ff91ef88a9fc1e928a0679..f2c7850f269e95d3ddc8f051d676f2971e9d5761 100644
--- a/smash/web/tests/test_view_export.py
+++ b/smash/web/tests/view/test_export.py
@@ -1,8 +1,8 @@
 # coding=utf-8
 from django.urls import reverse
 
-from functions import create_subject, create_appointment
-from . import LoggedInTestCase
+from web.tests import LoggedInTestCase
+from web.tests.functions import create_subject, create_appointment
 
 
 class TestExportView(LoggedInTestCase):
diff --git a/smash/web/tests/test_view_functions.py b/smash/web/tests/view/test_functions.py
similarity index 88%
rename from smash/web/tests/test_view_functions.py
rename to smash/web/tests/view/test_functions.py
index 7df3e0a0a13ae7b9fcb0d1dfd87c09cac5060f89..d03ab059ae136521a764883b443018f105fdc894 100644
--- a/smash/web/tests/test_view_functions.py
+++ b/smash/web/tests/view/test_functions.py
@@ -1,6 +1,6 @@
 from django.test import TestCase
 
-from functions import create_location, create_user, create_worker, get_test_location
+from web.tests.functions import create_location, create_user, create_worker, get_test_location
 from web.views.notifications import get_filter_locations
 
 
diff --git a/smash/web/tests/test_view_kit_request.py b/smash/web/tests/view/test_kit_request.py
similarity index 97%
rename from smash/web/tests/test_view_kit_request.py
rename to smash/web/tests/view/test_kit_request.py
index cc82b543178595e8433f7f5c26ac2aadb3090f39..f0d199b6fff21bc039695c1caa1e30e53330062a 100644
--- a/smash/web/tests/test_view_kit_request.py
+++ b/smash/web/tests/view/test_kit_request.py
@@ -3,11 +3,11 @@ import datetime
 from django.core import mail
 from django.urls import reverse
 
-from functions import create_appointment_type, create_appointment, create_visit
 from web.models import Item, Appointment, AppointmentTypeLink
+from web.tests import LoggedInTestCase
+from web.tests.functions import create_appointment_type, create_appointment, create_visit
 from web.views.kit import get_kit_requests
 from web.views.notifications import get_today_midnight_date
-from . import LoggedInTestCase
 
 
 class ViewFunctionsTests(LoggedInTestCase):
diff --git a/smash/web/tests/test_view_login.py b/smash/web/tests/view/test_login.py
similarity index 97%
rename from smash/web/tests/test_view_login.py
rename to smash/web/tests/view/test_login.py
index f3fa9b05d3152c6fc7a6542e03bbaf4a8937c0d1..f66d376e1efa0d5dc7c11f6c0617f095a4b57af5 100644
--- a/smash/web/tests/test_view_login.py
+++ b/smash/web/tests/view/test_login.py
@@ -5,8 +5,8 @@ from django.test import Client
 from django.test import TestCase
 from django.urls import reverse
 
-from functions import create_user
 from web.models import Worker
+from web.tests.functions import create_user
 
 
 class TestLoginView(TestCase):
diff --git a/smash/web/tests/test_view_notifications.py b/smash/web/tests/view/test_notifications.py
similarity index 98%
rename from smash/web/tests/test_view_notifications.py
rename to smash/web/tests/view/test_notifications.py
index 9c79dd8cbdac2dbdbfbd6e8608d659e65ddbfcd8..c2586124a76f713af2746ad99f1cf5063123c773 100644
--- a/smash/web/tests/test_view_notifications.py
+++ b/smash/web/tests/view/test_notifications.py
@@ -2,10 +2,11 @@ import datetime
 
 from django.contrib.auth.models import AnonymousUser
 
-from functions import create_appointment, create_location, create_worker, create_appointment_type
-from functions import create_subject
-from functions import create_visit
 from web.models import Appointment, Location, AppointmentTypeLink
+from web.tests import LoggedInTestCase
+from web.tests.functions import create_appointment, create_location, create_worker, create_appointment_type
+from web.tests.functions import create_subject
+from web.tests.functions import create_visit
 from web.views.notifications import \
     get_approaching_visits_for_mail_contact, \
     get_approaching_visits_for_mail_contact_count, \
@@ -22,7 +23,6 @@ from web.views.notifications import \
     get_unfinished_appointments, \
     get_unfinished_appointments_count, \
     get_unfinished_visits
-from . import LoggedInTestCase
 
 
 class NotificationViewTests(LoggedInTestCase):
diff --git a/smash/web/tests/test_view_statistics.py b/smash/web/tests/view/test_statistics.py
similarity index 94%
rename from smash/web/tests/test_view_statistics.py
rename to smash/web/tests/view/test_statistics.py
index eac349b67a9893e701d39d75606647b797e60592..795a1ed830cb1d8310daca986c651ff19e8a666b 100644
--- a/smash/web/tests/test_view_statistics.py
+++ b/smash/web/tests/view/test_statistics.py
@@ -3,7 +3,7 @@ from datetime import datetime
 
 from django.urls import reverse
 
-from . import LoggedInTestCase
+from web.tests import LoggedInTestCase
 
 __author__ = 'Valentin Grouès'
 
diff --git a/smash/web/tests/test_view_subjects.py b/smash/web/tests/view/test_subjects.py
similarity index 97%
rename from smash/web/tests/test_view_subjects.py
rename to smash/web/tests/view/test_subjects.py
index ef13ec1fda116890e7c1f8af20f51c17291836d8..8c721780bb9852e2ca929198ca69515e12d90b1b 100644
--- a/smash/web/tests/test_view_subjects.py
+++ b/smash/web/tests/view/test_subjects.py
@@ -5,7 +5,7 @@ from django.test import Client
 from django.test import TestCase
 from django.urls import reverse
 
-from functions import create_subject, create_visit, create_appointment, create_worker, get_test_location
+from web.tests.functions import create_subject, create_visit, create_appointment, create_worker, get_test_location
 from web.forms import SubjectAddForm, SubjectEditForm
 from web.models import Subject
 from web.models.constants import SEX_CHOICES_MALE, SUBJECT_TYPE_CHOICES_CONTROL, SUBJECT_TYPE_CHOICES_PATIENT
@@ -44,7 +44,7 @@ class SubjectsViewTests(TestCase):
         self.assertEqual(response.status_code, 200)
         self.assertFalse("Add visit" in response.content)
 
-    def test_render_subject_visit_details(self):
+    def test_render_subject_visit_details_without_visit(self):
         subject = create_subject()
 
         response = self.client.get(reverse('web.views.subject_visit_details', kwargs={'id': subject.id}))
diff --git a/smash/web/tests/test_view_visit.py b/smash/web/tests/view/test_visit.py
similarity index 96%
rename from smash/web/tests/test_view_visit.py
rename to smash/web/tests/view/test_visit.py
index b8c0a28696966ba193af9e1bba78da8d8a5e56ba..217475e6822e106079f7fc52550b706bcddd3a55 100644
--- a/smash/web/tests/test_view_visit.py
+++ b/smash/web/tests/view/test_visit.py
@@ -2,11 +2,11 @@ import datetime
 
 from django.urls import reverse
 
-from functions import create_subject, create_visit, create_appointment, create_appointment_type
 from web.forms import VisitDetailForm, VisitAddForm
 from web.models import Visit
+from web.tests import LoggedInTestCase
+from web.tests.functions import create_subject, create_visit, create_appointment, create_appointment_type
 from web.views.notifications import get_today_midnight_date
-from . import LoggedInTestCase
 
 
 class VisitViewTests(LoggedInTestCase):
diff --git a/smash/web/urls.py b/smash/web/urls.py
index bc687f2603a8d20c5e863d6dac8c4e9584a812b2..984f6ca1e978d64565f016b2baa8e6df94d3d0ec 100644
--- a/smash/web/urls.py
+++ b/smash/web/urls.py
@@ -70,7 +70,7 @@ urlpatterns = [
 
     url(r'^subjects$', views.subject.subjects, name='web.views.subjects'),
     url(r'^subjects/no_visit$', views.subject.subject_no_visits, name='web.views.subject_no_visits'),
-    url(r'^subjects/equire_contact$', views.subject.subject_require_contact, name='web.views.subject_require_contact'),
+    url(r'^subjects/require_contact$', views.subject.subject_require_contact, name='web.views.subject_require_contact'),
     url(r'^subjects/add$', views.subject.subject_add, name='web.views.subject_add'),
     url(r'^subjects/subject_visit_details/(?P<id>\d+)$', views.subject.subject_visit_details,
         name='web.views.subject_visit_details'),
@@ -120,7 +120,7 @@ urlpatterns = [
     ####################
 
     url(r'^equipment_and_rooms$', views.equipment.equipment_and_rooms, name='web.views.equipment_and_rooms'),
-    url(r'^equipment_and_rooms/eqdef$', views.equipment.equipment_def, name='web.views.equipment_def'),
+    url(r'^equipment_and_rooms/equipment_def$', views.equipment.equipment_def, name='web.views.equipment_def'),
     url(r'^equipment_and_rooms/kit_requests$', views.kit.kit_requests, name='web.views.kit_requests'),
     url(r'^equipment_and_rooms/kit_requests/(?P<start_date>[\w-]+)/$', views.kit.kit_requests_send_mail,
         name='web.views.kit_requests_send_mail'),
diff --git a/smash/web/views/export.py b/smash/web/views/export.py
index 359386c2552b4bd0c7e8afd46bb2353beba5917e..b1962c7e66b797f89b141da41266a584e470cdd4 100644
--- a/smash/web/views/export.py
+++ b/smash/web/views/export.py
@@ -91,7 +91,7 @@ def get_appointments_as_array():
     for appointment in appointments:
         if appointment.visit is not None:
             row = [appointment.visit.subject.nd_number, appointment.visit.subject.last_name,
-                   appointment.visit.subject.first_name, appointment.visit.follow_up_title()]
+                   appointment.visit.subject.first_name, appointment.visit.visit_number]
         else:
             row = ["---", "---", "---", "---"]
         for field in appointments_fields:
diff --git a/smash/web/views/subject.py b/smash/web/views/subject.py
index da6675ea4c4a7a796d8394edf639290c3d0f93a7..9031c0d52635fad4c8c8dc027cd2883d093886e4 100644
--- a/smash/web/views/subject.py
+++ b/smash/web/views/subject.py
@@ -83,18 +83,18 @@ def subject_edit(request, id):
 
 def subject_visit_details(request, id):
     subject_to_be_viewed = get_object_or_404(Subject, id=id)
-    visits = subject_to_be_viewed.visit_set.all()
+    visits = subject_to_be_viewed.visit_set.order_by("-visit_number").all()
     visits_data = []
     allow_add_visit = True
     for visit in visits:
         appointments = visit.appointment_set.all()
         finished = visit.is_finished
         visit_id = visit.id
-        visit_title = visit.follow_up_title()
+        visit_title = "Visit " + str(visit.visit_number)
         visit_form = VisitDetailForm(instance=visit)
         visits_data.append((visit_form, appointments, finished, visit_id, visit_title))
         if not visit.is_finished:
             allow_add_visit = False
 
-    return wrap_response(request, 'subjects/visitdetails.html',
+    return wrap_response(request, 'subjects/visit_details.html',
                          {'display': visits_data, "id": id, "allow_add_visit": allow_add_visit})