diff --git a/smash/web/forms.py b/smash/web/forms.py
index 35a319b8b2e49255efa9947ccc8c4682347f8d74..5033f1a17bbe6bc25d2f637d828be90c010799c8 100644
--- a/smash/web/forms.py
+++ b/smash/web/forms.py
@@ -5,6 +5,7 @@ from django.forms import ModelForm, Form
 from django.utils.dates import MONTHS
 
 from models import Subject, Worker, Appointment, Visit, AppointmentType
+from models.constants import SUBJECT_TYPE_CHOICES
 
 """
 Possible redundancy, but if need arises, contents of forms can be easily customized
@@ -249,6 +250,6 @@ class StatisticsForm(Form):
         self.fields['month'] = forms.ChoiceField(choices=MONTHS.items(), initial=month)
         self.fields['year'] = forms.ChoiceField(choices=year_choices, initial=year)
         choices = [(-1, "all")]
-        choices.extend(Subject.SUBJECT_TYPE_CHOICES.items())
+        choices.extend(SUBJECT_TYPE_CHOICES.items())
         self.fields['subject_type'] = forms.ChoiceField(choices=choices, initial="-1")
         self.fields['visit'] = forms.ChoiceField(choices=visit_choices, initial="-1")
diff --git a/smash/web/models.py b/smash/web/models.py
deleted file mode 100644
index 4fd299bd773d79422e0873495ca99fd5cdec4d1e..0000000000000000000000000000000000000000
--- a/smash/web/models.py
+++ /dev/null
@@ -1,643 +0,0 @@
-from __future__ import unicode_literals
-
-import datetime
-
-from django.contrib.auth.models import User
-from django.db import models
-
-
-def get_current_year():
-    return datetime.datetime.now().year
-
-
-BOOL_CHOICES = ((True, 'Yes'), (False, 'No'))
-
-
-class Location(models.Model):
-    name = models.CharField(max_length=20)
-
-    def __str__(self):
-        return "%s" % self.name
-
-    def __unicode__(self):
-        return "%s" % self.name
-
-
-class Language(models.Model):
-    name = models.CharField(max_length=20)
-    image = models.ImageField()
-
-    def __str__(self):
-        return self.name
-
-    def image_img(self):
-        if self.image:
-            return u'<img class="flag-icon" src="%s" />' % (self.image.url)
-        else:
-            return 'No image'
-
-    image_img.short_description = 'Flag icon'
-    image_img.allow_tags = True
-
-
-class Subject(models.Model):
-    SEX_CHOICES_MALE = 'M'
-    SEX_CHOICES_FEMALE = 'F'
-
-    SEX_CHOICES = (
-        (SEX_CHOICES_MALE, 'Male'),
-        (SEX_CHOICES_FEMALE, 'Female'),
-    )
-
-    SUBJECT_TYPE_CHOICES_CONTROL = 'C'
-    SUBJECT_TYPE_CHOICES = {
-        SUBJECT_TYPE_CHOICES_CONTROL: 'CONTROL',
-        'P': 'PATIENT',
-    }
-
-    def finish_all_visits(self):
-        visits = Visit.objects.filter(subject=self, is_finished=False)
-        for visit in visits:
-            visit.is_finished = True
-            visit.save()
-
-    def finish_all_appointments(self):
-        appointments = Appointment.objects.filter(visit__subject=self, status=Appointment.APPOINTMENT_STATUS_SCHEDULED)
-        for appointment in appointments:
-            appointment.status = Appointment.APPOINTMENT_STATUS_CANCELLED
-            appointment.save()
-
-    def mark_as_dead(self):
-        self.dead = True
-        self.save()
-
-        self.finish_all_visits()
-        self.finish_all_appointments()
-
-    def mark_as_rejected(self):
-        self.resigned = True
-        self.save()
-
-        self.finish_all_visits()
-        self.finish_all_appointments()
-
-    sex = models.CharField(max_length=1,
-                           choices=SEX_CHOICES,
-                           verbose_name='Sex'
-                           )
-    postponed = models.BooleanField(choices=BOOL_CHOICES,
-                                    verbose_name='Postponed',
-                                    default=False
-                                    )
-    datetime_contact_reminder = models.DateField(
-        null=True,
-        blank=True,
-        verbose_name='Contact on',
-    )
-    type = models.CharField(max_length=1,
-                            choices=SUBJECT_TYPE_CHOICES.items(),
-                            verbose_name='Type'
-                            )
-
-    dead = models.BooleanField(
-        verbose_name='Dead',
-        default=False,
-        editable=False
-    )
-    resigned = models.BooleanField(
-        verbose_name='Resigned',
-        default=False,
-        editable=False
-    )
-    default_location = models.ForeignKey(Location,
-                                         verbose_name='Default appointment location',
-                                         )
-    first_name = models.CharField(max_length=50,
-                                  verbose_name='First name'
-                                  )
-    last_name = models.CharField(max_length=50,
-                                 verbose_name='Last name'
-                                 )
-    languages = models.ManyToManyField(Language,
-                                       blank=True,
-                                       verbose_name='Known languages'
-                                       )
-    default_written_communication_language = models.ForeignKey(Language,
-                                                               null=True,
-                                                               blank=True,
-                                                               related_name="%(class)s_written_comunication",
-                                                               verbose_name='Default language for document generation'
-                                                               )
-    phone_number = models.CharField(max_length=20,
-                                    null=True,
-                                    blank=True,
-                                    verbose_name='Phone number'
-                                    )
-    phone_number_2 = models.CharField(max_length=20,
-                                      null=True,
-                                      blank=True,
-                                      verbose_name='Phone number 2'
-                                      )
-    phone_number_3 = models.CharField(max_length=20,
-                                      null=True,
-                                      blank=True,
-                                      verbose_name='Phone number 3'
-                                      )
-    email = models.EmailField(
-        null=True,
-        blank=True,
-        verbose_name='E-mail'
-    )
-    date_born = models.DateField(
-        null=True,
-        blank=True,
-        verbose_name='Date of birth (YYYY-MM-DD)'
-    )
-    address = models.CharField(max_length=255,
-                               blank=True,
-                               verbose_name='Address'
-                               )
-    postal_code = models.CharField(max_length=7,
-                                   blank=True,
-                                   verbose_name='Postal code'
-                                   )
-    city = models.CharField(max_length=50,
-                            blank=True,
-                            verbose_name='City'
-                            )
-    country = models.CharField(max_length=50,
-                               verbose_name='Country'
-                               )
-    screening_number = models.CharField(max_length=50,
-                                        unique=True,
-                                        verbose_name='Screening number'
-                                        )
-    nd_number = models.CharField(max_length=6,
-                                 blank=True,
-                                 verbose_name='ND number'
-                                 )
-    mpower_id = models.CharField(max_length=20,
-                                 blank=True,
-                                 verbose_name='MPower ID'
-                                 )
-    comments = models.TextField(max_length=2000,
-                                blank=True,
-                                verbose_name='Comments'
-                                )
-    date_added = models.DateField(verbose_name='Added on',
-                                  auto_now=True
-                                  )
-    referral = models.CharField(max_length=128,
-                                null=True,
-                                blank=True,
-                                verbose_name='Referred by'
-                                )
-    diagnosis = models.CharField(max_length=128,
-                                 null=True,
-                                 blank=True,
-                                 verbose_name='Diagnosis'
-                                 )
-    year_of_diagnosis = models.IntegerField(
-        default=0,
-        null=True,
-        blank=True,
-        verbose_name='Year of diagnosis (YYYY)'
-    )
-
-    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)
-
-    def __unicode__(self):
-        return "%s %s" % (self.first_name, self.last_name)
-
-
-class Item(models.Model):
-    is_fixed = models.BooleanField(
-        default=False,
-        verbose_name='Is the item fixed?'
-    )
-
-    disposable = models.BooleanField(
-        default=False,
-        verbose_name='Disposable set'
-    )
-
-    name = models.CharField(max_length=255,
-                            verbose_name='Name'
-                            )
-
-    def __str__(self):
-        return self.name
-
-    def __unicode__(self):
-        return self.name
-
-
-class Room(models.Model):
-    equipment = models.ManyToManyField(Item,
-                                       verbose_name='On-site equipment',
-                                       blank=True
-                                       )
-    owner = models.CharField(max_length=50,
-                             verbose_name='Owner'
-                             )
-    address = models.CharField(max_length=255,
-                               verbose_name='Address'
-                               )
-    city = models.CharField(max_length=50,
-                            verbose_name='City'
-                            )
-    room_number = models.IntegerField(
-        verbose_name='Room number'
-    )
-    floor = models.IntegerField(
-        verbose_name='Floor'
-    )
-    is_vehicle = models.BooleanField(
-        verbose_name='Is a vehicle?'
-    )
-
-    def __str__(self):
-        return "%d %s %s" % (self.room_number, self.address, self.city)
-
-    def __unicode__(self):
-        return "%d %s %s" % (self.room_number, self.address, self.city)
-
-
-class AppointmentType(models.Model):
-    DEFAULT_COLOR = '#cfc600'
-    DEFAULT_FONT_COLOR = '#00000'
-
-    required_equipment = models.ManyToManyField(Item,
-                                                verbose_name='Required equipment',
-                                                blank=True
-                                                )
-    code = models.CharField(max_length=20,
-                            verbose_name='Appointment code'
-                            )
-    description = models.CharField(max_length=2000,
-                                   verbose_name='Appointment description'
-                                   )
-    default_duration = models.IntegerField(
-        verbose_name='Default duration (in minutes)'
-    )
-    calendar_color_priority = models.IntegerField(
-        verbose_name='Calendar color priority',
-        default=1
-    )
-    calendar_color = models.CharField(max_length=2000,
-                                      verbose_name='Calendar color',
-                                      default=DEFAULT_COLOR
-                                      )
-    calendar_font_color = models.CharField(max_length=2000,
-                                           verbose_name='Calendar color',
-                                           default=DEFAULT_FONT_COLOR
-                                           )
-    rest_time = models.IntegerField(
-        verbose_name='Suggested rest time',
-        default=0
-    )
-    can_be_parallelized = models.BooleanField(
-        verbose_name='Can be parallelized',
-        default=False
-    )
-    REQ_ROLE_CHOICES = (
-        ('DOCTOR', 'Doctor'),
-        ('NURSE', 'Nurse'),
-        ('PSYCHOLOGIST', 'Psychologist'),
-        ('ANY', 'Any')
-    )
-    required_worker = models.CharField(max_length=20, choices=REQ_ROLE_CHOICES,
-                                       verbose_name='Type of worker required for appointment',
-                                       default='ANY'
-                                       )
-
-    class Meta:
-        ordering = ['description']
-
-    def __str__(self):
-        return self.description
-
-    def __unicode__(self):
-        return self.description
-
-
-class Worker(models.Model):
-    languages = models.ManyToManyField(Language,
-                                       verbose_name='Known languages'
-                                       )
-    locations = models.ManyToManyField(Location,
-                                       verbose_name='Locations'
-                                       )
-    appointments = models.ManyToManyField('Appointment', blank=True,
-                                          verbose_name='Appointments'
-                                          )
-    user = models.OneToOneField(User, blank=True, null=True,
-                                verbose_name='Username'
-                                )
-    first_name = models.CharField(max_length=50,
-                                  verbose_name='First name'
-                                  )
-    last_name = models.CharField(max_length=50,
-                                 verbose_name='Last name'
-                                 )
-    phone_number = models.CharField(max_length=20,
-                                    verbose_name='Phone number'
-                                    )
-    unit = models.CharField(max_length=50,
-                            verbose_name='Unit'
-                            )
-    email = models.EmailField(
-        verbose_name='E-mail'
-    )
-    ROLE_CHOICES_SECRETARY = "SECRETARY"
-    ROLE_CHOICES = (
-        ('DOCTOR', 'Doctor'),
-        ('NURSE', 'Nurse'),
-        ('PSYCHOLOGIST', 'Psychologist'),
-        ('TECHNICIAN', 'Technician'),
-        (ROLE_CHOICES_SECRETARY, 'Secretary')
-    )
-    role = models.CharField(max_length=20, choices=ROLE_CHOICES,
-                            verbose_name='Role'
-                            )
-    specialization = models.CharField(max_length=20,
-                                      verbose_name='Specialization'
-                                      )
-
-    def is_on_leave(self):
-        if len(self.holiday_set.filter(datetime_end__gt=datetime.datetime.now(),
-                                       datetime_start__lt=datetime.datetime.now())):
-            return True
-        return False
-
-    @staticmethod
-    def get_by_user(the_user):
-        if isinstance(the_user, User):
-            workers = Worker.objects.filter(user=the_user)
-            if len(workers) > 0:
-                return workers[0]
-            else:
-                return None
-        elif isinstance(the_user, Worker):
-            return the_user
-        elif the_user is not None:
-            raise TypeError("Unknown class type: " + the_user.__class__.__name__)
-        else:
-            return None
-
-    @staticmethod
-    def get_details(the_user):
-        if not the_user.is_authenticated:
-            return 'Guest', 'Test account'
-
-        person = Worker.objects.filter(user=the_user)
-
-        if len(person) == 0:
-            return the_user.get_full_name(), '<No worker information>'
-        else:
-            # For get_*_display, see:
-            # https://docs.djangoproject.com/en/1.10/topics/db/models/#field-options
-            return unicode(person[0]), person[0].get_role_display()
-
-    def __str__(self):
-        return "%s %s" % (self.first_name, self.last_name)
-
-    def __unicode__(self):
-        return "%s %s" % (self.first_name, self.last_name)
-
-
-class FlyingTeam(models.Model):
-    # doctor = models.ForeignKey(Worker, related_name='FlyingTeamDoctor',
-    #     verbose_name='Doctor'
-    # )
-    # nurse = models.ForeignKey(Worker, related_name='FlyingTeamNurse',
-    #     verbose_name='Nurse'
-    # )
-    # psychologist = models.ForeignKey(Worker, related_name='FlyingTeamPsychologist',
-    #     verbose_name='Psychologist'
-    # )
-    # datetime_called = models.DateTimeField(
-    #     verbose_name='Created on'
-    # )
-    # datetime_until = models.DateTimeField(
-    #     verbose_name='Disbanded on'
-    # )
-    #
-    # def __str__(self):
-    #     return "%s %s %s" % (self.doctor.last_name, self.nurse.last_name, self.psychologist.last_name)
-    #
-    # def __unicode__(self):
-    #     return "%s %s %s" % (self.doctor.last_name, self.nurse.last_name, self.psychologist.last_name)
-    place = models.CharField(max_length=255, verbose_name='Place')
-
-    def __str__(self):
-        return "%s" % self.place
-
-    def __unicode__(self):
-        return "%s" % self.place
-
-
-class Avaibility(models.Model):
-    person = models.ForeignKey(Worker, on_delete=models.CASCADE,
-                               verbose_name='Worker'
-                               )
-    day_number = models.IntegerField(
-        verbose_name='Day of the week'
-    )
-    available_from = models.TimeField(
-        verbose_name='Avaible since'
-    )
-    available_till = models.TimeField(
-        verbose_name='Avaible until'
-    )
-    is_current = models.BooleanField(
-        verbose_name='Is current?',
-        default=True
-    )
-
-    def __str__(self):
-        return "%d %s %s" % (self.day_number, self.person.last_name, self.person.first_name)
-
-    def __unicode__(self):
-        return "%d %s %s" % (self.day_number, self.person.last_name, self.person.first_name)
-
-
-class Holiday(models.Model):
-    person = models.ForeignKey(Worker, on_delete=models.CASCADE,
-                               verbose_name='Worker'
-                               )
-    datetime_start = models.DateTimeField(
-        verbose_name='On leave since'
-    )
-    datetime_end = models.DateTimeField(
-        verbose_name='On leave until'
-    )
-
-    def __str__(self):
-        return "%s %s" % (self.person.first_name, self.person.last_name)
-
-    def __unicode__(self):
-        return "%s %s" % (self.person.first_name, self.person.last_name)
-
-
-class Visit(models.Model):
-    subject = models.ForeignKey(Subject, on_delete=models.CASCADE,
-                                verbose_name='Subject'
-                                )
-    datetime_begin = models.DateTimeField(
-        verbose_name='Visit starts at'
-    )
-    datetime_end = models.DateTimeField(
-        verbose_name='Visit ends at'
-    )  # Deadline before which all appointments need to be scheduled
-
-    is_finished = models.BooleanField(
-        verbose_name='Has ended',
-        default=False
-    )
-    post_mail_sent = models.BooleanField(choices=BOOL_CHOICES,
-                                         verbose_name='Post mail sent',
-                                         default=False
-                                         )
-    appointment_types = models.ManyToManyField(AppointmentType,
-                                               verbose_name='Requested appointments',
-                                               blank=True,
-                                               )
-
-    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()
-
-        if (not self.subject.dead) and (not self.subject.resigned):
-            visit_started = self.datetime_begin
-
-            time_to_next_visit = datetime.timedelta(days=365)
-            if self.subject.type == Subject.SUBJECT_TYPE_CHOICES_CONTROL:
-                time_to_next_visit = datetime.timedelta(days=365 * 3 + 366)
-
-            Visit.objects.create(
-                subject=self.subject,
-                datetime_begin=visit_started + time_to_next_visit,
-                datetime_end=visit_started + time_to_next_visit + datetime.timedelta(days=93)
-            )
-
-
-class Appointment(models.Model):
-    APPOINTMENT_STATUS_SCHEDULED = 'SCHEDULED'
-    APPOINTMENT_STATUS_FINISHED = 'FINISHED'
-    APPOINTMENT_STATUS_CANCELLED = 'CANCELLED'
-    APPOINTMENT_STATUS_NO_SHOW = 'NO_SHOW'
-    APPOINTMENT_STATUS_CHOICES = {
-        APPOINTMENT_STATUS_SCHEDULED: 'Scheduled',
-        APPOINTMENT_STATUS_FINISHED: 'Finished',
-        APPOINTMENT_STATUS_CANCELLED: 'Cancelled',
-        APPOINTMENT_STATUS_NO_SHOW: 'No Show',
-    }
-
-    flying_team = models.ForeignKey(FlyingTeam,
-                                    verbose_name='Flying team (if applicable)',
-                                    null=True, blank=True
-                                    )
-    worker_assigned = models.ForeignKey(Worker,
-                                        verbose_name='Worker conducting the assessment (if applicable)',
-                                        null=True, blank=True
-                                        )
-    appointment_types = models.ManyToManyField(AppointmentType,
-                                               verbose_name='Appointment types',
-                                               blank=True
-                                               )
-    room = models.ForeignKey(Room,
-                             verbose_name='Room ID',
-                             null=True,
-                             blank=True
-                             )
-    location = models.ForeignKey(Location,
-                                 verbose_name='Location',
-                                 )
-    visit = models.ForeignKey(Visit,
-                              verbose_name='Visit ID'
-                              )
-    comment = models.TextField(max_length=1024,
-                               verbose_name='Comment',
-                               null=True,
-                               blank=True
-                               )
-    datetime_when = models.DateTimeField(
-        verbose_name='Appointment on',
-        null=True, blank=True
-    )
-    length = models.IntegerField(
-        verbose_name='Appointment length (in minutes)'
-    )  # Potentially redundant; but can be used to manually adjust appointment's length
-
-    status = models.CharField(max_length=20, choices=APPOINTMENT_STATUS_CHOICES.items(),
-                              verbose_name='Status',
-                              editable=False,
-                              default=APPOINTMENT_STATUS_SCHEDULED
-                              )
-
-    def mark_as_finished(self):
-        self.status = Appointment.APPOINTMENT_STATUS_FINISHED
-        self.save()
-
-    def mark_as_cancelled(self):
-        self.status = Appointment.APPOINTMENT_STATUS_CANCELLED
-        self.save()
-
-    def mark_as_no_show(self):
-        self.status = Appointment.APPOINTMENT_STATUS_NO_SHOW
-        self.save()
-
-    def datetime_until(self):
-        if self.datetime_when is None:
-            return None
-        else:
-            return self.datetime_when + datetime.timedelta(minutes=max(self.length, 15))
-
-    def color(self):
-        result = AppointmentType.DEFAULT_COLOR
-        priority = 1000000
-        for type in self.appointment_types.all():
-            if type.calendar_color_priority < priority:
-                priority = type.calendar_color_priority
-                result = type.calendar_color
-        return result
-
-    def font_color(self):
-        result = AppointmentType.DEFAULT_FONT_COLOR
-        priority = 1000000
-        for type in self.appointment_types.all():
-            if type.calendar_color_priority < priority:
-                priority = type.calendar_color_priority
-                result = type.calendar_font_color
-        return result
-
-    def title(self):
-        if self.visit.subject.screening_number == "---":
-            return self.comment.replace("\n", ";").replace("\r", ";")
-        else:
-            title = self.visit.subject.first_name + " " + self.visit.subject.last_name + " type: "
-            for appointment_type in self.appointment_types.all():
-                title += appointment_type.code + ", "
-            return title
diff --git a/smash/web/models/__init__.py b/smash/web/models/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..553a544844bcfc0ad97c2c2c81218ce291093e6a
--- /dev/null
+++ b/smash/web/models/__init__.py
@@ -0,0 +1,29 @@
+# coding=utf-8
+from __future__ import unicode_literals
+
+import datetime
+
+from django.contrib.auth.models import User
+
+from flying_team import FlyingTeam
+from location import Location
+from room import Room
+from visit import Visit
+from worker import Worker
+from appointment import Appointment
+from appointment_type import AppointmentType
+from avaibility import Avaibility
+from holiday import Holiday
+from item import Item
+from language import Language
+from subject import Subject
+
+__author__ = 'Valentin Grouès'
+
+
+def get_current_year():
+    return datetime.datetime.now().year
+
+
+__all__ = [FlyingTeam, Appointment, AppointmentType, Avaibility, Holiday, Item, Language, Location, Room, Subject,
+           Visit, Worker]
diff --git a/smash/web/models/appointment.py b/smash/web/models/appointment.py
new file mode 100644
index 0000000000000000000000000000000000000000..62e42d9a1c242016e56a428c9c812a6ba69e45cd
--- /dev/null
+++ b/smash/web/models/appointment.py
@@ -0,0 +1,112 @@
+# coding=utf-8
+import datetime
+
+from django.db import models
+
+from constants import APPOINTMENT_TYPE_DEFAULT_COLOR, APPOINTMENT_TYPE_DEFAULT_FONT_COLOR
+from . import FlyingTeam, Location, Room, Visit, Worker
+
+__author__ = 'Valentin Grouès'
+
+
+class Appointment(models.Model):
+    class Meta:
+        app_label = 'web'
+
+    APPOINTMENT_STATUS_SCHEDULED = 'SCHEDULED'
+    APPOINTMENT_STATUS_FINISHED = 'FINISHED'
+    APPOINTMENT_STATUS_CANCELLED = 'CANCELLED'
+    APPOINTMENT_STATUS_NO_SHOW = 'NO_SHOW'
+    APPOINTMENT_STATUS_CHOICES = {
+        APPOINTMENT_STATUS_SCHEDULED: 'Scheduled',
+        APPOINTMENT_STATUS_FINISHED: 'Finished',
+        APPOINTMENT_STATUS_CANCELLED: 'Cancelled',
+        APPOINTMENT_STATUS_NO_SHOW: 'No Show',
+    }
+
+    flying_team = models.ForeignKey(FlyingTeam,
+                                    verbose_name='Flying team (if applicable)',
+                                    null=True, blank=True
+                                    )
+    worker_assigned = models.ForeignKey(Worker,
+                                        verbose_name='Worker conducting the assessment (if applicable)',
+                                        null=True, blank=True
+                                        )
+    appointment_types = models.ManyToManyField("web.AppointmentType",
+                                               verbose_name='Appointment types',
+                                               blank=True
+                                               )
+    room = models.ForeignKey(Room,
+                             verbose_name='Room ID',
+                             null=True,
+                             blank=True
+                             )
+    location = models.ForeignKey(Location,
+                                 verbose_name='Location',
+                                 )
+    visit = models.ForeignKey(Visit,
+                              verbose_name='Visit ID'
+                              )
+    comment = models.TextField(max_length=1024,
+                               verbose_name='Comment',
+                               null=True,
+                               blank=True
+                               )
+    datetime_when = models.DateTimeField(
+        verbose_name='Appointment on',
+        null=True, blank=True
+    )
+    length = models.IntegerField(
+        verbose_name='Appointment length (in minutes)'
+    )  # Potentially redundant; but can be used to manually adjust appointment's length
+
+    status = models.CharField(max_length=20, choices=APPOINTMENT_STATUS_CHOICES.items(),
+                              verbose_name='Status',
+                              editable=False,
+                              default=APPOINTMENT_STATUS_SCHEDULED
+                              )
+
+    def mark_as_finished(self):
+        self.status = Appointment.APPOINTMENT_STATUS_FINISHED
+        self.save()
+
+    def mark_as_cancelled(self):
+        self.status = Appointment.APPOINTMENT_STATUS_CANCELLED
+        self.save()
+
+    def mark_as_no_show(self):
+        self.status = Appointment.APPOINTMENT_STATUS_NO_SHOW
+        self.save()
+
+    def datetime_until(self):
+        if self.datetime_when is None:
+            return None
+        else:
+            return self.datetime_when + datetime.timedelta(minutes=max(self.length, 15))
+
+    def color(self):
+        result = APPOINTMENT_TYPE_DEFAULT_COLOR
+        priority = 1000000
+        for type in self.appointment_types.all():
+            if type.calendar_color_priority < priority:
+                priority = type.calendar_color_priority
+                result = type.calendar_color
+        return result
+
+    def font_color(self):
+        result = APPOINTMENT_TYPE_DEFAULT_FONT_COLOR
+        priority = 1000000
+        for type in self.appointment_types.all():
+            if type.calendar_color_priority < priority:
+                priority = type.calendar_color_priority
+                result = type.calendar_font_color
+        return result
+
+    def title(self):
+        if self.visit.subject.screening_number == "---":
+            return self.comment.replace("\n", ";").replace("\r", ";")
+        else:
+            title = self.visit.subject.first_name + " " + self.visit.subject.last_name + " type: "
+            for appointment_type in self.appointment_types.all():
+                title += appointment_type.code + ", "
+            return title
diff --git a/smash/web/models/appointment_type.py b/smash/web/models/appointment_type.py
new file mode 100644
index 0000000000000000000000000000000000000000..9b3e48dcba89575e8a7d6ad8b9f75078ca9a786a
--- /dev/null
+++ b/smash/web/models/appointment_type.py
@@ -0,0 +1,64 @@
+# coding=utf-8
+from django.db import models
+
+from constants import APPOINTMENT_TYPE_DEFAULT_COLOR, APPOINTMENT_TYPE_DEFAULT_FONT_COLOR
+
+__author__ = 'Valentin Grouès'
+
+
+class AppointmentType(models.Model):
+    class Meta:
+        app_label = 'web'
+
+    required_equipment = models.ManyToManyField("web.Item",
+                                                verbose_name='Required equipment',
+                                                blank=True
+                                                )
+    code = models.CharField(max_length=20,
+                            verbose_name='Appointment code'
+                            )
+    description = models.CharField(max_length=2000,
+                                   verbose_name='Appointment description'
+                                   )
+    default_duration = models.IntegerField(
+        verbose_name='Default duration (in minutes)'
+    )
+    calendar_color_priority = models.IntegerField(
+        verbose_name='Calendar color priority',
+        default=1
+    )
+    calendar_color = models.CharField(max_length=2000,
+                                      verbose_name='Calendar color',
+                                      default=APPOINTMENT_TYPE_DEFAULT_COLOR
+                                      )
+    calendar_font_color = models.CharField(max_length=2000,
+                                           verbose_name='Calendar color',
+                                           default=APPOINTMENT_TYPE_DEFAULT_FONT_COLOR
+                                           )
+    rest_time = models.IntegerField(
+        verbose_name='Suggested rest time',
+        default=0
+    )
+    can_be_parallelized = models.BooleanField(
+        verbose_name='Can be parallelized',
+        default=False
+    )
+    REQ_ROLE_CHOICES = (
+        ('DOCTOR', 'Doctor'),
+        ('NURSE', 'Nurse'),
+        ('PSYCHOLOGIST', 'Psychologist'),
+        ('ANY', 'Any')
+    )
+    required_worker = models.CharField(max_length=20, choices=REQ_ROLE_CHOICES,
+                                       verbose_name='Type of worker required for appointment',
+                                       default='ANY'
+                                       )
+
+    class Meta:
+        ordering = ['description']
+
+    def __str__(self):
+        return self.description
+
+    def __unicode__(self):
+        return self.description
diff --git a/smash/web/models/avaibility.py b/smash/web/models/avaibility.py
new file mode 100644
index 0000000000000000000000000000000000000000..bbc465ade1dca8b6b2a417c863561dfa92076ea1
--- /dev/null
+++ b/smash/web/models/avaibility.py
@@ -0,0 +1,32 @@
+# coding=utf-8
+from django.db import models
+
+__author__ = 'Valentin Grouès'
+
+
+class Avaibility(models.Model):
+    class Meta:
+        app_label = 'web'
+
+    person = models.ForeignKey("web.Worker", on_delete=models.CASCADE,
+                               verbose_name='Worker'
+                               )
+    day_number = models.IntegerField(
+        verbose_name='Day of the week'
+    )
+    available_from = models.TimeField(
+        verbose_name='Avaible since'
+    )
+    available_till = models.TimeField(
+        verbose_name='Avaible until'
+    )
+    is_current = models.BooleanField(
+        verbose_name='Is current?',
+        default=True
+    )
+
+    def __str__(self):
+        return "%d %s %s" % (self.day_number, self.person.last_name, self.person.first_name)
+
+    def __unicode__(self):
+        return "%d %s %s" % (self.day_number, self.person.last_name, self.person.first_name)
diff --git a/smash/web/models/constants.py b/smash/web/models/constants.py
new file mode 100644
index 0000000000000000000000000000000000000000..1d8ad089f2621f01d3053eb1f0f6c08d58c69b2f
--- /dev/null
+++ b/smash/web/models/constants.py
@@ -0,0 +1,16 @@
+# coding=utf-8
+__author__ = 'Valentin Grouès'
+BOOL_CHOICES = ((True, 'Yes'), (False, 'No'))
+SEX_CHOICES_MALE = 'M'
+SEX_CHOICES_FEMALE = 'F'
+SEX_CHOICES = (
+    (SEX_CHOICES_MALE, 'Male'),
+    (SEX_CHOICES_FEMALE, 'Female'),
+)
+SUBJECT_TYPE_CHOICES_CONTROL = 'C'
+SUBJECT_TYPE_CHOICES = {
+    SUBJECT_TYPE_CHOICES_CONTROL: 'CONTROL',
+    'P': 'PATIENT',
+}
+APPOINTMENT_TYPE_DEFAULT_COLOR = '#cfc600'
+APPOINTMENT_TYPE_DEFAULT_FONT_COLOR = '#00000'
diff --git a/smash/web/models/flying_team.py b/smash/web/models/flying_team.py
new file mode 100644
index 0000000000000000000000000000000000000000..04d6f6705bc00c049793836dd27985398f921094
--- /dev/null
+++ b/smash/web/models/flying_team.py
@@ -0,0 +1,38 @@
+# coding=utf-8
+from django.db import models
+
+__author__ = 'Valentin Grouès'
+
+
+class FlyingTeam(models.Model):
+    class Meta:
+        app_label = 'web'
+
+    # doctor = models.ForeignKey(Worker, related_name='FlyingTeamDoctor',
+    #     verbose_name='Doctor'
+    # )
+    # nurse = models.ForeignKey(Worker, related_name='FlyingTeamNurse',
+    #     verbose_name='Nurse'
+    # )
+    # psychologist = models.ForeignKey(Worker, related_name='FlyingTeamPsychologist',
+    #     verbose_name='Psychologist'
+    # )
+    # datetime_called = models.DateTimeField(
+    #     verbose_name='Created on'
+    # )
+    # datetime_until = models.DateTimeField(
+    #     verbose_name='Disbanded on'
+    # )
+    #
+    # def __str__(self):
+    #     return "%s %s %s" % (self.doctor.last_name, self.nurse.last_name, self.psychologist.last_name)
+    #
+    # def __unicode__(self):
+    #     return "%s %s %s" % (self.doctor.last_name, self.nurse.last_name, self.psychologist.last_name)
+    place = models.CharField(max_length=255, verbose_name='Place')
+
+    def __str__(self):
+        return "%s" % self.place
+
+    def __unicode__(self):
+        return "%s" % self.place
diff --git a/smash/web/models/holiday.py b/smash/web/models/holiday.py
new file mode 100644
index 0000000000000000000000000000000000000000..6e3f37b454f37ba604bbe032c90b8f7b9f04e181
--- /dev/null
+++ b/smash/web/models/holiday.py
@@ -0,0 +1,25 @@
+# coding=utf-8
+from django.db import models
+
+__author__ = 'Valentin Grouès'
+
+
+class Holiday(models.Model):
+    class Meta:
+        app_label = 'web'
+
+    person = models.ForeignKey("web.Worker", on_delete=models.CASCADE,
+                               verbose_name='Worker'
+                               )
+    datetime_start = models.DateTimeField(
+        verbose_name='On leave since'
+    )
+    datetime_end = models.DateTimeField(
+        verbose_name='On leave until'
+    )
+
+    def __str__(self):
+        return "%s %s" % (self.person.first_name, self.person.last_name)
+
+    def __unicode__(self):
+        return "%s %s" % (self.person.first_name, self.person.last_name)
diff --git a/smash/web/models/item.py b/smash/web/models/item.py
new file mode 100644
index 0000000000000000000000000000000000000000..8946783178382a01faa66f9412acd8a37e08a9e6
--- /dev/null
+++ b/smash/web/models/item.py
@@ -0,0 +1,29 @@
+# coding=utf-8
+from django.db import models
+
+__author__ = 'Valentin Grouès'
+
+
+class Item(models.Model):
+    class Meta:
+        app_label = 'web'
+
+    is_fixed = models.BooleanField(
+        default=False,
+        verbose_name='Is the item fixed?'
+    )
+
+    disposable = models.BooleanField(
+        default=False,
+        verbose_name='Disposable set'
+    )
+
+    name = models.CharField(max_length=255,
+                            verbose_name='Name'
+                            )
+
+    def __str__(self):
+        return self.name
+
+    def __unicode__(self):
+        return self.name
diff --git a/smash/web/models/language.py b/smash/web/models/language.py
new file mode 100644
index 0000000000000000000000000000000000000000..582191834a0d00922561e01341102a6586110c1e
--- /dev/null
+++ b/smash/web/models/language.py
@@ -0,0 +1,24 @@
+# coding=utf-8
+from django.db import models
+
+__author__ = 'Valentin Grouès'
+
+
+class Language(models.Model):
+    class Meta:
+        app_label = 'web'
+
+    name = models.CharField(max_length=20)
+    image = models.ImageField()
+
+    def __str__(self):
+        return self.name
+
+    def image_img(self):
+        if self.image:
+            return u'<img class="flag-icon" src="%s" />' % self.image.url
+        else:
+            return 'No image'
+
+    image_img.short_description = 'Flag icon'
+    image_img.allow_tags = True
diff --git a/smash/web/models/location.py b/smash/web/models/location.py
new file mode 100644
index 0000000000000000000000000000000000000000..a81f2af303079029647c42755cf0b0202ded476d
--- /dev/null
+++ b/smash/web/models/location.py
@@ -0,0 +1,17 @@
+# coding=utf-8
+from django.db import models
+
+__author__ = 'Valentin Grouès'
+
+
+class Location(models.Model):
+    class Meta:
+        app_label = 'web'
+
+    name = models.CharField(max_length=20)
+
+    def __str__(self):
+        return "%s" % self.name
+
+    def __unicode__(self):
+        return "%s" % self.name
diff --git a/smash/web/models/room.py b/smash/web/models/room.py
new file mode 100644
index 0000000000000000000000000000000000000000..1a02850b8a24bc4f591c3038eae465af14188e69
--- /dev/null
+++ b/smash/web/models/room.py
@@ -0,0 +1,35 @@
+# coding=utf-8
+from django.db import models
+
+__author__ = 'Valentin Grouès'
+
+
+class Room(models.Model):
+    equipment = models.ManyToManyField("web.Item",
+                                       verbose_name='On-site equipment',
+                                       blank=True
+                                       )
+    owner = models.CharField(max_length=50,
+                             verbose_name='Owner'
+                             )
+    address = models.CharField(max_length=255,
+                               verbose_name='Address'
+                               )
+    city = models.CharField(max_length=50,
+                            verbose_name='City'
+                            )
+    room_number = models.IntegerField(
+        verbose_name='Room number'
+    )
+    floor = models.IntegerField(
+        verbose_name='Floor'
+    )
+    is_vehicle = models.BooleanField(
+        verbose_name='Is a vehicle?'
+    )
+
+    def __str__(self):
+        return "%d %s %s" % (self.room_number, self.address, self.city)
+
+    def __unicode__(self):
+        return "%d %s %s" % (self.room_number, self.address, self.city)
diff --git a/smash/web/models/subject.py b/smash/web/models/subject.py
new file mode 100644
index 0000000000000000000000000000000000000000..2eddd765f1f0f1c2442d52b266cff76a7507a487
--- /dev/null
+++ b/smash/web/models/subject.py
@@ -0,0 +1,177 @@
+# coding=utf-8
+from django.db import models
+
+from constants import BOOL_CHOICES, SEX_CHOICES, SUBJECT_TYPE_CHOICES
+from . import Appointment, Language, Location, Visit
+
+__author__ = 'Valentin Grouès'
+
+
+class Subject(models.Model):
+    class Meta:
+        app_label = 'web'
+
+    def finish_all_visits(self):
+        visits = Visit.objects.filter(subject=self, is_finished=False)
+        for visit in visits:
+            visit.is_finished = True
+            visit.save()
+
+    def finish_all_appointments(self):
+        appointments = Appointment.objects.filter(visit__subject=self, status=Appointment.APPOINTMENT_STATUS_SCHEDULED)
+        for appointment in appointments:
+            appointment.status = Appointment.APPOINTMENT_STATUS_CANCELLED
+            appointment.save()
+
+    def mark_as_dead(self):
+        self.dead = True
+        self.save()
+
+        self.finish_all_visits()
+        self.finish_all_appointments()
+
+    def mark_as_rejected(self):
+        self.resigned = True
+        self.save()
+
+        self.finish_all_visits()
+        self.finish_all_appointments()
+
+    sex = models.CharField(max_length=1,
+                           choices=SEX_CHOICES,
+                           verbose_name='Sex'
+                           )
+    postponed = models.BooleanField(choices=BOOL_CHOICES,
+                                    verbose_name='Postponed',
+                                    default=False
+                                    )
+    datetime_contact_reminder = models.DateField(
+        null=True,
+        blank=True,
+        verbose_name='Contact on',
+    )
+    type = models.CharField(max_length=1,
+                            choices=SUBJECT_TYPE_CHOICES.items(),
+                            verbose_name='Type'
+                            )
+
+    dead = models.BooleanField(
+        verbose_name='Dead',
+        default=False,
+        editable=False
+    )
+    resigned = models.BooleanField(
+        verbose_name='Resigned',
+        default=False,
+        editable=False
+    )
+    default_location = models.ForeignKey(Location,
+                                         verbose_name='Default appointment location',
+                                         )
+    first_name = models.CharField(max_length=50,
+                                  verbose_name='First name'
+                                  )
+    last_name = models.CharField(max_length=50,
+                                 verbose_name='Last name'
+                                 )
+    languages = models.ManyToManyField(Language,
+                                       blank=True,
+                                       verbose_name='Known languages'
+                                       )
+    default_written_communication_language = models.ForeignKey(Language,
+                                                               null=True,
+                                                               blank=True,
+                                                               related_name="%(class)s_written_comunication",
+                                                               verbose_name='Default language for document generation'
+                                                               )
+    phone_number = models.CharField(max_length=20,
+                                    null=True,
+                                    blank=True,
+                                    verbose_name='Phone number'
+                                    )
+    phone_number_2 = models.CharField(max_length=20,
+                                      null=True,
+                                      blank=True,
+                                      verbose_name='Phone number 2'
+                                      )
+    phone_number_3 = models.CharField(max_length=20,
+                                      null=True,
+                                      blank=True,
+                                      verbose_name='Phone number 3'
+                                      )
+    email = models.EmailField(
+        null=True,
+        blank=True,
+        verbose_name='E-mail'
+    )
+    date_born = models.DateField(
+        null=True,
+        blank=True,
+        verbose_name='Date of birth (YYYY-MM-DD)'
+    )
+    address = models.CharField(max_length=255,
+                               blank=True,
+                               verbose_name='Address'
+                               )
+    postal_code = models.CharField(max_length=7,
+                                   blank=True,
+                                   verbose_name='Postal code'
+                                   )
+    city = models.CharField(max_length=50,
+                            blank=True,
+                            verbose_name='City'
+                            )
+    country = models.CharField(max_length=50,
+                               verbose_name='Country'
+                               )
+    screening_number = models.CharField(max_length=50,
+                                        unique=True,
+                                        verbose_name='Screening number'
+                                        )
+    nd_number = models.CharField(max_length=6,
+                                 blank=True,
+                                 verbose_name='ND number'
+                                 )
+    mpower_id = models.CharField(max_length=20,
+                                 blank=True,
+                                 verbose_name='MPower ID'
+                                 )
+    comments = models.TextField(max_length=2000,
+                                blank=True,
+                                verbose_name='Comments'
+                                )
+    date_added = models.DateField(verbose_name='Added on',
+                                  auto_now=True
+                                  )
+    referral = models.CharField(max_length=128,
+                                null=True,
+                                blank=True,
+                                verbose_name='Referred by'
+                                )
+    diagnosis = models.CharField(max_length=128,
+                                 null=True,
+                                 blank=True,
+                                 verbose_name='Diagnosis'
+                                 )
+    year_of_diagnosis = models.IntegerField(
+        default=0,
+        null=True,
+        blank=True,
+        verbose_name='Year of diagnosis (YYYY)'
+    )
+
+    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)
+
+    def __unicode__(self):
+        return "%s %s" % (self.first_name, self.last_name)
diff --git a/smash/web/models/visit.py b/smash/web/models/visit.py
new file mode 100644
index 0000000000000000000000000000000000000000..313b960dc86ddf35f23e54ebbdd3ee5724f64b6a
--- /dev/null
+++ b/smash/web/models/visit.py
@@ -0,0 +1,63 @@
+# coding=utf-8
+import datetime
+
+from django.db import models
+
+from constants import BOOL_CHOICES, SUBJECT_TYPE_CHOICES_CONTROL
+
+__author__ = 'Valentin Grouès'
+
+
+class Visit(models.Model):
+    class Meta:
+        app_label = 'web'
+
+    subject = models.ForeignKey("web.Subject", on_delete=models.CASCADE,
+                                verbose_name='Subject'
+                                )
+    datetime_begin = models.DateTimeField(
+        verbose_name='Visit starts at'
+    )
+    datetime_end = models.DateTimeField(
+        verbose_name='Visit ends at'
+    )  # Deadline before which all appointments need to be scheduled
+
+    is_finished = models.BooleanField(
+        verbose_name='Has ended',
+        default=False
+    )
+    post_mail_sent = models.BooleanField(choices=BOOL_CHOICES,
+                                         verbose_name='Post mail sent',
+                                         default=False
+                                         )
+    appointment_types = models.ManyToManyField("web.AppointmentType",
+                                               verbose_name='Requested appointments',
+                                               blank=True,
+                                               )
+
+    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()
+
+        if (not self.subject.dead) and (not self.subject.resigned):
+            visit_started = self.datetime_begin
+
+            time_to_next_visit = datetime.timedelta(days=365)
+            if self.subject.type == SUBJECT_TYPE_CHOICES_CONTROL:
+                time_to_next_visit = datetime.timedelta(days=365 * 3 + 366)
+
+            Visit.objects.create(
+                subject=self.subject,
+                datetime_begin=visit_started + time_to_next_visit,
+                datetime_end=visit_started + time_to_next_visit + datetime.timedelta(days=93)
+            )
diff --git a/smash/web/models/worker.py b/smash/web/models/worker.py
new file mode 100644
index 0000000000000000000000000000000000000000..d6bf3c1e5fdac0fd3eebe83ab0303f188fce4a1d
--- /dev/null
+++ b/smash/web/models/worker.py
@@ -0,0 +1,95 @@
+# coding=utf-8
+import datetime
+
+from django.contrib.auth.models import User
+from django.db import models
+
+__author__ = 'Valentin Grouès'
+
+
+class Worker(models.Model):
+    class Meta:
+        app_label = 'web'
+
+    languages = models.ManyToManyField("web.Language",
+                                       verbose_name='Known languages'
+                                       )
+    locations = models.ManyToManyField("web.Location",
+                                       verbose_name='Locations'
+                                       )
+    appointments = models.ManyToManyField('web.Appointment', blank=True,
+                                          verbose_name='Appointments'
+                                          )
+    user = models.OneToOneField(User, blank=True, null=True,
+                                verbose_name='Username'
+                                )
+    first_name = models.CharField(max_length=50,
+                                  verbose_name='First name'
+                                  )
+    last_name = models.CharField(max_length=50,
+                                 verbose_name='Last name'
+                                 )
+    phone_number = models.CharField(max_length=20,
+                                    verbose_name='Phone number'
+                                    )
+    unit = models.CharField(max_length=50,
+                            verbose_name='Unit'
+                            )
+    email = models.EmailField(
+        verbose_name='E-mail'
+    )
+    ROLE_CHOICES_SECRETARY = "SECRETARY"
+    ROLE_CHOICES = (
+        ('DOCTOR', 'Doctor'),
+        ('NURSE', 'Nurse'),
+        ('PSYCHOLOGIST', 'Psychologist'),
+        ('TECHNICIAN', 'Technician'),
+        (ROLE_CHOICES_SECRETARY, 'Secretary')
+    )
+    role = models.CharField(max_length=20, choices=ROLE_CHOICES,
+                            verbose_name='Role'
+                            )
+    specialization = models.CharField(max_length=20,
+                                      verbose_name='Specialization'
+                                      )
+
+    def is_on_leave(self):
+        if len(self.holiday_set.filter(datetime_end__gt=datetime.datetime.now(),
+                                       datetime_start__lt=datetime.datetime.now())):
+            return True
+        return False
+
+    @staticmethod
+    def get_by_user(the_user):
+        if isinstance(the_user, User):
+            workers = Worker.objects.filter(user=the_user)
+            if len(workers) > 0:
+                return workers[0]
+            else:
+                return None
+        elif isinstance(the_user, Worker):
+            return the_user
+        elif the_user is not None:
+            raise TypeError("Unknown class type: " + the_user.__class__.__name__)
+        else:
+            return None
+
+    @staticmethod
+    def get_details(the_user):
+        if not the_user.is_authenticated:
+            return 'Guest', 'Test account'
+
+        person = Worker.objects.filter(user=the_user)
+
+        if len(person) == 0:
+            return the_user.get_full_name(), '<No worker information>'
+        else:
+            # For get_*_display, see:
+            # https://docs.djangoproject.com/en/1.10/topics/db/models/#field-options
+            return unicode(person[0]), person[0].get_role_display()
+
+    def __str__(self):
+        return "%s %s" % (self.first_name, self.last_name)
+
+    def __unicode__(self):
+        return "%s %s" % (self.first_name, self.last_name)
diff --git a/smash/web/tests/functions.py b/smash/web/tests/functions.py
index 3b5f1a3e21a513384ed62378fb2f1a3fd186041d..4adab0a0359062730e5d801833ce0a2ddae4fe3d 100644
--- a/smash/web/tests/functions.py
+++ b/smash/web/tests/functions.py
@@ -3,7 +3,8 @@ import datetime
 from django.contrib.auth.models import User
 
 from web.models import Location, AppointmentType, Subject, Worker, Visit, Appointment
-from web.views import get_today_midnight_date
+from web.models.constants import SEX_CHOICES_MALE, SUBJECT_TYPE_CHOICES_CONTROL
+from web.views.notifications import get_today_midnight_date
 
 
 def create_location(name="test"):
@@ -31,8 +32,8 @@ def create_subject():
         first_name="Piotr",
         last_name="Gawron",
         default_location=get_test_location(),
-        sex=Subject.SEX_CHOICES_MALE,
-        type=Subject.SUBJECT_TYPE_CHOICES_CONTROL,
+        sex=SEX_CHOICES_MALE,
+        type=SUBJECT_TYPE_CHOICES_CONTROL,
         screening_number="piotr's number",
         country="france")
 
diff --git a/smash/web/tests/test_SubjectAddForm.py b/smash/web/tests/test_SubjectAddForm.py
index c0af1e319da6431fc861173f4ddbfc7444d540ca..8cac955ff624a5d7d322e4695037ad8a8704c86e 100644
--- a/smash/web/tests/test_SubjectAddForm.py
+++ b/smash/web/tests/test_SubjectAddForm.py
@@ -2,7 +2,7 @@ from django.test import TestCase
 
 from functions import get_test_location
 from web.forms import SubjectAddForm
-from web.models import Subject
+from web.models.constants import SEX_CHOICES_MALE, SUBJECT_TYPE_CHOICES_CONTROL
 
 
 class SubjectAddFormTests(TestCase):
@@ -10,8 +10,8 @@ class SubjectAddFormTests(TestCase):
         location = get_test_location()
         self.sample_data = {'first_name': 'name',
                             'last_name': 'name',
-                            'sex': Subject.SEX_CHOICES_MALE,
-                            'type': Subject.SUBJECT_TYPE_CHOICES_CONTROL,
+                            'sex': SEX_CHOICES_MALE,
+                            'type': SUBJECT_TYPE_CHOICES_CONTROL,
                             'default_location': location.id,
                             'screening_number': "123",
                             'country': 'Luxembourg'
diff --git a/smash/web/tests/test_SubjectEditForm.py b/smash/web/tests/test_SubjectEditForm.py
index 7baf1732c696d75c0899e34c95f27e97b526736b..a5de02f96f94b7abf3c5e3e0f49303dbfc8ba68f 100644
--- a/smash/web/tests/test_SubjectEditForm.py
+++ b/smash/web/tests/test_SubjectEditForm.py
@@ -4,6 +4,7 @@ from functions import get_test_location
 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
 
 
 class SubjectEditFormTests(TestCase):
@@ -11,8 +12,8 @@ class SubjectEditFormTests(TestCase):
         location = get_test_location()
         self.sample_data = {'first_name': 'name',
                             'last_name': 'name',
-                            'sex': Subject.SEX_CHOICES_MALE,
-                            'type': Subject.SUBJECT_TYPE_CHOICES_CONTROL,
+                            'sex': SEX_CHOICES_MALE,
+                            'type': SUBJECT_TYPE_CHOICES_CONTROL,
                             'default_location': location.id,
                             'country': 'Luxembourg',
                             'screening_number': '123',
diff --git a/smash/web/tests/test_view_functions.py b/smash/web/tests/test_view_functions.py
index b43d3c33b6064ce5b4d29bca4b49c2e07ae3240a..db206b6b31194cdfbe5a955e6973d872a0edf87f 100644
--- a/smash/web/tests/test_view_functions.py
+++ b/smash/web/tests/test_view_functions.py
@@ -1,7 +1,7 @@
 from django.test import TestCase
 
 from functions import create_location, create_user, create_worker, get_test_location
-from web.views import get_filter_locations
+from web.views.notifications import get_filter_locations
 
 
 class ViewFunctionsTests(TestCase):
diff --git a/smash/web/tests/test_view_kit_request.py b/smash/web/tests/test_view_kit_request.py
index 19883898505928742ed2d051fb1bef542826ae10..556308fd0066b8449868cf91215fc6ca154fba10 100644
--- a/smash/web/tests/test_view_kit_request.py
+++ b/smash/web/tests/test_view_kit_request.py
@@ -3,9 +3,10 @@ import datetime
 from django.test import TestCase, RequestFactory
 from django.urls import reverse
 
-from functions import create_user, create_appointment_type, create_appointment, create_location
+from functions import create_user, create_appointment_type, create_appointment
 from web.models import Item, Appointment
-from web.views import kit_requests, get_today_midnight_date
+from web.views.kit import kit_requests
+from web.views.notifications import get_today_midnight_date
 
 
 class ViewFunctionsTests(TestCase):
diff --git a/smash/web/tests/test_view_notifications.py b/smash/web/tests/test_view_notifications.py
index 60095c78547145cc621cfd586fc0233db88a60b5..cf193787194089fdb78612a2ef2c5962bb97aa44 100644
--- a/smash/web/tests/test_view_notifications.py
+++ b/smash/web/tests/test_view_notifications.py
@@ -7,10 +7,11 @@ from functions import create_appointment, create_location, create_worker, create
 from functions import create_subject
 from functions import create_visit
 from web.models import Appointment, Location
-from web.views import get_exceeded_visit_notifications_count, get_approaching_visits_without_appointments_count, \
-    get_today_midnight_date, get_subject_with_no_visit_notifications_count, get_unfinished_appointments_count, \
+from web.views.notifications import get_exceeded_visit_notifications_count, \
+    get_approaching_visits_without_appointments_count, \
+    get_subject_with_no_visit_notifications_count, get_unfinished_appointments_count, \
     get_approaching_visits_for_mail_contact_count, get_subjects_with_reminder_count, \
-    get_visits_without_appointments_count
+    get_visits_without_appointments_count, get_today_midnight_date
 
 
 class NotificationViewTests(TestCase):
diff --git a/smash/web/tests/test_view_visit.py b/smash/web/tests/test_view_visit.py
index da9d39fccd4ed4be075fd468ff2c4e3ff9f2a21c..e1979cea43307c6793cc2685ee0f99146170264b 100644
--- a/smash/web/tests/test_view_visit.py
+++ b/smash/web/tests/test_view_visit.py
@@ -3,7 +3,7 @@ from django.test import TestCase, RequestFactory
 from django.urls import reverse
 
 from functions import create_subject, create_visit, create_appointment
-from web.views import visit_details
+from web.views.visit import visit_details
 
 
 class VisitViewTests(TestCase):
diff --git a/smash/web/urls.py b/smash/web/urls.py
index 6cbc1fcefde1c3ad82ea887069b1d56693b3dee1..0d0d0b6356aa8491e0119076ce6637bde3693adf 100644
--- a/smash/web/urls.py
+++ b/smash/web/urls.py
@@ -20,64 +20,68 @@ from django.conf.urls import url
 from web import views
 
 urlpatterns = [
-    url(r'^appointments$', views.appointments, name='web.views.appointments'),
-    url(r'^appointments/unfinished$', views.unfinished_appointments, name='web.views.unfinished_appointments'),
-    url(r'^appointments/details/(?P<id>\d+)$', views.appointment_details, name='web.views.appointment_details'),
-    url(r'^appointments/add/(?P<id>\d+)$', views.appointment_add, name='web.views.appointment_add'),
-    url(r'^appointments/edit/(?P<id>\d+)$', views.appointment_edit, name='web.views.appointment_edit'),
-    url(r'^appointments/edit_datetime/(?P<id>\d+)$', views.appointment_edit_datetime,
+    url(r'^appointments$', views.appointment.appointments, name='web.views.appointments'),
+    url(r'^appointments/unfinished$', views.appointment.unfinished_appointments,
+        name='web.views.unfinished_appointments'),
+    url(r'^appointments/details/(?P<id>\d+)$', views.appointment.appointment_details,
+        name='web.views.appointment_details'),
+    url(r'^appointments/add/(?P<id>\d+)$', views.appointment.appointment_add, name='web.views.appointment_add'),
+    url(r'^appointments/edit/(?P<id>\d+)$', views.appointment.appointment_edit, name='web.views.appointment_edit'),
+    url(r'^appointments/edit_datetime/(?P<id>\d+)$', views.appointment.appointment_edit_datetime,
         name='web.views.appointment_edit_datetime'),
-    url(r'^appointments/mark/(?P<id>\d+)/(?P<as_what>[A-z]+)$', views.appointment_mark,
+    url(r'^appointments/mark/(?P<id>\d+)/(?P<as_what>[A-z]+)$', views.appointment.appointment_mark,
         name='web.views.appointment_mark'),
 
-    url(r'^visits$', views.visits, name='web.views.visits'),
-    url(r'^visits/exceeded$', views.exceeded_visits, name='web.views.exceeded_visits'),
-    url(r'^visits/unfinished$', views.unfinished_visits, name='web.views.unfinished_visits'),
-    url(r'^visits/approaching$', views.approaching_visits_without_appointments,
+    url(r'^visits$', views.visit.visits, name='web.views.visits'),
+    url(r'^visits/exceeded$', views.visit.exceeded_visits, name='web.views.exceeded_visits'),
+    url(r'^visits/unfinished$', views.visit.unfinished_visits, name='web.views.unfinished_visits'),
+    url(r'^visits/approaching$', views.visit.approaching_visits_without_appointments,
         name='web.views.approaching_visits_without_appointments'),
-    url(r'^visits/approaching_post_mail$', views.approaching_visits_for_mail_contact,
+    url(r'^visits/approaching_post_mail$', views.visit.approaching_visits_for_mail_contact,
         name='web.views.approaching_visits_for_mail_contact'),
-    url(r'^visits/missing_appointments$', views.visits_with_missing_appointments,
+    url(r'^visits/missing_appointments$', views.visit.visits_with_missing_appointments,
         name='web.views.visits_with_missing_appointments'),
-    url(r'^visits/details/(?P<id>\d+)$', views.visit_details, name='web.views.visit_details'),
-    url(r'^visits/add$', views.visit_add, name='web.views.visit_add'),
-    url(r'^visits/add/(?P<subject_id>\d+)$', views.visit_add, name='web.views.visit_add'),
-    url(r'^visit/mark/(?P<id>\d+)/(?P<as_what>[A-z]+)$', views.visit_mark, name='web.views.visit_mark'),
+    url(r'^visits/details/(?P<id>\d+)$', views.visit.visit_details, name='web.views.visit_details'),
+    url(r'^visits/add$', views.visit.visit_add, name='web.views.visit_add'),
+    url(r'^visits/add/(?P<subject_id>\d+)$', views.visit.visit_add, name='web.views.visit_add'),
+    url(r'^visit/mark/(?P<id>\d+)/(?P<as_what>[A-z]+)$', views.visit.visit_mark, name='web.views.visit_mark'),
 
-    url(r'^subjects$', views.subjects, name='web.views.subjects'),
-    url(r'^subjects/no_visit$', views.subject_no_visits, name='web.views.subject_no_visits'),
-    url(r'^subjects/equire_contact$', views.subject_require_contact, name='web.views.subject_require_contact'),
-    url(r'^subjects/add$', views.subject_add, name='web.views.subject_add'),
-    url(r'^subjects/subject_visit_details/(?P<id>\d+)$', views.subject_visit_details,
+    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/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'),
-    url(r'^subjects/edit/(?P<id>\d+)$', views.subject_edit, name='web.views.subject_edit'),
-    url(r'^subjects/delete/(?P<id>\d+)$', views.subject_delete, name='web.views.subject_delete'),
-    url(r'^subjects/mark/(?P<id>\d+)/(?P<as_what>[A-z]+)$', views.subject_mark, name='web.views.subject_mark'),
+    url(r'^subjects/edit/(?P<id>\d+)$', views.subject.subject_edit, name='web.views.subject_edit'),
+    url(r'^subjects/delete/(?P<id>\d+)$', views.subject.subject_delete, name='web.views.subject_delete'),
+    url(r'^subjects/mark/(?P<id>\d+)/(?P<as_what>[A-z]+)$', views.subject.subject_mark, name='web.views.subject_mark'),
 
-    url(r'^doctors$', views.doctors, name='web.views.doctors'),
-    url(r'^doctors/add$', views.doctor_add, name='web.views.doctor_add'),
-    url(r'^doctors/details/(?P<doctor_id>\d+)$', views.doctor_details, name='web.views.doctor_details'),
-    url(r'^doctors/edit/(?P<doctor_id>\d+)$', views.doctor_edit, name='web.views.doctor_edit'),
-    url(r'^doctors/availability/(?P<doctor_id>\d+)$', views.doctor_availability, name='web.views.doctor_availability'),
-    url(r'^doctors/availability/(?P<doctor_id>\d+)/delete/(?P<availability_id>\d+)$', views.doctor_availability_delete,
+    url(r'^doctors$', views.doctor.doctors, name='web.views.doctors'),
+    url(r'^doctors/add$', views.doctor.doctor_add, name='web.views.doctor_add'),
+    url(r'^doctors/details/(?P<doctor_id>\d+)$', views.doctor.doctor_details, name='web.views.doctor_details'),
+    url(r'^doctors/edit/(?P<doctor_id>\d+)$', views.doctor.doctor_edit, name='web.views.doctor_edit'),
+    url(r'^doctors/availability/(?P<doctor_id>\d+)$', views.doctor.doctor_availability,
+        name='web.views.doctor_availability'),
+    url(r'^doctors/availability/(?P<doctor_id>\d+)/delete/(?P<availability_id>\d+)$',
+        views.doctor.doctor_availability_delete,
         name='web.views.doctor_availability_delete'),
 
-    url(r'^equipment_and_rooms$', views.equipment_and_rooms, name='web.views.equipment_and_rooms'),
-    url(r'^equipment_and_rooms/eqdef$', views.equipment_def, name='web.views.equipment_def'),
-    url(r'^equipment_and_rooms/kit_requests$', views.kit_requests, name='web.views.kit_requests'),
-    url(r'^equipment_and_rooms/kit_requests/(?P<start_date>[\w-]+)/$', views.kit_requests_send_mail,
+    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/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'),
     url(r'^equipment_and_rooms/kit_requests/(?P<start_date>[\w-]+)/(?P<end_date>[\w-]+)/$',
-        views.kit_requests_send_mail, name='web.views.kit_requests_send_mail'),
+        views.kit.kit_requests_send_mail, name='web.views.kit_requests_send_mail'),
 
-    url(r'^mail_templates$', views.mail_templates, name='web.views.mail_templates'),
-    url(r'^statistics$', views.statistics, name='web.views.statistics'),
+    url(r'^mail_templates$', views.mails.mail_templates, name='web.views.mail_templates'),
+    url(r'^statistics$', views.statistics.statistics, name='web.views.statistics'),
 
-    url(r'^export$', views.export, name='web.views.export'),
-    url(r'^export/(?P<type>[A-z]+)$', views.export_to_csv2, name='web.views.export_to_csv2'),
+    url(r'^export$', views.export.export, name='web.views.export'),
+    url(r'^export/(?P<type>[A-z]+)$', views.export.export_to_csv2, name='web.views.export_to_csv2'),
 
-    url(r'^login$', views.login, name='web.views.login'),
-    url(r'^logout$', views.logout, name='web.views.logout'),
+    url(r'^login$', views.auth.login, name='web.views.login'),
+    url(r'^logout$', views.auth.logout, name='web.views.logout'),
 
     url(r'^$', views.index, name='web.views.index')
 ]
diff --git a/smash/web/views.py b/smash/web/views.py
deleted file mode 100644
index 989dd33c6190687c41bd808cadc8a6ae00fdd7be..0000000000000000000000000000000000000000
--- a/smash/web/views.py
+++ /dev/null
@@ -1,890 +0,0 @@
-from __future__ import unicode_literals
-
-import csv
-import datetime
-
-from django.contrib.auth.decorators import login_required
-from django.contrib.auth.models import User, AnonymousUser
-from django.db.models import Case
-from django.db.models import Count
-from django.db.models import When
-from django.forms import HiddenInput
-from django.http import HttpResponse
-from django.shortcuts import redirect, get_object_or_404
-from django.shortcuts import render
-from django.utils.dateparse import parse_datetime
-
-from auth import do_logout, do_login
-from forms import SubjectDetailForm, WorkerEditForm, WorkerDetailForm, AppointmentDetailForm, AppointmentAddForm, \
-    AppointmentEditForm, KitRequestForm, SubjectEditForm, SubjectAddForm, VisitAddForm, WorkerAddForm, VisitDetailForm, \
-    StatisticsForm
-from models import Worker, Location, Visit, Subject, Appointment, Avaibility, Item, AppointmentType
-from statistics import StatisticsManager
-from statistics import get_previous_year_and_month
-
-handler404 = 'web.views.e404_page_not_found'
-handler500 = 'web.views.e500_error'
-handler403 = 'web.views.e403_permission_denied'
-handler400 = 'web.views.e400_bad_request'
-
-
-def index(request):
-    if request.user.is_authenticated():
-        return redirect(appointments)
-    return redirect(login)
-
-
-def e404_page_not_found(request, context=None):
-    return render(request, "errors/404.html", context, status=404)
-
-
-def e500_error(request, context=None):
-    return render(request, "errors/500.html", context, status=500)
-
-
-def e403_permission_denied(request, context=None):
-    return render(request, "errors/403.html", context, status=403)
-
-
-def e400_bad_request(request, context=None):
-    return render(request, "errors/400.html", context, status=400)
-
-
-def login(request):
-    context = {
-        'state': 'initial'
-    }
-    if request.GET and request.GET.get('error'):
-        context['state'] = request.GET.get('error')
-
-    if request.method == "GET" and request.GET:
-        context['next'] = request.GET.get('next')
-
-    if request.method == "POST" and request.POST:
-        state, message = do_login(request)
-        if state:
-            if request.POST.get('next'):
-                return redirect(request.POST.get('next'))
-            else:
-                return redirect(appointments)
-        else:
-            return redirect('/login?error=' + message)
-    return render(request, "login.html", context)
-
-
-class NotificationCount(object):
-    title = ""
-    count = 0
-    style = ""
-    type = ''
-
-    def __init__(self, title="Unknown", count=0, style="fa fa-users text-aqua", type='web.views.appointments'):
-        self.title = title
-        self.count = count
-        self.style = style
-        self.type = type
-
-
-def get_filter_locations(user):
-    worker = None
-
-    if isinstance(user, User):
-        workers = Worker.objects.filter(user=user)
-        if len(workers) > 0:
-            worker = workers[0]
-    elif isinstance(user, Worker):
-        worker = user
-    elif isinstance(user, AnonymousUser):
-        # anonymous user shouldn't see anything
-        return Location.objects.filter(id=-1)
-    elif user is not None:
-        raise TypeError("Unknown class type: " + user.__class__.__name__)
-
-    if worker is None or worker.locations.count() == 0:
-        return Location.objects.all()
-    else:
-        return worker.locations.all()
-
-
-def get_exceeded_visits(user):
-    return Visit.objects.filter(datetime_end__lt=get_today_midnight_date(),
-                                is_finished=False,
-                                subject__default_location__in=get_filter_locations(user)
-                                ).order_by('datetime_begin')
-
-
-def get_exceeded_visit_notifications_count(user):
-    notification = NotificationCount(
-        title="exceeded visit time",
-        count=get_exceeded_visits(user).count(),
-        style="fa fa-thermometer-4 text-red",
-        type='web.views.exceeded_visits')
-    return notification
-
-
-def get_subjects_with_no_visit(user):
-    result = Subject.objects.annotate(my_count=Count(Case(When(visit__is_finished=False, then=1)))).filter(
-        dead=False,
-        resigned=False,
-        my_count=0,
-        default_location__in=get_filter_locations(user),
-        postponed=False,
-        datetime_contact_reminder__isnull=True,
-    )
-    return result
-
-
-def get_subjects_with_reminder(user):
-    tomorrow = get_today_midnight_date() + datetime.timedelta(days=1)
-
-    result = Subject.objects.filter(
-        dead=False,
-        resigned=False,
-        default_location__in=get_filter_locations(user),
-        datetime_contact_reminder__lt=tomorrow,
-    )
-    return result
-
-
-def get_subjects_with_reminder_count(user):
-    notification = NotificationCount(
-        title="subject required contact",
-        count=get_subjects_with_reminder(user).count(),
-        style="fa fa-users text-aqua",
-        type='web.views.subject_require_contact')
-    return notification
-
-
-def get_subject_with_no_visit_notifications_count(user):
-    notification = NotificationCount(
-        title="subject without visit",
-        count=get_subjects_with_no_visit(user).count(),
-        style="fa fa-users text-aqua",
-        type='web.views.subject_no_visits')
-    return notification
-
-
-def get_visits_without_appointments_count(user):
-    notification = NotificationCount(
-        title="unfinished visits ",
-        count=len(get_unfinished_visits(user)),
-        style="fa fa-user-times text-yellow",
-        type='web.views.unfinished_visits')
-    return notification
-
-
-def get_visits_with_missing_appointments_count(user):
-    notification = NotificationCount(
-        title="visits with missing appointments",
-        count=len(get_active_visits_with_missing_appointments(user)),
-        style="fa fa-user-times text-yellow",
-        type='web.views.visits_with_missing_appointments')
-    return notification
-
-
-def get_active_visits_without_appointments(user):
-    today = get_today_midnight_date()
-    return Visit.objects.annotate(
-        my_count=Count(Case(When(appointment__status=Appointment.APPOINTMENT_STATUS_SCHEDULED, then=1)))).filter(
-        datetime_begin__lt=today,
-        datetime_end__gt=today,
-        is_finished=False,
-        subject__default_location__in=get_filter_locations(user),
-        my_count=0)
-
-
-def waiting_for_appointment(visit):
-    required_types = visit.appointment_types.all()
-    appointment_types = []
-    for appointment in visit.appointment_set.all():
-        for type in appointment.appointment_types.all():
-            if (appointment.status in [Appointment.APPOINTMENT_STATUS_FINISHED,
-                                       Appointment.APPOINTMENT_STATUS_SCHEDULED]) and (not (type in appointment_types)):
-                appointment_types.append(type)
-    result = False
-    for type in required_types:
-        if not (type in appointment_types):
-            result = True
-    return result
-
-
-def get_unfinished_visits(user):
-    result = []
-    for visit in get_active_visits_without_appointments(user):
-        if not waiting_for_appointment(visit):
-            result.append(visit)
-    return result
-
-
-def get_active_visits_with_missing_appointments(user):
-    result = []
-    for visit in get_active_visits_without_appointments(user):
-        if waiting_for_appointment(visit):
-            result.append(visit)
-    return result
-
-
-def get_approaching_visits_without_appointments_count(user):
-    notification = NotificationCount(
-        title="approaching visits ",
-        count=get_approaching_visits_without_appointments(user).count(),
-        style="fa fa-users text-aqua",
-        type='web.views.approaching_visits_without_appointments')
-    return notification
-
-
-def get_approaching_visits_for_mail_contact_count(user):
-    notification = NotificationCount(
-        title="post mail for approaching visits",
-        count=get_approaching_visits_for_mail_contact(user).count(),
-        style="fa fa-users text-aqua",
-        type='web.views.approaching_visits_for_mail_contact')
-    return notification
-
-
-def get_approaching_visits_without_appointments(user):
-    today = get_today_midnight_date()
-    today_plus_two_months = today + datetime.timedelta(days=91)
-    return Visit.objects.annotate(
-        my_count=Count(Case(When(appointment__status=Appointment.APPOINTMENT_STATUS_SCHEDULED, then=1)))).filter(
-        datetime_begin__gt=today,
-        datetime_begin__lt=today_plus_two_months,
-        is_finished=False,
-        subject__default_location__in=get_filter_locations(user),
-        my_count=0)
-
-
-def get_approaching_visits_for_mail_contact(user):
-    today = get_today_midnight_date()
-    today_plus_three_months = today + datetime.timedelta(days=91)
-    today_plus_six_months = today + datetime.timedelta(days=183)
-    return Visit.objects.annotate(
-        my_count=Count(Case(When(appointment__status=Appointment.APPOINTMENT_STATUS_SCHEDULED, then=1)))).filter(
-        datetime_begin__gt=today_plus_three_months,
-        datetime_begin__lt=today_plus_six_months,
-        is_finished=False,
-        post_mail_sent=False,
-        subject__default_location__in=get_filter_locations(user),
-        my_count=0)
-
-
-def get_unfinished_appointments_count(user):
-    return NotificationCount(
-        title="unfinished appointments ",
-        count=get_unfinished_appointments(user).count(),
-        style="fa fa-history text-yellow",
-        type='web.views.unfinished_appointments')
-
-
-def get_unfinished_appointments(user):
-    return Appointment.objects.filter(
-        datetime_when__lt=get_today_midnight_date(),
-        status=Appointment.APPOINTMENT_STATUS_SCHEDULED,
-        location__in=get_filter_locations(user),
-    )
-
-
-def get_notifications(the_user):
-    workers = Worker.objects.filter(user=the_user)
-    notifications = []
-    count = 0
-    if len(workers) > 0:
-        person = workers[0]
-        if person.role == Worker.ROLE_CHOICES_SECRETARY:
-            notifications.append(get_exceeded_visit_notifications_count(person))
-            notifications.append(get_visits_without_appointments_count(person))
-            notifications.append(get_approaching_visits_without_appointments_count(person))
-            notifications.append(get_unfinished_appointments_count(person))
-            notifications.append(get_visits_with_missing_appointments_count(person))
-            notifications.append(get_subject_with_no_visit_notifications_count(person))
-            notifications.append(get_approaching_visits_for_mail_contact_count(person))
-            notifications.append(get_subjects_with_reminder_count(person))
-
-            for notification in notifications:
-                count += notification.count
-    return count, notifications
-
-
-"""
-Saturates response with information about logged user
-"""
-
-
-@login_required
-def wrap_response(request, template, params):
-    person, role = Worker.get_details(request.user)
-
-    notifications = get_notifications(request.user)
-
-    final_params = params.copy()
-    final_params.update({
-        'person': person,
-        'role': role,
-        'notifications': notifications
-    })
-
-    return render(request, template, final_params)
-
-
-def logout(request):
-    state, message = do_logout(request)
-    return redirect('/login?error=' + message)
-
-
-def visits(request):
-    visit_list = Visit.objects.order_by('-datetime_begin')
-    context = {
-        'visit_list': visit_list
-    }
-
-    return wrap_response(request, 'visits/index.html', context)
-
-
-def exceeded_visits(request):
-    context = {
-        'visit_list': get_exceeded_visits(request.user)
-    }
-    return wrap_response(request, 'visits/index.html', context)
-
-
-def unfinished_visits(request):
-    context = {
-        'visit_list': get_unfinished_visits(request.user)
-    }
-
-    return wrap_response(request, 'visits/index.html', context)
-
-
-def visits_with_missing_appointments(request):
-    context = {
-        'visit_list': get_active_visits_with_missing_appointments(request.user)
-    }
-
-    return wrap_response(request, 'visits/index.html', context)
-
-
-def approaching_visits_without_appointments(request):
-    context = {
-        'visit_list': get_approaching_visits_without_appointments(request.user)
-    }
-    return wrap_response(request, 'visits/index.html', context)
-
-
-def approaching_visits_for_mail_contact(request):
-    context = {
-        'visit_list': get_approaching_visits_for_mail_contact(request.user)
-    }
-    return wrap_response(request, 'visits/index.html', context)
-
-
-def visit_details(request, id):
-    displayed_visit = get_object_or_404(Visit, id=id)
-    if request.method == 'POST':
-        visit_form = VisitDetailForm(request.POST, request.FILES, instance=displayed_visit)
-        if visit_form.is_valid():
-            visit_form.save()
-    else:
-        visit_form = VisitDetailForm(instance=displayed_visit)
-
-    visit_finished = displayed_visit.is_finished
-    visit_id = displayed_visit.id
-    displayed_subject = displayed_visit.subject
-    list_of_appointments = displayed_visit.appointment_set.all()
-
-    can_finish = not waiting_for_appointment(displayed_visit)
-
-    for appointment in list_of_appointments:
-        if appointment.status == Appointment.APPOINTMENT_STATUS_SCHEDULED:
-            can_finish = False
-
-    subject_form = SubjectDetailForm(instance=displayed_subject)
-
-    return wrap_response(request, 'visits/details.html', {
-        'vform': visit_form,
-        'sform': subject_form,
-        'loApp': list_of_appointments,
-        'visFinished': visit_finished,
-        'canFinish': can_finish,
-        'vid': visit_id,
-        'visit': displayed_visit})
-
-
-def visit_mark(request, id, as_what):
-    visit = get_object_or_404(Visit, id=id)
-    if as_what == 'finished':
-        visit.mark_as_finished()
-
-    return redirect(visit_details, id=id)
-
-
-def visit_add(request, subject_id=-1):
-    if request.method == 'POST':
-        form = VisitAddForm(request.POST, request.FILES)
-        if form.is_valid():
-            visit = form.save()
-            return redirect(visit_details, visit.id)
-    else:
-        subjects = Subject.objects.filter(id=subject_id)
-        subject = None
-        if len(subjects) > 0:
-            subject = subjects[0]
-        form = VisitAddForm(initial={'subject': subject})
-
-    return wrap_response(request, 'visits/add.html', {'form': form})
-
-
-def subjects(request):
-    subjects_list = Subject.objects.order_by('-last_name')
-    context = {
-        'subjects_list': subjects_list
-    }
-
-    return wrap_response(request, 'subjects/index.html', context)
-
-
-def subject_add(request):
-    if request.method == 'POST':
-        form = SubjectAddForm(request.POST, request.FILES)
-        if form.is_valid():
-            screening_number = form.cleaned_data['screening_number']
-            if screening_number == '':
-                screening_number = get_new_screening_number()  # FIXME: method doesn't exist
-            form.save()
-            return redirect(subjects)
-    else:
-        form = SubjectAddForm()
-
-    return wrap_response(request, 'subjects/add.html', {'form': form})
-
-
-def subject_no_visits(request):
-    subjects_list = get_subjects_with_no_visit(request.user).order_by('-last_name')
-    context = {
-        'subjects_list': subjects_list
-    }
-
-    return wrap_response(request, 'subjects/index.html', context)
-
-
-def subject_require_contact(request):
-    subjects_list = get_subjects_with_reminder(request.user).order_by('-last_name')
-    context = {
-        'subjects_list': subjects_list
-    }
-
-    return wrap_response(request, 'subjects/index.html', context)
-
-
-def subject_edit(request, id):
-    the_subject = get_object_or_404(Subject, id=id)
-    if request.method == 'POST':
-        form = SubjectEditForm(request.POST, request.FILES, instance=the_subject)
-        if form.is_valid():
-            form.save()
-            return redirect(subjects)
-    else:
-        form = SubjectEditForm(instance=the_subject)
-    return wrap_response(request, 'subjects/edit.html', {
-        'form': form,
-        'subject': the_subject
-    })
-
-
-def subject_delete(request, id):
-    the_subject = get_object_or_404(Subject, id=id)
-    if request.method == 'POST':
-        the_subject.delete()
-        return redirect(subjects)
-    else:
-        form = SubjectEditForm(instance=the_subject)
-    return wrap_response(request, 'subjects/delete.html', {'form': form})
-
-
-def subject_mark(request, id, as_what):
-    who = get_object_or_404(Subject, id=id)
-    if as_what == 'dead':
-        who.mark_as_dead()
-    elif as_what == 'rejected':
-        who.mark_as_rejected()
-    return redirect(subject_edit, id=id)
-
-
-def appointment_mark(request, id, as_what):
-    appointment = get_object_or_404(Appointment, id=id)
-    if as_what == 'finished':
-        appointment.mark_as_finished()
-    elif as_what == 'cancelled':
-        appointment.mark_as_cancelled()
-    elif as_what == 'no_show':
-        appointment.mark_as_no_show()
-    else:
-        return e500_error(request)
-    return redirect(visit_details, id=appointment.visit.id)
-
-
-def subject_visit_details(request, id):
-    locsubject = get_object_or_404(Subject, id=id)
-    visits = locsubject.visit_set.all()
-    endlist = []
-    for vis in visits:
-        assign = vis.appointment_set.all()
-        finished = vis.is_finished
-        visid = vis.id
-        visit_title = vis.follow_up_title()
-        visform = VisitDetailForm(instance=vis)
-        endlist.append((visform, assign, finished, visid, visit_title))
-
-    return wrap_response(request, 'subjects/visitdetails.html', {'display': endlist, "id": id})
-
-
-def doctors(request):
-    doctors_list = Worker.objects.order_by('-last_name')
-    context = {
-        'doctors_list': doctors_list
-    }
-
-    return wrap_response(request, "doctors/index.html", context)
-
-
-def doctor_add(request):
-    if request.method == 'POST':
-        form = WorkerAddForm(request.POST, request.FILES)
-        if form.is_valid():
-            form.save()
-            return redirect(doctors)
-    else:
-        form = WorkerAddForm()
-
-    return wrap_response(request, 'doctors/add.html', {'form': form})
-
-
-def doctor_edit(request, doctor_id):
-    the_doctor = get_object_or_404(Worker, id=doctor_id)
-    if request.method == 'POST':
-        form = WorkerEditForm(request.POST, request.FILES, instance=the_doctor)
-        if form.is_valid():
-            form.save()
-            return redirect(doctors)
-    else:
-        form = WorkerEditForm(instance=the_doctor)
-    return wrap_response(request, 'doctors/edit.html', {'form': form})
-
-
-def doctor_details(request, doctor_id):
-    the_doctor = get_object_or_404(Worker, id=doctor_id)
-    form = WorkerDetailForm(instance=the_doctor)
-
-    return wrap_response(request, 'doctors/details.html', {'form': form})
-
-
-def doctor_availability(request, doctor_id):
-    avall = Avaibility.objects.filter(person=doctor_id)
-
-    avmon = avall.filter(day_number=1)
-    avtue = avall.filter(day_number=2)
-    avwed = avall.filter(day_number=3)
-    avthu = avall.filter(day_number=4)
-    avfri = avall.filter(day_number=5)
-    avsat = avall.filter(day_number=6)
-    avsun = avall.filter(day_number=7)
-
-    context = {
-        'avmon': avmon,
-        'avtue': avtue,
-        'avwed': avwed,
-        'avthu': avthu,
-        'avfri': avfri,
-        'avsat': avsat,
-        'avsun': avsun,
-        'id': doctor_id
-    }
-
-    return wrap_response(request, "doctors/availability_index.html", context)
-
-
-def doctor_availability_delete(request, doctor_id, availability_id):
-    availibility = Avaibility.objects.filter(id=availability_id)
-    if len(availibility) > 0:
-        availibility.delete()
-    return redirect(doctoravail, id=doctor_id)  # FIXME doctoravail doesn't exist
-
-
-def equipment_def(request):
-    equipment_list = Item.objects.order_by('-name')
-    context = {
-        'equipment_list': equipment_list
-    }
-
-    return wrap_response(request, "eqdef/index.html", context)
-
-
-def equipment_and_rooms(request):
-    return wrap_response(request, "equipment_and_rooms/index.html", {})
-
-
-def mail_templates(request):
-    return wrap_response(request, "mail_templates/index.html", {})
-
-
-"""
-# An initial draft of a function that was supposed to suggest date, room and worker for an appointment
-def suggest_details(appoint):
-    avaibleWorkers = Worker.objects.get()
-    if appoint.appointment_type__required_worker == 'DOCTOR':
-        avaibleWorkers.filter(role='DOCTOR')
-    elif appoint.appointment_type__required_worker == 'NURSE':
-        avaibleWorkers.filter(role__in=['DOCTOR','NURSE'])
-    elif appoint.appointment_type__required_worker == 'PSYCHOLOGIST':
-        avaibleWorkers.filter(role__in=['DOCTOR','PSYCHOLOGIST'])
-    avaibleRooms = Room.objects.get
-    requireditems = appoint.appointment_type.required_equipment.filter(is_fixed=True)
-    reduce(operator.and_, (Q(equipment__contains=requireditems) for x in avaibleRooms))
-"""
-
-
-def get_today_midnight_date():
-    today = datetime.datetime.now()
-    today_midnight = datetime.datetime(today.year, today.month, today.day)
-    return today_midnight
-
-
-def get_calendar_full_appointments(user):
-    month_ago = get_today_midnight_date() + datetime.timedelta(days=-31)
-    return Appointment.objects.filter(
-        datetime_when__gt=month_ago,
-        location__in=get_filter_locations(user),
-    ).order_by('datetime_when')
-
-
-def appointments(request):
-    approaching_list = Appointment.objects.filter(
-        datetime_when__gt=get_today_midnight_date(),
-        location__in=get_filter_locations(request.user),
-        status=Appointment.APPOINTMENT_STATUS_SCHEDULED
-    ).order_by('datetime_when')
-
-    full_list = get_calendar_full_appointments(request.user)
-
-    context = {
-        'approaching_list': approaching_list,
-        'full_list': full_list
-    }
-
-    return wrap_response(request, "appointments/index.html", context)
-
-
-def unfinished_appointments(request):
-    appointments = get_unfinished_appointments(request.user)
-    context = {
-        'appointment_list': appointments,
-    }
-
-    return wrap_response(request, "appointments/list.html", context)
-
-
-def appointment_details(request, id):
-    the_appointment = get_object_or_404(Appointment, id=id)
-    form = AppointmentDetailForm(instance=the_appointment)
-    return wrap_response(request, 'appointments/details.html', {'form': form})
-
-
-def appointment_add(request, id):
-    full_list = get_calendar_full_appointments(request.user)
-    if request.method == 'POST':
-        form = AppointmentAddForm(request.POST, request.FILES, user=request.user)
-        form.fields['visit'].widget = HiddenInput()
-        if form.is_valid():
-            form.save()
-            return redirect(visit_details, id=id)
-    else:
-        form = AppointmentAddForm(initial={'visit': id}, user=request.user)
-        form.fields['visit'].widget = HiddenInput()
-
-    return wrap_response(request, 'appointments/add.html',
-                         {'form': form, 'visitID': id, 'full_appointment_list': full_list})
-
-
-def appointment_edit(request, id):
-    the_appointment = get_object_or_404(Appointment, id=id)
-    if request.method == 'POST':
-        appointment_form = AppointmentEditForm(request.POST,
-                                               request.FILES,
-                                               instance=the_appointment,
-                                               user=request.user,
-                                               prefix="appointment")
-        subject_form = SubjectEditForm(request.POST, instance=the_appointment.visit.subject, prefix="subject")
-
-        if appointment_form.is_valid() and subject_form.is_valid():
-            appointment_form.save()
-            subject_form.save()
-            return redirect(appointments)
-    else:
-        appointment_form = AppointmentEditForm(instance=the_appointment, user=request.user, prefix="appointment")
-
-        subject_form = SubjectEditForm(instance=the_appointment.visit.subject, prefix="subject")
-
-    return wrap_response(request, 'appointments/edit.html', {
-        'form': appointment_form,
-        'subject_form': subject_form,
-        'id': id,
-        'appointment': the_appointment
-    })
-
-
-def appointment_edit_datetime(request, id):
-    the_appointment = get_object_or_404(Appointment, id=id)
-    if request.method == 'POST':
-        form = AppointmentEditForm(request.POST, request.FILES, instance=the_appointment, user=request.user)
-        if form.is_valid():
-            form.save()
-            return redirect(appointments)
-    else:
-        the_appointment.datetime_when = the_appointment.visit.datetime_begin
-        form = AppointmentEditForm(instance=the_appointment, user=request.user)
-    return wrap_response(request, 'appointments/edit.html', {'form': form})
-
-
-# because we don't  wrap_response we must force login required
-@login_required
-def export_to_csv2(request, type="subjects"):
-    # Create the HttpResponse object with the appropriate CSV header.
-    response = HttpResponse(content_type='text/csv')
-    response['Content-Disposition'] = 'attachment; filename="' + type + '-' + get_today_midnight_date().strftime(
-        "%Y-%m-%d") + '.csv"'
-
-    writer = csv.writer(response, quotechar=str(u'"'), quoting=csv.QUOTE_ALL)
-    if type == "subjects":
-        write_subjects_to_csv(writer)
-    elif type == "appointments":
-        write_appointments_to_csv(writer)
-    else:
-        return e500_error(request)
-    return response
-
-
-def write_subjects_to_csv(writer):
-    subject_fields = []
-    for field in Subject._meta.fields:
-        if field.name != "ID":
-            subject_fields.append(field)
-
-    field_names = []
-    for field in subject_fields:
-        field_names.append(field.verbose_name)
-
-    writer.writerow(field_names)
-
-    subjects = Subject.objects.order_by('-last_name')
-    for subject in subjects:
-        row = []
-        for field in subject_fields:
-            row.append(getattr(subject, field.name))
-        writer.writerow([unicode(s).replace("\n", ";").replace("\r", ";").encode("utf-8") for s in row])
-
-
-def write_appointments_to_csv(writer):
-    appointments_fields = []
-    for field in Appointment._meta.fields:
-        if field.name != "visit" and field.name != "id" and field.name != "worker_assigned" and field.name != "appointment_types" and field.name != "room" and field.name != "flying_team":
-            appointments_fields.append(field)
-
-    field_names = ['ND number', 'Family name', 'Name', 'Visit']
-    for field in appointments_fields:
-        field_names.append(field.verbose_name)
-
-    writer.writerow(field_names)
-
-    appointments = Appointment.objects.order_by('-datetime_when')
-
-    for appointment in appointments:
-        row = [appointment.visit.subject.nd_number, appointment.visit.subject.last_name,
-               appointment.visit.subject.first_name, appointment.visit.follow_up_title()]
-        for field in appointments_fields:
-            row.append(getattr(appointment, field.name))
-        type_string = ""
-        for type in appointment.appointment_types.all():
-            type_string += type.code + ","
-        row.append(type_string)
-        writer.writerow([unicode(s).replace("\n", ";").replace("\r", ";").encode("utf-8") for s in row])
-
-
-def export(request):
-    return wrap_response(request, 'export/index.html', {})
-
-
-def get_kit_requests(user, start_date=None, end_date=None):
-    if start_date is None:
-        start_date = get_today_midnight_date() + datetime.timedelta(days=1)
-        end_date = start_date + datetime.timedelta(days=7)
-    else:
-        if isinstance(start_date, str):
-            start_date = parse_datetime(start_date)
-        if (end_date is not None) and (isinstance(end_date, str)):
-            end_date = parse_datetime(end_date)
-
-    appointment_types = AppointmentType.objects.filter(required_equipment__disposable=True)
-
-    appointments = Appointment.objects.filter(
-        appointment_types__in=appointment_types,
-        datetime_when__gt=start_date,
-        location__in=get_filter_locations(user),
-        status=Appointment.APPOINTMENT_STATUS_SCHEDULED,
-    )
-    if end_date is not None:
-        appointments = appointments.filter(datetime_when__lt=end_date)
-
-    result = {
-        'start_date': start_date,
-        'end_date': end_date,
-        'appointments': appointments,
-    }
-    return result
-
-
-def get_kit_requests_data(request, start_date=None, end_date=None):
-    form = KitRequestForm()
-    if request.method == 'POST':
-        form = KitRequestForm(request.POST)
-        if form.is_valid():
-            form_data = form.cleaned_data
-            start_date = form_data.get('start_date')
-            end_date = form_data.get('end_date')
-
-    params = get_kit_requests(request.user, start_date, end_date)
-    params.update({
-        'form': form
-    })
-    return params
-
-
-def kit_requests(request):
-    return wrap_response(request, 'equipment_and_rooms/kit_requests.html', get_kit_requests_data(request))
-
-
-def kit_requests_send_mail(request, start_date, end_date=None):
-    return wrap_response(request, 'equipment_and_rooms/kit_requests_send_mail.html',
-                         get_kit_requests_data(request, start_date, end_date))
-
-
-def statistics(request):
-    statistics_manager = StatisticsManager()
-    visit_choices = [("-1", "all")]
-    visit_choices.extend([(rank, rank) for rank in statistics_manager.visits_ranks])
-    year_previous_month, previous_month = get_previous_year_and_month()
-
-    form = StatisticsForm(request.GET, visit_choices=visit_choices, month=previous_month, year=year_previous_month)
-    if not form.is_valid():
-        form.is_bound = False
-    month = form.data.get('month', previous_month)
-    year = form.data.get('year', year_previous_month)
-    subject_type = form.data.get('subject_type', "-1")
-    visit = form.data.get('visit', "-1")
-    if subject_type == "-1":
-        subject_type = None
-    if visit == "-1":
-        visit = None
-    monthly_statistics = statistics_manager.get_statistics_for_month(month, year, subject_type, visit)
-    return wrap_response(request, 'statistics/index.html', {
-        'form': form,
-        'monthly_statistics': monthly_statistics
-    })
diff --git a/smash/web/views/__init__.py b/smash/web/views/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..12bf76cd82f60c4af59f4559463487ae28a0b82a
--- /dev/null
+++ b/smash/web/views/__init__.py
@@ -0,0 +1,63 @@
+# coding=utf-8
+
+from django.contrib.auth.decorators import login_required
+from django.shortcuts import redirect, render
+
+from notifications import get_notifications
+from ..models import Worker
+
+__author__ = 'Valentin Grouès'
+handler404 = 'web.views.e404_page_not_found'
+handler500 = 'web.views.e500_error'
+handler403 = 'web.views.e403_permission_denied'
+handler400 = 'web.views.e400_bad_request'
+
+
+def index(request):
+    if request.user.is_authenticated():
+        return redirect('web.views.appointments')
+    return redirect('web.views.login')
+
+
+def e404_page_not_found(request, context=None):
+    return render(request, "errors/404.html", context, status=404)
+
+
+def e500_error(request, context=None):
+    return render(request, "errors/500.html", context, status=500)
+
+
+def e403_permission_denied(request, context=None):
+    return render(request, "errors/403.html", context, status=403)
+
+
+def e400_bad_request(request, context=None):
+    return render(request, "errors/400.html", context, status=400)
+
+
+@login_required
+def wrap_response(request, template, params):
+    person, role = Worker.get_details(request.user)
+
+    notifications = get_notifications(request.user)
+
+    final_params = params.copy()
+    final_params.update({
+        'person': person,
+        'role': role,
+        'notifications': notifications
+    })
+
+    return render(request, template, final_params)
+
+
+import auth
+import appointment
+import visit
+import doctor
+import subject
+import equipment
+import kit
+import mails
+import statistics
+import export
diff --git a/smash/web/views/appointment.py b/smash/web/views/appointment.py
new file mode 100644
index 0000000000000000000000000000000000000000..471f33c293d3feb5a39e46127f18d9e6de7d946d
--- /dev/null
+++ b/smash/web/views/appointment.py
@@ -0,0 +1,112 @@
+# coding=utf-8
+from django.forms import HiddenInput
+from django.shortcuts import get_object_or_404, redirect
+
+from notifications import get_today_midnight_date, get_filter_locations, get_calendar_full_appointments, \
+    get_unfinished_appointments
+from . import e500_error, wrap_response
+from ..forms import AppointmentDetailForm, AppointmentAddForm, AppointmentEditForm, SubjectEditForm
+from ..models import Appointment
+
+__author__ = 'Valentin Grouès'
+
+
+def appointment_mark(request, id, as_what):
+    appointment = get_object_or_404(Appointment, id=id)
+    if as_what == 'finished':
+        appointment.mark_as_finished()
+    elif as_what == 'cancelled':
+        appointment.mark_as_cancelled()
+    elif as_what == 'no_show':
+        appointment.mark_as_no_show()
+    else:
+        return e500_error(request)
+    return redirect('web.views.visit_details', id=appointment.visit.id)
+
+
+def appointments(request):
+    approaching_list = Appointment.objects.filter(
+        datetime_when__gt=get_today_midnight_date(),
+        location__in=get_filter_locations(request.user),
+        status=Appointment.APPOINTMENT_STATUS_SCHEDULED
+    ).order_by('datetime_when')
+
+    full_list = get_calendar_full_appointments(request.user)
+
+    context = {
+        'approaching_list': approaching_list,
+        'full_list': full_list
+    }
+
+    return wrap_response(request, "appointments/index.html", context)
+
+
+def unfinished_appointments(request):
+    appointments = get_unfinished_appointments(request.user)
+    context = {
+        'appointment_list': appointments,
+    }
+
+    return wrap_response(request, "appointments/list.html", context)
+
+
+def appointment_details(request, id):
+    the_appointment = get_object_or_404(Appointment, id=id)
+    form = AppointmentDetailForm(instance=the_appointment)
+    return wrap_response(request, 'appointments/details.html', {'form': form})
+
+
+def appointment_add(request, id):
+    full_list = get_calendar_full_appointments(request.user)
+    if request.method == 'POST':
+        form = AppointmentAddForm(request.POST, request.FILES, user=request.user)
+        form.fields['visit'].widget = HiddenInput()
+        if form.is_valid():
+            form.save()
+            return redirect('web.views.visit_details', id=id)
+    else:
+        form = AppointmentAddForm(initial={'visit': id}, user=request.user)
+        form.fields['visit'].widget = HiddenInput()
+
+    return wrap_response(request, 'appointments/add.html',
+                         {'form': form, 'visitID': id, 'full_appointment_list': full_list})
+
+
+def appointment_edit(request, id):
+    the_appointment = get_object_or_404(Appointment, id=id)
+    if request.method == 'POST':
+        appointment_form = AppointmentEditForm(request.POST,
+                                               request.FILES,
+                                               instance=the_appointment,
+                                               user=request.user,
+                                               prefix="appointment")
+        subject_form = SubjectEditForm(request.POST, instance=the_appointment.visit.subject, prefix="subject")
+
+        if appointment_form.is_valid() and subject_form.is_valid():
+            appointment_form.save()
+            subject_form.save()
+            return redirect('web.views.appointments')
+    else:
+        appointment_form = AppointmentEditForm(instance=the_appointment, user=request.user, prefix="appointment")
+
+        subject_form = SubjectEditForm(instance=the_appointment.visit.subject, prefix="subject")
+
+    return wrap_response(request, 'appointments/edit.html', {
+        'form': appointment_form,
+        'subject_form': subject_form,
+        'id': id,
+        'appointment': the_appointment
+    })
+
+
+def appointment_edit_datetime(request, id):
+    the_appointment = get_object_or_404(Appointment, id=id)
+    if request.method == 'POST':
+        form = AppointmentEditForm(request.POST, request.FILES, instance=the_appointment, user=request.user)
+        if form.is_valid():
+            form.save()
+            return redirect('web.views.appointments')
+    else:
+        the_appointment.datetime_when = the_appointment.visit.datetime_begin
+        form = AppointmentEditForm(instance=the_appointment, user=request.user)
+    return wrap_response(request, 'appointments/edit.html', {'form': form})
diff --git a/smash/web/views/auth.py b/smash/web/views/auth.py
new file mode 100644
index 0000000000000000000000000000000000000000..9ff4730d8e5030c28a1e7fe4159e6c981f93d168
--- /dev/null
+++ b/smash/web/views/auth.py
@@ -0,0 +1,39 @@
+# coding=utf-8
+from django.shortcuts import redirect, render
+
+from ..auth import do_login, do_logout
+
+__author__ = 'Valentin Grouès'
+
+
+def login(request):
+    context = {
+        'state': 'initial'
+    }
+    if request.GET and request.GET.get('error'):
+        context['state'] = request.GET.get('error')
+
+    if request.method == "GET" and request.GET:
+        context['next'] = request.GET.get('next')
+
+    if request.method == "POST" and request.POST:
+        state, message = do_login(request)
+        if state:
+            if request.POST.get('next'):
+                return redirect(request.POST.get('next'))
+                # FIXME: risk of phishing attacks
+                #  see https://www.owasp.org/index.php/Unvalidated_Redirects_and_Forwards_Cheat_Sheet
+            else:
+                return redirect('web.views.appointments')
+        else:
+            response = redirect('web.views.login')
+            response['Location'] += "?error={}".format(message)
+            return response
+    return render(request, "login.html", context)
+
+
+def logout(request):
+    state, message = do_logout(request)
+    response = redirect('web.views.login')
+    response['Location'] += "?error={}".format(message)
+    return response
diff --git a/smash/web/views/doctor.py b/smash/web/views/doctor.py
new file mode 100644
index 0000000000000000000000000000000000000000..a59e322198ccf22db36e2463534dbf9b7445cb13
--- /dev/null
+++ b/smash/web/views/doctor.py
@@ -0,0 +1,80 @@
+# coding=utf-8
+from django.shortcuts import redirect, get_object_or_404
+
+from . import wrap_response
+from ..forms import WorkerAddForm, WorkerEditForm, WorkerDetailForm
+from ..models import Worker, Avaibility
+
+__author__ = 'Valentin Grouès'
+
+
+def doctors(request):
+    doctors_list = Worker.objects.order_by('-last_name')
+    context = {
+        'doctors_list': doctors_list
+    }
+
+    return wrap_response(request, "doctors/index.html", context)
+
+
+def doctor_add(request):
+    if request.method == 'POST':
+        form = WorkerAddForm(request.POST, request.FILES)
+        if form.is_valid():
+            form.save()
+            return redirect('web.views.doctors')
+    else:
+        form = WorkerAddForm()
+
+    return wrap_response(request, 'doctors/add.html', {'form': form})
+
+
+def doctor_edit(request, doctor_id):
+    the_doctor = get_object_or_404(Worker, id=doctor_id)
+    if request.method == 'POST':
+        form = WorkerEditForm(request.POST, request.FILES, instance=the_doctor)
+        if form.is_valid():
+            form.save()
+            return redirect('web.views.doctors')
+    else:
+        form = WorkerEditForm(instance=the_doctor)
+    return wrap_response(request, 'doctors/edit.html', {'form': form})
+
+
+def doctor_details(request, doctor_id):
+    the_doctor = get_object_or_404(Worker, id=doctor_id)
+    form = WorkerDetailForm(instance=the_doctor)
+
+    return wrap_response(request, 'doctors/details.html', {'form': form})
+
+
+def doctor_availability(request, doctor_id):
+    avall = Avaibility.objects.filter(person=doctor_id)
+
+    avmon = avall.filter(day_number=1)
+    avtue = avall.filter(day_number=2)
+    avwed = avall.filter(day_number=3)
+    avthu = avall.filter(day_number=4)
+    avfri = avall.filter(day_number=5)
+    avsat = avall.filter(day_number=6)
+    avsun = avall.filter(day_number=7)
+
+    context = {
+        'avmon': avmon,
+        'avtue': avtue,
+        'avwed': avwed,
+        'avthu': avthu,
+        'avfri': avfri,
+        'avsat': avsat,
+        'avsun': avsun,
+        'id': doctor_id
+    }
+
+    return wrap_response(request, "doctors/availability_index.html", context)
+
+
+def doctor_availability_delete(request, doctor_id, availability_id):
+    availibility = Avaibility.objects.filter(id=availability_id)
+    if len(availibility) > 0:
+        availibility.delete()
+    return redirect(doctoravail, id=doctor_id)  # FIXME doctoravail doesn't exist
diff --git a/smash/web/views/equipment.py b/smash/web/views/equipment.py
new file mode 100644
index 0000000000000000000000000000000000000000..417b1f7da97263985fee54c31aad127abcadb391
--- /dev/null
+++ b/smash/web/views/equipment.py
@@ -0,0 +1,18 @@
+# coding=utf-8
+from . import wrap_response
+from ..models import Item
+
+__author__ = 'Valentin Grouès'
+
+
+def equipment_def(request):
+    equipment_list = Item.objects.order_by('-name')
+    context = {
+        'equipment_list': equipment_list
+    }
+
+    return wrap_response(request, "eqdef/index.html", context)
+
+
+def equipment_and_rooms(request):
+    return wrap_response(request, "equipment_and_rooms/index.html", {})
diff --git a/smash/web/views/export.py b/smash/web/views/export.py
new file mode 100644
index 0000000000000000000000000000000000000000..8d5182b027ce6e08abfd72ce4390a9a1bd2bfcec
--- /dev/null
+++ b/smash/web/views/export.py
@@ -0,0 +1,78 @@
+# coding=utf-8
+import csv
+
+from django.contrib.auth.decorators import login_required
+from django.http import HttpResponse
+
+from notifications import get_today_midnight_date
+from . import e500_error, wrap_response
+from ..models import Subject, Appointment
+
+__author__ = 'Valentin Grouès'
+
+
+@login_required
+def export_to_csv2(request, type="subjects"):
+    # Create the HttpResponse object with the appropriate CSV header.
+    response = HttpResponse(content_type='text/csv')
+    response['Content-Disposition'] = 'attachment; filename="' + type + '-' + get_today_midnight_date().strftime(
+        "%Y-%m-%d") + '.csv"'
+
+    writer = csv.writer(response, quotechar=str(u'"'), quoting=csv.QUOTE_ALL)
+    if type == "subjects":
+        write_subjects_to_csv(writer)
+    elif type == "appointments":
+        write_appointments_to_csv(writer)
+    else:
+        return e500_error(request)
+    return response
+
+
+def write_subjects_to_csv(writer):
+    subject_fields = []
+    for field in Subject._meta.fields:
+        if field.name != "ID":
+            subject_fields.append(field)
+
+    field_names = []
+    for field in subject_fields:
+        field_names.append(field.verbose_name)
+
+    writer.writerow(field_names)
+
+    subjects = Subject.objects.order_by('-last_name')
+    for subject in subjects:
+        row = []
+        for field in subject_fields:
+            row.append(getattr(subject, field.name))
+        writer.writerow([unicode(s).replace("\n", ";").replace("\r", ";").encode("utf-8") for s in row])
+
+
+def write_appointments_to_csv(writer):
+    appointments_fields = []
+    for field in Appointment._meta.fields:
+        if field.name != "visit" and field.name != "id" and field.name != "worker_assigned" and field.name != "appointment_types" and field.name != "room" and field.name != "flying_team":
+            appointments_fields.append(field)
+
+    field_names = ['ND number', 'Family name', 'Name', 'Visit']
+    for field in appointments_fields:
+        field_names.append(field.verbose_name)
+
+    writer.writerow(field_names)
+
+    appointments = Appointment.objects.order_by('-datetime_when')
+
+    for appointment in appointments:
+        row = [appointment.visit.subject.nd_number, appointment.visit.subject.last_name,
+               appointment.visit.subject.first_name, appointment.visit.follow_up_title()]
+        for field in appointments_fields:
+            row.append(getattr(appointment, field.name))
+        type_string = ""
+        for type in appointment.appointment_types.all():
+            type_string += type.code + ","
+        row.append(type_string)
+        writer.writerow([unicode(s).replace("\n", ";").replace("\r", ";").encode("utf-8") for s in row])
+
+
+def export(request):
+    return wrap_response(request, 'export/index.html', {})
diff --git a/smash/web/views/kit.py b/smash/web/views/kit.py
new file mode 100644
index 0000000000000000000000000000000000000000..36d8e440054732ef5be864f3fe13fc050ff23f4c
--- /dev/null
+++ b/smash/web/views/kit.py
@@ -0,0 +1,65 @@
+# coding=utf-8
+import datetime
+
+from django.utils.dateparse import parse_datetime
+
+from notifications import get_filter_locations, get_today_midnight_date
+from . import wrap_response
+from ..forms import KitRequestForm
+from ..models import AppointmentType, Appointment
+
+__author__ = 'Valentin Grouès'
+
+
+def get_kit_requests(user, start_date=None, end_date=None):
+    if start_date is None:
+        start_date = get_today_midnight_date() + datetime.timedelta(days=1)
+        end_date = start_date + datetime.timedelta(days=7)
+    else:
+        if isinstance(start_date, str):
+            start_date = parse_datetime(start_date)
+        if (end_date is not None) and (isinstance(end_date, str)):
+            end_date = parse_datetime(end_date)
+
+    appointment_types = AppointmentType.objects.filter(required_equipment__disposable=True)
+
+    appointments = Appointment.objects.filter(
+        appointment_types__in=appointment_types,
+        datetime_when__gt=start_date,
+        location__in=get_filter_locations(user),
+        status=Appointment.APPOINTMENT_STATUS_SCHEDULED,
+    )
+    if end_date is not None:
+        appointments = appointments.filter(datetime_when__lt=end_date)
+
+    result = {
+        'start_date': start_date,
+        'end_date': end_date,
+        'appointments': appointments,
+    }
+    return result
+
+
+def get_kit_requests_data(request, start_date=None, end_date=None):
+    form = KitRequestForm()
+    if request.method == 'POST':
+        form = KitRequestForm(request.POST)
+        if form.is_valid():
+            form_data = form.cleaned_data
+            start_date = form_data.get('start_date')
+            end_date = form_data.get('end_date')
+
+    params = get_kit_requests(request.user, start_date, end_date)
+    params.update({
+        'form': form
+    })
+    return params
+
+
+def kit_requests(request):
+    return wrap_response(request, 'equipment_and_rooms/kit_requests.html', get_kit_requests_data(request))
+
+
+def kit_requests_send_mail(request, start_date, end_date=None):
+    return wrap_response(request, 'equipment_and_rooms/kit_requests_send_mail.html',
+                         get_kit_requests_data(request, start_date, end_date))
diff --git a/smash/web/views/mails.py b/smash/web/views/mails.py
new file mode 100644
index 0000000000000000000000000000000000000000..e949129f6cae5b3059ffb2cb0a5edf1dc2ac917c
--- /dev/null
+++ b/smash/web/views/mails.py
@@ -0,0 +1,8 @@
+# coding=utf-8
+from . import wrap_response
+
+__author__ = 'Valentin Grouès'
+
+
+def mail_templates(request):
+    return wrap_response(request, "mail_templates/index.html", {})
diff --git a/smash/web/views/notifications.py b/smash/web/views/notifications.py
new file mode 100644
index 0000000000000000000000000000000000000000..b3b5d5ae97b2bfec6350ca819be5644792f1cb55
--- /dev/null
+++ b/smash/web/views/notifications.py
@@ -0,0 +1,256 @@
+# coding=utf-8
+import datetime
+
+from django.contrib.auth.models import User, AnonymousUser
+from django.db.models import Count, Case, When
+
+from ..models import Worker, Subject, Visit, Appointment, Location
+
+__author__ = 'Valentin Grouès'
+
+
+class NotificationCount(object):
+    title = ""
+    count = 0
+    style = ""
+    type = ''
+
+    def __init__(self, title="Unknown", count=0, style="fa fa-users text-aqua", type='web.views.appointments'):
+        self.title = title
+        self.count = count
+        self.style = style
+        self.type = type
+
+
+def get_exceeded_visit_notifications_count(user):
+    notification = NotificationCount(
+        title="exceeded visit time",
+        count=get_exceeded_visits(user).count(),
+        style="fa fa-thermometer-4 text-red",
+        type='web.views.exceeded_visits')
+    return notification
+
+
+def get_subjects_with_reminder_count(user):
+    notification = NotificationCount(
+        title="subject required contact",
+        count=get_subjects_with_reminder(user).count(),
+        style="fa fa-users text-aqua",
+        type='web.views.subject_require_contact')
+    return notification
+
+
+def get_subject_with_no_visit_notifications_count(user):
+    notification = NotificationCount(
+        title="subject without visit",
+        count=get_subjects_with_no_visit(user).count(),
+        style="fa fa-users text-aqua",
+        type='web.views.subject_no_visits')
+    return notification
+
+
+def get_visits_without_appointments_count(user):
+    notification = NotificationCount(
+        title="unfinished visits ",
+        count=len(get_unfinished_visits(user)),
+        style="fa fa-user-times text-yellow",
+        type='web.views.unfinished_visits')
+    return notification
+
+
+def get_visits_with_missing_appointments_count(user):
+    notification = NotificationCount(
+        title="visits with missing appointments",
+        count=len(get_active_visits_with_missing_appointments(user)),
+        style="fa fa-user-times text-yellow",
+        type='web.views.visits_with_missing_appointments')
+    return notification
+
+
+def get_approaching_visits_without_appointments_count(user):
+    notification = NotificationCount(
+        title="approaching visits ",
+        count=get_approaching_visits_without_appointments(user).count(),
+        style="fa fa-users text-aqua",
+        type='web.views.approaching_visits_without_appointments')
+    return notification
+
+
+def get_approaching_visits_for_mail_contact_count(user):
+    notification = NotificationCount(
+        title="post mail for approaching visits",
+        count=get_approaching_visits_for_mail_contact(user).count(),
+        style="fa fa-users text-aqua",
+        type='web.views.approaching_visits_for_mail_contact')
+    return notification
+
+
+def get_unfinished_appointments_count(user):
+    return NotificationCount(
+        title="unfinished appointments ",
+        count=get_unfinished_appointments(user).count(),
+        style="fa fa-history text-yellow",
+        type='web.views.unfinished_appointments')
+
+
+def get_notifications(the_user):
+    workers = Worker.objects.filter(user=the_user)
+    notifications = []
+    count = 0
+    if len(workers) > 0:
+        person = workers[0]
+        if person.role == Worker.ROLE_CHOICES_SECRETARY:
+            notifications.append(get_exceeded_visit_notifications_count(person))
+            notifications.append(get_visits_without_appointments_count(person))
+            notifications.append(get_approaching_visits_without_appointments_count(person))
+            notifications.append(get_unfinished_appointments_count(person))
+            notifications.append(get_visits_with_missing_appointments_count(person))
+            notifications.append(get_subject_with_no_visit_notifications_count(person))
+            notifications.append(get_approaching_visits_for_mail_contact_count(person))
+            notifications.append(get_subjects_with_reminder_count(person))
+
+            for notification in notifications:
+                count += notification.count
+    return count, notifications
+
+
+def get_subjects_with_no_visit(user):
+    result = Subject.objects.annotate(my_count=Count(Case(When(visit__is_finished=False, then=1)))).filter(
+        dead=False,
+        resigned=False,
+        my_count=0,
+        default_location__in=get_filter_locations(user),
+        postponed=False,
+        datetime_contact_reminder__isnull=True,
+    )
+    return result
+
+
+def get_subjects_with_reminder(user):
+    tomorrow = get_today_midnight_date() + datetime.timedelta(days=1)
+
+    result = Subject.objects.filter(
+        dead=False,
+        resigned=False,
+        default_location__in=get_filter_locations(user),
+        datetime_contact_reminder__lt=tomorrow,
+    )
+    return result
+
+
+def get_active_visits_with_missing_appointments(user):
+    result = []
+    for visit in get_active_visits_without_appointments(user):
+        if waiting_for_appointment(visit):
+            result.append(visit)
+    return result
+
+
+def get_unfinished_visits(user):
+    result = []
+    for visit in get_active_visits_without_appointments(user):
+        if not waiting_for_appointment(visit):
+            result.append(visit)
+    return result
+
+
+def get_approaching_visits_without_appointments(user):
+    today = get_today_midnight_date()
+    today_plus_two_months = today + datetime.timedelta(days=91)
+    return Visit.objects.annotate(
+        my_count=Count(Case(When(appointment__status=Appointment.APPOINTMENT_STATUS_SCHEDULED, then=1)))).filter(
+        datetime_begin__gt=today,
+        datetime_begin__lt=today_plus_two_months,
+        is_finished=False,
+        subject__default_location__in=get_filter_locations(user),
+        my_count=0)
+
+
+def get_approaching_visits_for_mail_contact(user):
+    today = get_today_midnight_date()
+    today_plus_three_months = today + datetime.timedelta(days=91)
+    today_plus_six_months = today + datetime.timedelta(days=183)
+    return Visit.objects.annotate(
+        my_count=Count(Case(When(appointment__status=Appointment.APPOINTMENT_STATUS_SCHEDULED, then=1)))).filter(
+        datetime_begin__gt=today_plus_three_months,
+        datetime_begin__lt=today_plus_six_months,
+        is_finished=False,
+        post_mail_sent=False,
+        subject__default_location__in=get_filter_locations(user),
+        my_count=0)
+
+
+def get_exceeded_visits(user):
+    return Visit.objects.filter(datetime_end__lt=get_today_midnight_date(),
+                                is_finished=False,
+                                subject__default_location__in=get_filter_locations(user)
+                                ).order_by('datetime_begin')
+
+
+def get_unfinished_appointments(user):
+    return Appointment.objects.filter(
+        datetime_when__lt=get_today_midnight_date(),
+        status=Appointment.APPOINTMENT_STATUS_SCHEDULED,
+        location__in=get_filter_locations(user),
+    )
+
+
+def waiting_for_appointment(visit):
+    required_types = visit.appointment_types.all()
+    appointment_types = []
+    for appointment in visit.appointment_set.all():
+        for type in appointment.appointment_types.all():
+            if (appointment.status in [Appointment.APPOINTMENT_STATUS_FINISHED,
+                                       Appointment.APPOINTMENT_STATUS_SCHEDULED]) and (not (type in appointment_types)):
+                appointment_types.append(type)
+    result = False
+    for type in required_types:
+        if not (type in appointment_types):
+            result = True
+    return result
+
+
+def get_active_visits_without_appointments(user):
+    today = get_today_midnight_date()
+    return Visit.objects.annotate(
+        my_count=Count(Case(When(appointment__status=Appointment.APPOINTMENT_STATUS_SCHEDULED, then=1)))).filter(
+        datetime_begin__lt=today,
+        datetime_end__gt=today,
+        is_finished=False,
+        subject__default_location__in=get_filter_locations(user),
+        my_count=0)
+
+
+def get_filter_locations(user):
+    worker = None
+
+    if isinstance(user, User):
+        workers = Worker.objects.filter(user=user)
+        if len(workers) > 0:
+            worker = workers[0]
+    elif isinstance(user, Worker):
+        worker = user
+    elif isinstance(user, AnonymousUser):
+        # anonymous user shouldn't see anything
+        return Location.objects.filter(id=-1)
+    elif user is not None:
+        raise TypeError("Unknown class type: " + user.__class__.__name__)
+
+    if worker is None or worker.locations.count() == 0:
+        return Location.objects.all()
+    else:
+        return worker.locations.all()
+
+
+def get_today_midnight_date():
+    today = datetime.datetime.now()
+    today_midnight = datetime.datetime(today.year, today.month, today.day)
+    return today_midnight
+
+
+def get_calendar_full_appointments(user):
+    month_ago = get_today_midnight_date() + datetime.timedelta(days=-31)
+    return Appointment.objects.filter(
+        datetime_when__gt=month_ago,
+        location__in=get_filter_locations(user),
+    ).order_by('datetime_when')
diff --git a/smash/web/views/statistics.py b/smash/web/views/statistics.py
new file mode 100644
index 0000000000000000000000000000000000000000..486427f5f6d4ac18de9606b121378b4f24121ce7
--- /dev/null
+++ b/smash/web/views/statistics.py
@@ -0,0 +1,30 @@
+# coding=utf-8
+from . import wrap_response
+from ..forms import StatisticsForm
+from ..statistics import StatisticsManager, get_previous_year_and_month
+
+__author__ = 'Valentin Grouès'
+
+
+def statistics(request):
+    statistics_manager = StatisticsManager()
+    visit_choices = [("-1", "all")]
+    visit_choices.extend([(rank, rank) for rank in statistics_manager.visits_ranks])
+    year_previous_month, previous_month = get_previous_year_and_month()
+
+    form = StatisticsForm(request.GET, visit_choices=visit_choices, month=previous_month, year=year_previous_month)
+    if not form.is_valid():
+        form.is_bound = False
+    month = form.data.get('month', previous_month)
+    year = form.data.get('year', year_previous_month)
+    subject_type = form.data.get('subject_type', "-1")
+    visit = form.data.get('visit', "-1")
+    if subject_type == "-1":
+        subject_type = None
+    if visit == "-1":
+        visit = None
+    monthly_statistics = statistics_manager.get_statistics_for_month(month, year, subject_type, visit)
+    return wrap_response(request, 'statistics/index.html', {
+        'form': form,
+        'monthly_statistics': monthly_statistics
+    })
diff --git a/smash/web/views/subject.py b/smash/web/views/subject.py
new file mode 100644
index 0000000000000000000000000000000000000000..d8987167f2c7db1be39b577d660853862ace54a1
--- /dev/null
+++ b/smash/web/views/subject.py
@@ -0,0 +1,101 @@
+# coding=utf-8
+
+from django.shortcuts import redirect, get_object_or_404
+
+from notifications import get_subjects_with_no_visit, get_subjects_with_reminder
+from . import wrap_response
+from ..forms import SubjectAddForm, SubjectEditForm, VisitDetailForm
+from ..models import Subject
+
+__author__ = 'Valentin Grouès'
+
+
+def subjects(request):
+    subjects_list = Subject.objects.order_by('-last_name')
+    context = {
+        'subjects_list': subjects_list
+    }
+
+    return wrap_response(request, 'subjects/index.html', context)
+
+
+def subject_add(request):
+    if request.method == 'POST':
+        form = SubjectAddForm(request.POST, request.FILES)
+        if form.is_valid():
+            screening_number = form.cleaned_data['screening_number']
+            if screening_number == '':
+                screening_number = get_new_screening_number()  # FIXME: method doesn't exist
+            form.save()
+            return redirect('web.views.subjects')
+    else:
+        form = SubjectAddForm()
+
+    return wrap_response(request, 'subjects/add.html', {'form': form})
+
+
+def subject_no_visits(request):
+    subjects_list = get_subjects_with_no_visit(request.user).order_by('-last_name')
+    context = {
+        'subjects_list': subjects_list
+    }
+
+    return wrap_response(request, 'subjects/index.html', context)
+
+
+def subject_require_contact(request):
+    subjects_list = get_subjects_with_reminder(request.user).order_by('-last_name')
+    context = {
+        'subjects_list': subjects_list
+    }
+
+    return wrap_response(request, 'subjects/index.html', context)
+
+
+def subject_edit(request, id):
+    the_subject = get_object_or_404(Subject, id=id)
+    if request.method == 'POST':
+        form = SubjectEditForm(request.POST, request.FILES, instance=the_subject)
+        if form.is_valid():
+            form.save()
+            return redirect('web.views.subjects')
+    else:
+        form = SubjectEditForm(instance=the_subject)
+    return wrap_response(request, 'subjects/edit.html', {
+        'form': form,
+        'subject': the_subject
+    })
+
+
+def subject_delete(request, id):
+    the_subject = get_object_or_404(Subject, id=id)
+    if request.method == 'POST':
+        the_subject.delete()
+        return redirect('web.views.subjects')
+    else:
+        form = SubjectEditForm(instance=the_subject)
+    return wrap_response(request, 'subjects/delete.html', {'form': form})
+
+
+def subject_mark(request, id, as_what):
+    who = get_object_or_404(Subject, id=id)
+    if as_what == 'dead':
+        who.mark_as_dead()
+    elif as_what == 'rejected':
+        who.mark_as_rejected()
+    return redirect('web.views.subject_edit', id=id)
+
+
+def subject_visit_details(request, id):
+    locsubject = get_object_or_404(Subject, id=id)
+    visits = locsubject.visit_set.all()
+    endlist = []
+    for vis in visits:
+        assign = vis.appointment_set.all()
+        finished = vis.is_finished
+        visid = vis.id
+        visit_title = vis.follow_up_title()
+        visform = VisitDetailForm(instance=vis)
+        endlist.append((visform, assign, finished, visid, visit_title))
+
+    return wrap_response(request, 'subjects/visitdetails.html', {'display': endlist, "id": id})
diff --git a/smash/web/views/visit.py b/smash/web/views/visit.py
new file mode 100644
index 0000000000000000000000000000000000000000..dfce9763c938cc7e0f467320a99c5b48e40d3ab8
--- /dev/null
+++ b/smash/web/views/visit.py
@@ -0,0 +1,113 @@
+# coding=utf-8
+from django.shortcuts import get_object_or_404, redirect
+
+from notifications import get_active_visits_with_missing_appointments, get_unfinished_visits, \
+    get_approaching_visits_without_appointments, get_approaching_visits_for_mail_contact, get_exceeded_visits, \
+    waiting_for_appointment
+from . import wrap_response
+from ..forms import VisitDetailForm, SubjectDetailForm, VisitAddForm
+from ..models import Visit, Appointment, Subject
+
+__author__ = 'Valentin Grouès'
+
+
+def visits(request):
+    visit_list = Visit.objects.order_by('-datetime_begin')
+    context = {
+        'visit_list': visit_list
+    }
+
+    return wrap_response(request, 'visits/index.html', context)
+
+
+def visits_with_missing_appointments(request):
+    context = {
+        'visit_list': get_active_visits_with_missing_appointments(request.user)
+    }
+
+    return wrap_response(request, 'visits/index.html', context)
+
+
+def approaching_visits_without_appointments(request):
+    context = {
+        'visit_list': get_approaching_visits_without_appointments(request.user)
+    }
+    return wrap_response(request, 'visits/index.html', context)
+
+
+def approaching_visits_for_mail_contact(request):
+    context = {
+        'visit_list': get_approaching_visits_for_mail_contact(request.user)
+    }
+    return wrap_response(request, 'visits/index.html', context)
+
+
+def visit_details(request, id):
+    displayed_visit = get_object_or_404(Visit, id=id)
+    if request.method == 'POST':
+        visit_form = VisitDetailForm(request.POST, request.FILES, instance=displayed_visit)
+        if visit_form.is_valid():
+            visit_form.save()
+    else:
+        visit_form = VisitDetailForm(instance=displayed_visit)
+
+    visit_finished = displayed_visit.is_finished
+    visit_id = displayed_visit.id
+    displayed_subject = displayed_visit.subject
+    list_of_appointments = displayed_visit.appointment_set.all()
+
+    can_finish = not waiting_for_appointment(displayed_visit)
+
+    for appointment in list_of_appointments:
+        if appointment.status == Appointment.APPOINTMENT_STATUS_SCHEDULED:
+            can_finish = False
+
+    subject_form = SubjectDetailForm(instance=displayed_subject)
+
+    return wrap_response(request, 'visits/details.html', {
+        'vform': visit_form,
+        'sform': subject_form,
+        'loApp': list_of_appointments,
+        'visFinished': visit_finished,
+        'canFinish': can_finish,
+        'vid': visit_id,
+        'visit': displayed_visit})
+
+
+def visit_mark(request, id, as_what):
+    visit = get_object_or_404(Visit, id=id)
+    if as_what == 'finished':
+        visit.mark_as_finished()
+
+    return redirect('web.views.visit_details', id=id)
+
+
+def visit_add(request, subject_id=-1):
+    if request.method == 'POST':
+        form = VisitAddForm(request.POST, request.FILES)
+        if form.is_valid():
+            visit = form.save()
+            return redirect('web.views.visit_details', visit.id)
+    else:
+        subjects = Subject.objects.filter(id=subject_id)
+        subject = None
+        if len(subjects) > 0:
+            subject = subjects[0]
+        form = VisitAddForm(initial={'subject': subject})
+
+    return wrap_response(request, 'visits/add.html', {'form': form})
+
+
+def exceeded_visits(request):
+    context = {
+        'visit_list': get_exceeded_visits(request.user)
+    }
+    return wrap_response(request, 'visits/index.html', context)
+
+
+def unfinished_visits(request):
+    context = {
+        'visit_list': get_unfinished_visits(request.user)
+    }
+
+    return wrap_response(request, 'visits/index.html', context)