"""Test the views of the ``trainings`` app"""

import io
import json
import zipfile
from datetime import timedelta
from operator import itemgetter
from unittest import skip

from mock import call, patch

from django.contrib import messages
from django.core.urlresolvers import reverse
from django.template.loader import render_to_string
from django.test import override_settings
from django.utils import timezone
from django.utils.http import urlquote
from django.utils.text import Truncator
from django.utils.translation import ugettext_lazy as _

from mtp.apps.accounts.factories import (
    BusinessUnitFactory,
    GroupRoleFactory,
    JobProfileFactory,
    TrainerFactory)
from mtp.apps.common.tests.test_cases import DEFAULT_PASSWORD, MTPTestCase, MTPBaseViewsTestCase
from mtp.apps.common.tests.waffle_test_utils import override_switch
from mtp.apps.evaluations.models import Evaluation
from mtp.apps.questionnaires.models import Quiz
from mtp.apps.surveys.factories import PoolFactory, QuestionFactory
from mtp.apps.surveys.models import Question
from mtp.apps.tasks.models import TASK_STATES

from ..constants import ACTIVITY_LAUNCH_ACTION, MODULE_CHOICES
from ..factories import TrainingFactory, TrainingTaskFactory
from ..models import (
    ResultsToExcelActivityMixin,
    Training,
    TrainingActivity,
    TrainingSession,
    TASK_TYPES,
    TrainingTask
)
from ..views import (
    TrainingListView, TrainingSessionCreate,
    TrainingCreateDraftView
)

from .test_cases import TrainingWithActivitiesTestCase


class BaseTrainingViewsTestCase(MTPBaseViewsTestCase):
    """Base test case to use when testing the training views"""

    @classmethod
    def setUpTestData(cls):
        """Some data needed for the test"""

        super().setUpTestData()

        # Associate the new_trainer with one job_profile
        cls.job_profile = JobProfileFactory()
        GroupRoleFactory(user=cls.new_trainer, group=cls.job_profile)

        # Associate the new_trainer with one business_unit
        business_unit = BusinessUnitFactory()
        GroupRoleFactory(user=cls.new_trainer, group=business_unit)


class TrainingListViewTestCase(BaseTrainingViewsTestCase, TrainingWithActivitiesTestCase):
    """The tests for the list view of the ``Training`` model"""

    def test_training_list_view_as_trainer(self):
        """Test the training list view methods with the "trainer" user"""

        view = self.get_view(view=TrainingListView(), user=self.trainer)

        # This user own two trainings
        self.assertEqual(len(view.get_queryset()), 2)

    def test_training_list_view_as_new_trainer(self):
        """Test the training list view methods with the "new_trainer" user"""

        view = self.get_view(view=TrainingListView(), user=self.new_trainer)

        # This user don't own any training
        self.assertEqual(len(view.get_queryset()), 0)

    def test_training_list_view_with_business_unit_filter(self):
        """Test the training list view methods with business unit filters"""

        self.login_as_trainer()

        business_unit = BusinessUnitFactory()
        GroupRoleFactory(
            group=business_unit,
            user=self.trainer
        )
        training_list_url = '%s?business_unit=%s' % (
            Training.get_list_url(),
            business_unit.pk
        )

        response = self.client.get(training_list_url)

        # No training associated with this brand new business unit
        self.assertEqual(
            len(response.context['training_list']),
            0
        )

        # Create one using this business unit
        TrainingFactory(owner=self.trainer, business_unit=business_unit)

        response = self.client.get(training_list_url)

        # We got the new training
        self.assertEqual(
            len(response.context['training_list']),
            1
        )

    @override_switch(name='enable_assessments', active=True)
    def test_training_list_view_with_job_profile_filter(self):
        """Test the training list view methods with job profile filters"""

        self.login_as_trainer()

        job_profile = JobProfileFactory()
        GroupRoleFactory(
            group=job_profile,
            user=self.trainer
        )
        training_list_url = '%s?job_profile=%s' % (
            Training.get_list_url(),
            job_profile.pk
        )

        response = self.client.get(training_list_url)

        # No training associated with this brand new job profile
        self.assertEqual(
            len(response.context['training_list']),
            0
        )

        # Create one using this job profile
        TrainingFactory(owner=self.trainer, job_profile=job_profile)

        response = self.client.get(training_list_url)

        # We got the new training
        self.assertEqual(
            len(response.context['training_list']),
            1
        )

    def test_training_list_view_includes_waiting_tasks(self):
        """Test that if the user launched tasks about trainings, they are displayed"""

        self.login_as_admin()
        # Create other trainings like the first one
        admin_training2, admin_training3, admin_training4 = TrainingFactory.create_batch(
            3,
            owner=self.admin,
            business_unit=self.business_unit,
            job_profile=self.job_profile,
            theme=self.theme,
        )

        # Create a business unit for filtering (without trainings, just to have nothing in the list)
        business_unit = BusinessUnitFactory()
        GroupRoleFactory(group=business_unit, user=self.admin)

        # At first, no waiting trainings
        response = self.client.get(Training.get_list_url())
        self.assertNotIn('waiting_tasks_grouped', response.context)
        self.assertNotIn('waiting-trainings', str(response.content))

        # Even if no training at all in the list (ensure we don't have a 500 even if no trainings)
        response = self.client.get(
            '%s?business_unit=%s' % (Training.get_list_url(), business_unit.pk)
        )
        self.assertEqual(len(response.context['training_list']), 0)
        self.assertNotIn('waiting_tasks_grouped', response.context)
        self.assertNotIn('waiting-trainings', str(response.content))

        # Create some fake tasks, we set the started date to know the order
        now = timezone.now()

        # Two duplicate for the second training
        TrainingTaskFactory.create_batch(2, training=admin_training2,
                                         user=self.admin,
                                         task_type=TASK_TYPES.DUPLICATE_TO_MEMBER,
                                         started_at=now - timedelta(minutes=10))
        # One draft for the third training
        TrainingTaskFactory(training=admin_training3, user=self.admin,
                            task_type=TASK_TYPES.CREATE_DRAFT,
                            started_at=now - timedelta(minutes=8))
        # One duplicate for the fourth training
        TrainingTaskFactory(training=admin_training4, user=self.admin,
                            task_type=TASK_TYPES.DUPLICATE_TO_MEMBER,
                            started_at=now - timedelta(minutes=6))

        # One FINISHED duplicate for the fourth training
        TrainingTaskFactory(training=admin_training4, user=self.admin,
                            task_type=TASK_TYPES.DUPLICATE_TO_MEMBER,
                            state=TASK_STATES.SUCCESS,
                            started_at=now - timedelta(minutes=4))

        # And a task for another user
        TrainingTaskFactory(training=self.training, user=self.trainer,
                            task_type=TASK_TYPES.CREATE_DRAFT,
                            started_at=now - timedelta(minutes=2))

        # Now, display the list again, we should have our waiting trainings
        response = self.client.get(Training.get_list_url())

        # Ensure the context is ok
        self.assertIn('waiting_tasks_grouped', response.context)
        self.assertDictEqual(
            response.context['waiting_tasks_grouped'],
            TrainingTask.objects.group_tasks(TrainingTask.objects.filter(
                user=self.admin,
                state__in=TASK_STATES.UNREADY_STATES.values,
                training_id__in=list(
                    response.context['training_list'].values_list('pk', flat=True)
                ),
            ))
        )
        self.assertIn('waiting_tasks_grouped_json', response.context)
        self.assertDictEqual(
            json.loads(response.context['waiting_tasks_grouped_json']),
            TrainingTask.objects.group_tasks_for_json(TrainingTask.objects.filter(
                user=self.admin,
                state__in=TASK_STATES.UNREADY_STATES.values,
                training_id__in=list(
                    response.context['training_list'].values_list('pk', flat=True)
                ),
            ))
        )

        # Do some checks in the html
        html = str(response.content)
        self.assertEqual(html.count('<thead class="waiting-trainings">'), 1)
        self.assertEqual(html.count('<tbody class="waiting-trainings">'), 1)
        self.assertEqual(html.count('<tr class="waiting-training"'), 3)
        self.assertEqual(html.count('0 / 2'), 1)
        self.assertEqual(html.count('0 / 1'), 2)
        self.assertIn('WaitingTrainingsPoller.init', html)

        # Now, listing on a job profile without these trainings should display no waiting ones
        response = self.client.get(
            '%s?business_unit=%s' % (Training.get_list_url(), business_unit.pk)
        )
        self.assertNotIn('waiting_tasks_grouped', response.context)
        self.assertNotIn('waiting-trainings', str(response.content))

        # The other user should see its own waiting tasks
        self.client.logout()
        self.login_as_trainer()
        response = self.client.get(Training.get_list_url())

        # Ensure the context is ok
        self.assertIn('waiting_tasks_grouped', response.context)
        self.assertDictEqual(
            response.context['waiting_tasks_grouped'],
            TrainingTask.objects.group_tasks(TrainingTask.objects.filter(
                user=self.trainer,
                state__in=TASK_STATES.UNREADY_STATES.values,
                training_id__in=list(
                    response.context['training_list'].values_list('pk', flat=True)
                ),
            ))
        )
        self.assertIn('waiting_tasks_grouped_json', response.context)
        self.assertDictEqual(
            json.loads(response.context['waiting_tasks_grouped_json']),
            TrainingTask.objects.group_tasks_for_json(TrainingTask.objects.filter(
                user=self.trainer,
                state__in=TASK_STATES.UNREADY_STATES.values,
                training_id__in=list(
                    response.context['training_list'].values_list('pk', flat=True)
                ),
            ))
        )

        # Do some checks in the html
        html = str(response.content)
        self.assertEqual(html.count('<thead class="waiting-trainings">'), 1)
        self.assertEqual(html.count('<tbody class="waiting-trainings">'), 1)
        self.assertEqual(html.count('<tr class="waiting-training"'), 1)
        self.assertEqual(html.count('0 / 1'), 1)
        self.assertIn('WaitingTrainingsPoller.init', html)

        # And another trainer should see nothing
        self.client.logout()
        trainer2 = TrainerFactory()
        trainer2.set_password(DEFAULT_PASSWORD)
        trainer2.save()
        self.login_as_trainer(trainer2.username)

        response = self.client.get(Training.get_list_url())
        html = str(response.content)
        self.assertNotIn('waiting_tasks_grouped', response.context)
        self.assertNotIn('waiting_tasks_grouped_json', response.context)
        self.assertNotIn('waiting-trainings', html)
        self.assertNotIn('WaitingTrainingsPoller.init', html)

    def test_training_list_view_includes_recent_trainings(self):
        """Test that if some recent trainings exist for the user, they are highlighted"""

        # The trainer have a recent training
        self.login_as_trainer()

        # Do some checks in the html
        response = self.client.get(Training.get_list_url())
        html = str(response.content)

        # The list of recent trainings should be there
        self.assertEqual(html.count('<tbody class="recent-trainings">'), 1)

        # And our training should appear only once: in the recent training list, with a notice
        self.assertEqual(
            html.count('<tr class="training-row training_item_%s' % self.training.pk),
            1
        )
        # Notice for the two trainings
        self.assertEqual(html.count('<aside>%s</aside>' % _('Recently created')), 2)

        # But another trainee won't see it
        self.client.logout()
        trainer2 = TrainerFactory()
        trainer2.set_password(DEFAULT_PASSWORD)
        trainer2.save()
        self.login_as_trainer(trainer2.username)

        # Do some checks in the html
        response = self.client.get(Training.get_list_url())
        html = str(response.content)

        # The list of recent trainings should not be there
        self.assertNotIn('<tbody class="recent-trainings">', html)


class WaitingAndRecentTrainingsJsonViewTestCase(TrainingWithActivitiesTestCase):
    """Test that the view returning waiting/recent trainings via ajax works

    Notes
    -----
    Data in tests are created like in ``TrainingListViewTestCase``:
    ``test_json_view_includes_waiting_tasks``
        => ``test_training_list_view_includes_waiting_tasks``
    ``test_json_view_includes_recent_trainings``
        => ``test_training_list_view_includes_recent_trainings``

    """

    url = Training.get_waiting_and_recent_json_list_url()

    @staticmethod
    def get_json_grouped_task(training, remaining):
        """Helper to get the data to compare the json of a grouped-tasks entry

        Parameters
        -----------
        training: Training
            The training for which we want the grouped-tasks entry
        remaining: dict
            A dict for the remaining tasks for this training with
            - as key the task type
            - as value the list of tasks

        Returns
        -------
        dict
            The dict expected to be found in an entry of the ``grouped_tasks``
            of the json result of ``WaitingAndRecentTrainingsJsonView``

        """

        return {
            'training_name': training.name,
            'tasks': {
                task_type.constant: {
                    'remaining': [task.pk for task in tasks]
                }
                for task_type, tasks in remaining.items()
            }
        }

    @staticmethod
    def get_json_task(state, task, task_type, training):
        """Helper to get the data to compare the json of a grouped-tasks entry

        Parameters
        -----------
        state: TASK_STATES
            The state the task should be
        task: TrainingTask
            The task for which this json entry should be
        task_type: TASK_TYPES
            The type of the task
        training: Training
            The training for which this task should be

        Returns
        -------
        dict
            The dict expected to be found in an entry of the ``asks``
            of the json result of ``WaitingAndRecentTrainingsJsonView``

        """

        return {
            'state': state.constant,
            'task_id': task.celery_task_id,
            'task_type': task_type,
            'training_id': training.pk
        }

    @staticmethod
    def get_recent_trainings_html(user, trainings):
        """Helper to return the html expected in the ``recent_trainings`` entry

        Parameters
        ----------
        user: MTPUser
            The user for which we want to have the trainings
        trainings: list of Training
            The trainings expected to be found in the html

        Returns
        -------
        str
            The html expected to be found

        """

        # Enhance the trainings with annotation
        trainings = Training.objects.non_deleted().filter(
            id__in=[training.pk for training in trainings]
        ).available_for_user(
            user
        ).annotate_is_duplicable_by(
            user
        ).recently_created(
            user
        )

        return render_to_string('cms/trainings/training_list_include_trs.html', {
            'training_list': trainings
        })

    def test_json_view_includes_waiting_tasks(self):
        """Test that if the user launched tasks about trainings, they are returned"""

        self.login_as_admin()
        # Create other trainings like the first one
        admin_training2, admin_training3, admin_training4 = TrainingFactory.create_batch(
            3,
            owner=self.admin,
            business_unit=self.business_unit,
            job_profile=self.job_profile,
            theme=self.theme,
        )

        # Create a business unit for filtering (without trainings, just to have nothing in the list)
        business_unit = BusinessUnitFactory()
        GroupRoleFactory(group=business_unit, user=self.admin)

        # At first, no waiting trainings
        response = self.client.get(self.url, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
        self.assertJSONEqual(response.content.decode('utf-8'), {
            'tasks': [],
            'grouped_tasks': {},
        })

        # Create some fake tasks, we set the started date to know the order
        now = timezone.now()

        # Formatting as in TrainingListViewTestCase.test_training_list_view_includes_waiting_tasks

        # Two duplicate for the second training
        t2_1, t2_2 = TrainingTaskFactory.create_batch(2, training=admin_training2,
                                                      user=self.admin,
                                                      task_type=TASK_TYPES.DUPLICATE_TO_MEMBER,
                                                      started_at=now - timedelta(minutes=10))
        # One draft for the third training
        t3_1 = TrainingTaskFactory(training=admin_training3, user=self.admin,
                                   task_type=TASK_TYPES.CREATE_DRAFT,
                                   started_at=now - timedelta(minutes=8))
        # One duplicate for the fourth training
        t4_1 = TrainingTaskFactory(training=admin_training4, user=self.admin,
                                   task_type=TASK_TYPES.DUPLICATE_TO_MEMBER,
                                   started_at=now - timedelta(minutes=6))

        # One FINISHED duplicate for the fourth training
        t4_2 = TrainingTaskFactory(training=admin_training4, user=self.admin,
                                   task_type=TASK_TYPES.DUPLICATE_TO_MEMBER,
                                   state=TASK_STATES.SUCCESS,
                                   started_at=now - timedelta(minutes=4))

        # And a task for another user
        t0_1 = TrainingTaskFactory(training=self.training, user=self.trainer,
                                   task_type=TASK_TYPES.CREATE_DRAFT,
                                   started_at=now - timedelta(minutes=2))

        # Now, get the json again, we should have our waiting trainings
        response = self.client.get(self.url, HTTP_X_REQUESTED_WITH='XMLHttpRequest')

        self.assertJSONEqual(response.content.decode('utf-8'), {
            'grouped_tasks': {
                admin_training2.pk: self.get_json_grouped_task(
                    admin_training2, {TASK_TYPES.DUPLICATE_TO_MEMBER: [t2_1, t2_2]}
                ),
                admin_training3.pk: self.get_json_grouped_task(
                    admin_training3, {TASK_TYPES.CREATE_DRAFT: [t3_1]}
                ),
                admin_training4.pk: self.get_json_grouped_task(
                    admin_training4, {TASK_TYPES.DUPLICATE_TO_MEMBER: [t4_1, t4_2]}
                ),
            },
            'tasks': [
                self.get_json_task(
                    TASK_STATES.PENDING, t2_1, TASK_TYPES.DUPLICATE_TO_MEMBER, admin_training2),
                self.get_json_task(
                    TASK_STATES.PENDING, t2_2, TASK_TYPES.DUPLICATE_TO_MEMBER, admin_training2),
                self.get_json_task(
                    TASK_STATES.PENDING, t3_1, TASK_TYPES.CREATE_DRAFT, admin_training3),
                self.get_json_task(
                    TASK_STATES.PENDING, t4_1, TASK_TYPES.DUPLICATE_TO_MEMBER, admin_training4),
                self.get_json_task(
                    TASK_STATES.SUCCESS, t4_2, TASK_TYPES.DUPLICATE_TO_MEMBER, admin_training4),
            ]
        })

        # The other user should see its own waiting tasks
        self.client.logout()
        self.login_as_trainer()
        response = self.client.get(self.url, HTTP_X_REQUESTED_WITH='XMLHttpRequest')

        self.assertJSONEqual(response.content.decode('utf-8'), {
            'grouped_tasks': {
                self.training.pk: self.get_json_grouped_task(
                    self.training, {TASK_TYPES.CREATE_DRAFT: [t0_1]}
                ),
            },
            'tasks': [
                self.get_json_task(
                    TASK_STATES.PENDING, t0_1, TASK_TYPES.CREATE_DRAFT, self.training),
            ]
        })

        # And another trainer should see nothing
        self.client.logout()
        trainer2 = TrainerFactory()
        trainer2.set_password(DEFAULT_PASSWORD)
        trainer2.save()
        self.login_as_trainer(trainer2.username)

        response = self.client.get(self.url, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
        self.assertJSONEqual(response.content.decode('utf-8'), {
            'tasks': [],
            'grouped_tasks': {},
        })

    def test_json_view_includes_recent_trainings(self):
        """Test that if some recent trainings exist for the user, they are returned"""

        url = self.url + '?include_recent=1'

        # The trainer have a recent training
        self.login_as_trainer()

        response = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest')

        self.assertJSONEqual(response.content.decode('utf-8'), {
            'tasks': [],
            'grouped_tasks': {},
            'recent_trainings': self.get_recent_trainings_html(
                self.trainer,
                self.trainer.trainings.recently_created(self.trainer)
            ),
        })

        # But another trainee won't see it
        self.client.logout()
        trainer2 = TrainerFactory()
        trainer2.set_password(DEFAULT_PASSWORD)
        trainer2.save()
        self.login_as_trainer(trainer2.username)

        response = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest')

        self.assertJSONEqual(response.content.decode('utf-8'), {
            'tasks': [],
            'grouped_tasks': {},
            'recent_trainings': self.get_recent_trainings_html(trainer2, []),
        })


class TrainingCreateViewTestCase(TrainingWithActivitiesTestCase):
    """The tests for the create view of the ``Training`` model"""

    def test_owner_should_be_saved(self):
        """Test that the "owner" of a ``Training`` should correctly set when saved"""

        # Ensure the user have no trainings in his collection
        self.assertEqual(len(self.new_trainer.trainings.all()), 0)

        self.client.login(username=self.new_trainer.username,
                          password=DEFAULT_PASSWORD)

        self.client.post(self.training.get_create_url(), {
            'name': 'training name',
            'theme': self.theme.pk
        })

        # Now, the user have one training (because the owner was correctly set)
        self.assertEqual(len(self.new_trainer.trainings.all()), 1)


@skip('The survey are not usable now')
class TrainingSurveyFlagTestCase(MTPTestCase):
    """Test that the waffle switch `enable_surveys` is used in training views"""

    @classmethod
    def setUpTestData(cls):
        """Create a survey pool with a question"""

        super().setUpTestData()

        QuestionFactory(
            question_type=Question.QUESTION_TYPES.OPEN,
            value_type=Question.VALUE_TYPES.NONE,
            pool=PoolFactory(author=cls.trainer),
        )

        cls.survey_url = cls.training.get_survey_url()

    @override_switch('enable_surveys', False)
    def test_no_survey_buttons_absent_on_trainings_if_surveys_waffle_flag_off(self):
        """Test that the survey buttons are absent if switch is off"""

        self.login_as_trainer()

        response = self.client.get(self.training.get_list_url())

        self.assertFalse(response.context['user_can_have_survey'])
        self.assertNotIn(self.survey_url, str(response.content))

    @override_switch('enable_surveys', True)
    def test_survey_buttons_present_on_trainings_if_surveys_waffle_flag_on(self):
        """Test that the survey buttons are present if switch is on"""

        self.login_as_trainer()

        response = self.client.get(self.training.get_list_url())

        self.assertTrue(response.context['user_can_have_survey'])
        self.assertIn(self.survey_url, str(response.content))

    @override_switch('enable_surveys', False)
    def test_no_survey_button_absent_on_training_if_surveys_waffle_flag_off(self):
        """Test that the survey button is absent if switch is off"""

        self.login_as_trainer()

        response = self.client.get(self.training.get_edit_url())

        self.assertFalse(response.context['user_can_have_survey'])
        self.assertNotIn(self.training.get_survey_url(), str(response.content))

    @override_switch('enable_surveys', True)
    def test_survey_button_present_on_training_if_surveys_waffle_flag_on(self):
        """Test that the survey button is present if switch is on"""

        self.login_as_trainer()

        response = self.client.get(self.training.get_edit_url())

        self.assertTrue(response.context['user_can_have_survey'])
        self.assertIn(self.training.get_survey_url(), str(response.content))


class BaseTrainingEditViewTestCase:
    """Base class to use when testing the views from """

    def test_owner_should_be_accepted(self):
        """Test that the owner can correctly access the view"""

        self.login_as_trainer()

        response = self.client.get(self.url)

        self.assertEqual(response.status_code, 200)

    def test_not_owner_should_be_rejected(self):
        """Test the no-owner cannot access the view"""

        self.login_as_admin()

        response = self.client.get(self.url)

        self.assertEqual(response.status_code, 404)


class TrainingUpdateViewTestCase(BaseTrainingEditViewTestCase, MTPTestCase):
    """The tests for the update view of the ``Training`` model"""

    @classmethod
    def setUpTestData(cls):
        """Define the url to test"""

        super().setUpTestData()

        cls.url = cls.training.get_edit_url()


class TrainingPublishViewTestCase(BaseTrainingEditViewTestCase, MTPTestCase):
    """The tests for the publish view of the ``Training`` model"""

    @classmethod
    def setUpTestData(cls):
        """Define the url to test"""

        super().setUpTestData()

        # We must create a draft training, because only the drafts can be published
        cls.training = cls.training.create_draft()
        cls.url = cls.training.get_publish_version_state_url()

    def test_publish_view_should_publish_the_training(self):
        """Test that the publish view should correctly publish the training"""

        self.login_as_trainer()
        self.client.post(self.url)

        self.training.refresh_from_db()  # We need this

        self.assertEqual(self.training.version_state,
                         self.training.VERSION_STATES.PUBLISHED)


class TrainingCreateDraftViewTestCase(BaseTrainingEditViewTestCase, TrainingWithActivitiesTestCase):
    """The tests for the create draft view of the ``Training`` model"""

    @classmethod
    def setUpTestData(cls):
        """Define the url to test"""

        super().setUpTestData()

        cls.url = cls.training.get_create_draft_url()

    def test_get_if_not_in_progress_should_allow_creating_a_draft(self):
        """Test that when a draft creation is not in progress, it's marked as allowed in the view"""

        self.login_as_trainer()
        response = self.client.get(self.url)

        self.assertTrue(response.context['create_draft_allowed'])
        self.assertNotIn('create_draft_not_allowed_reason', response.context)

        # We should have the form in the response
        self.assertIn(
            '<form action="%s"' % self.training.get_create_draft_url(),
            str(response.content)
        )

    def test_get_if_in_progress_should_deny_creating_a_draft(self):
        """Test that when a draft creation is in progress, it's marked as allowed in the view"""

        self.login_as_trainer()

        # Simulate a draft creation in progress
        self.training.redis_instance.lock_create_draft(owner=self.training.owner)

        response = self.client.get(self.url)

        self.assertFalse(response.context['create_draft_allowed'])
        self.assertTrue(len(response.context['create_draft_not_allowed_reason']))

        # We should not have the form in the response
        self.assertNotIn(
            '<form action="%s"' % self.training.get_create_draft_url(),
            str(response.content)
        )

    def test_post_if_not_in_progress_should_create_a_draft(self):
        """Test that the create draft view should correctly create a draft."""

        self.login_as_trainer()

        # We'll only check if the ``create_draft`` method is called
        with patch.object(Training, 'create_draft') as patched:
            response = self.client.post(self.url)

        # The method ``create_draft`` should have been called one times, in async mode
        patched.assert_called_once_with(
            author=self.trainer,
            async=True,
            skip_in_progress=True,
        )
        # We should have been redirected to the list of trainings
        self.assertRedirects(response, self.training.get_list_url())

        # And we should have a success message
        message_found = False
        for message in messages.get_messages(response.wsgi_request):
            if message.message == TrainingCreateDraftView.success_message:
                message_found = message.level == messages.constants.SUCCESS

        if not message_found:
            self.fail('Expected success message not found')

    def test_post_if_in_progress_should_not_create_a_draft(self):
        """Test that the create draft view should not create a draft if already in progress.

        Notes
        -----
        This notes is copied from the ``post`` method of the ``TrainingCreateDraftView`` view:

        In the ``get`` method we stop the user if a new draft is being created.
        We don't do it in this ``post`` method, by passing ``skip_in_progress=True``, because it
        really should happen very rarely, when the user submits the form twice. So we simply
        ignore and do nothing if a draft creation is already in progress, and give the user its
        success message.

        """

        self.login_as_trainer()

        # Simulate a draft creation in progress
        self.training.redis_instance.lock_create_draft(owner=self.training.owner)

        # We'll only check if the ``create_draft`` method is called
        with patch.object(Training, 'create_draft') as patched:
            response = self.client.post(self.url)

        # The method ``create_draft`` should have been called one times, in async mode
        patched.assert_called_once_with(
            author=self.trainer,
            async=True,
            skip_in_progress=True,
        )

        # We should have been redirected to the list of trainings
        self.assertRedirects(response, self.training.get_list_url())

        # And we should have a success message
        message_found = False
        for message in messages.get_messages(response.wsgi_request):
            if message.message == TrainingCreateDraftView.success_message:
                message_found = message.level == messages.constants.SUCCESS

        if not message_found:
            self.fail('Expected success message not found')


class TrainingDuplicateViewTestCase(TrainingWithActivitiesTestCase):
    """The tests for the training duplicate view of the ``Training`` model"""

    @classmethod
    def setUpTestData(cls):
        """Define the url to test, set the trainer as the manager of the business unit"""

        super().setUpTestData()

        cls.url = cls.admin_training.get_duplicate_url()

    def test_view_on_get_request(self):
        """Test that we have the correct number of duplicates in the context."""

        self.login_as_admin()
        response = self.client.get(self.url)

        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.template_name[0], 'trainings/training_duplicate.html')

        # We will have 2 duplicate:
        # - for ``self.new_trainer`` (will have a ``PUBLISHED`` one)
        # - for ``self.trainer_manager`` (will have a ``DRAFT``)
        self.assertEqual(response.context_data['total_duplicates_nb'], 2)

    def test_view_on_post_request(self):
        """Test that the ``duplicate_to_member`` method is correctly called."""

        self.login_as_admin()

        # We'll only check if the ``duplicate_to_member`` method is called
        with patch.object(Training, 'duplicate_to_member') as patched:
            response = self.client.post(self.url)

        # The method ``duplicate_to_member`` should have been called three times, in async mode
        patched.assert_has_calls(
            [
                call(
                    new_state=self.training.VERSION_STATES.PUBLISHED,
                    params={
                        'owner_id': self.new_trainer.pk,
                        'business_unit_id': self.business_unit.pk,
                    },
                    async=True,
                    skip_in_progress=True,
                    author=self.admin,
                ),
                call(
                    new_state=self.training.VERSION_STATES.DRAFT,
                    params={
                        'owner_id': self.trainer_manager.pk,
                        'business_unit_id': self.business_unit_2.pk,
                    },
                    async=True,
                    skip_in_progress=True,
                    author=self.admin,
                ),
            ], any_order=True
        )

        self.assertRedirects(response, Training.get_list_url())


class TrainingPublishDuplicateTestCase(TrainingWithActivitiesTestCase):
    """The tests for the training publish and duplicate view of the ``Training`` model"""

    @classmethod
    def setUpTestData(cls):
        """Define the url to test"""

        super().setUpTestData()

        # We must create a draft training, because only the drafts can be published
        cls.draft_training = cls.admin_training.create_draft()
        cls.url = cls.draft_training.get_publish_and_share_url()

    def test_view_on_get_request(self):
        """Test that we have the correct number of duplicates in the context."""

        self.login_as_admin()
        response = self.client.get(self.url)

        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.context['total_duplicates_nb'], 3)

    def test_view_should_publish_and_duplicate_the_training(self):
        """Test that the publish view should correctly publish the training"""

        self.login_as_admin()

        with patch.object(Training, 'duplicate_to_member') as patched:
            response = self.client.post(self.url)

        patched.assert_has_calls(
            [
                call(
                    new_state=self.training.VERSION_STATES.PUBLISHED,
                    params={
                        'owner_id': self.new_trainer.pk,
                        'business_unit_id': self.business_unit.pk,
                    },
                    async=True,
                    skip_in_progress=True,
                    author=self.admin,
                ),
                call(
                    new_state=self.training.VERSION_STATES.DRAFT,
                    params={
                        'owner_id': self.trainer_manager.pk,
                        'business_unit_id': self.business_unit_2.pk,
                    },
                    async=True,
                    skip_in_progress=True,
                    author=self.admin,
                ),
            ], any_order=True
        )

        self.assertRedirects(response, Training.get_list_url())

        # Our draft training should be now in a "PUBLISHED" state
        self.draft_training.refresh_from_db()
        self.assertEqual(
            self.draft_training.version_state,
            self.draft_training.VERSION_STATES.PUBLISHED
        )


class TrainingArchiveViewTestCase(BaseTrainingEditViewTestCase, MTPTestCase):
    """The tests for the training archive view of the ``Training`` model"""

    @classmethod
    def setUpTestData(cls):
        """Define the url to test"""

        super().setUpTestData()

        cls.url = cls.training.get_archive_version_state_url()

    def test_archive_view_should_archive_the_training(self):
        """"Test that the delete view should correctly archive the training"""

        self.login_as_trainer()
        self.client.post(self.url)

        self.training.refresh_from_db()

        # The training is now archived
        self.assertEqual(
            self.training.version_state,
            Training.VERSION_STATES.ARCHIVED
        )

    def test_archive_view_with_archive_children_should_archive_the_children(self):
        """"Test that the delete view should correctly archive the training and its children"""

        self.login_as_admin()
        self.client.post(
            self.admin_training.get_archive_version_state_url(),
            {'archive_children': True}
        )

        self.admin_training.refresh_from_db()
        self.training.refresh_from_db()

        # The training is now archived
        self.assertEqual(
            self.admin_training.version_state,
            Training.VERSION_STATES.ARCHIVED
        )

        # And the trainer's training also
        self.assertEqual(
            self.training.version_state,
            Training.VERSION_STATES.ARCHIVED
        )


class TrainingDeleteViewTestCase(BaseTrainingEditViewTestCase, MTPTestCase):
    """The tests for the training delete view of the ``Training`` model"""

    @classmethod
    def setUpTestData(cls):
        """Define the url to test"""

        super().setUpTestData()

        # We must create a draft training, because only the drafts can be deleted
        cls.training = cls.training.create_draft()
        cls.url = cls.training.get_delete_url()

    def test_delete_view_should_soft_delete_the_training(self):
        """"Test that the delete view should correctly soft-delete the training"""

        self.login_as_trainer()
        self.client.post(self.url)

        self.training.refresh_from_db()  # We need this

        # The training is now soft-deleted
        self.assertEqual(self.training.is_deleted, True)


class TrainingOptionsEditViewTestCase(BaseTrainingEditViewTestCase, MTPTestCase):
    """The tests for the edit view of the ``TrainingClientOptions`` model"""

    @classmethod
    def setUpTestData(cls):
        """Define the url to test"""

        super().setUpTestData()

        # We must create a draft training, because only the drafts options can be edited
        cls.training = cls.training.create_draft()
        cls.url = cls.training.options.get_edit_url()


class TrainingActivityDeleteViewTestCase(TrainingWithActivitiesTestCase):
    """The tests for the delete view of the ``TrainingActivity`` model"""

    @classmethod
    def setUpTestData(cls):
        """Define the url to test"""

        super().setUpTestData()

        # We must create a draft training, because we can only edit / delete the activities
        # from a draft training
        cls.training = cls.training.create_draft()

    def test_delete_view_should_soft_delete_the_activity(self):
        """"Test that the delete view should correctly soft-delete the activity"""

        self.login_as_trainer()

        for activity in self.training.activities.non_deleted():
            with self.subTest(activity=activity):
                self.client.post(activity.get_delete_url())
                activity.refresh_from_db()
                self.assertEqual(activity.is_deleted, True)


class TrainingActivityDuplicateViewTestCase(TrainingWithActivitiesTestCase):
    """The tests for the duplicate view of the ``TrainingActivity`` model"""

    @classmethod
    def setUpTestData(cls):
        """Define the url to test"""

        super().setUpTestData()

        # We must create a draft training, because we can only duplicate activities
        # from a draft training
        cls.draft_training = cls.training.create_draft()

    def setUp(self):
        """Login as a trainer"""

        super().setUp()

        self.login_as_trainer()

    def test_training_activity_duplicate_view_should_call_duplicate_in_training(self):
        """"Test that the training duplicate view should correctly duplicate the activity"""

        first_activity = self.draft_training.activities.first()
        expected_name = Truncator(
            '%s - %s' % (first_activity.name, _('Copy'))
        ).chars(255, '…')

        with patch.object(TrainingActivity, 'duplicate_in_training') as mocked:
            self.client.post(first_activity.get_training_duplicate_url())

        mocked.assert_called_once_with(self.draft_training, name=expected_name)

    def test_trainings_duplicate_view_should_call_duplicate_in_training(self):
        """Test that the trainings duplicate should correctly duplicate the activity"""

        first_activity = self.training.activities.first()

        # Duplicate the first activity in our published training in the draft training
        with patch.object(TrainingActivity, 'duplicate_in_training') as mocked:
            self.client.post(
                first_activity.get_trainings_duplicate_url(),
                {
                    'trainings': [self.draft_training.pk]
                }
            )

        mocked.assert_called_once_with(self.draft_training)

        # Another draft training
        draft_training_2 = self.training.create_draft()

        # Duplicate the first activity in our published training in both of our draft
        with patch.object(TrainingActivity, 'duplicate_in_training') as mocked:
            self.client.post(
                first_activity.get_trainings_duplicate_url(),
                {
                    'trainings': [self.draft_training.pk, draft_training_2.pk]
                }
            )

        mocked.assert_has_calls(
            [
                call(self.draft_training),
                call(draft_training_2)
            ],
            any_order=True
        )

    def test_private_library_duplicate_view_should_call_duplicate_in_library(self):
        """Test that the private library duplicate view should correctly duplicate the activity"""

        first_activity = self.training.activities.first()

        # Duplicate the first activity in our published training in the draft training
        with patch.object(TrainingActivity, 'duplicate_in_library') as mocked:
            self.client.post(
                first_activity.get_private_library_duplicate_url(),
                {
                    'duplicate_name': first_activity.name
                }
            )

        mocked.assert_called_once_with(
            is_shared=False,
            owner=self.trainer,
            name=first_activity.name
        )

    def test_public_library_duplicate_view_should_call_duplicate_in_library(self):
        """Test that the public library duplicate view should correctly duplicate the activity"""

        first_activity = self.training.activities.first()

        # Duplicate the first activity in our published training in the draft training
        with patch.object(TrainingActivity, 'duplicate_in_library') as mocked:
            self.client.post(
                first_activity.get_public_library_duplicate_url(),
                {
                    'duplicate_name': first_activity.name
                }
            )

        mocked.assert_called_once_with(
            is_shared=True,
            owner=self.trainer,
            name=first_activity.name
        )


class TrainingSessionListViewTestCase(MTPTestCase):
    """The tests for the list view of the ``TrainingSession`` model"""

    def test_session_list_view_without_training_for_trainer(self):
        """Test the output of the session list methods without a training filter"""

        self.login_as_trainer()

        # Call the view with the trainer user
        response = self.client.get(reverse('trainings:session-list'))

        self.assertEqual(
            response.status_code,
            200
        )

        # The trainer see one session (for the training he owns)
        self.assertEqual(
            len(response.context['trainingsession_list']),
            1
        )

    def test_session_list_has_next_url_param_for_edit_and_close_links(self):
        """Test the next url param is injected into the edit link"""

        self.login_as_trainer()

        # let's add a GET param to be sure it is injected too
        list_url = '%s%s' % (reverse('trainings:session-list'), '?page=1')
        response = self.client.get(list_url)

        # the next url param should be present in the edit link and link back
        # to the sessions list page
        list_url_encoded = urlquote(list_url)

        edit_link = '%s?next=%s' % (
            self.training_session.get_edit_url(),
            list_url_encoded
        )
        self.assertIn(
            edit_link,
            response.rendered_content
        )

        close_link = '%s?next=%s' % (
            self.training_session.get_close_url(),
            list_url_encoded
        )
        self.assertIn(
            close_link,
            response.rendered_content
        )

    def test_session_list_view_without_training_for_trainee(self):
        """Test the output of the session list methods without a training filter"""

        self.login_as_trainee()

        # Call the view with the trainee user
        response = self.client.get(reverse('trainings:session-list'))

        self.assertEqual(
            response.status_code,
            200
        )

        # The trainee see one session (he was added into)
        self.assertEqual(
            len(response.context['trainingsession_list']),
            1
        )

    def test_session_list_view_with_job_profile_manager(self):
        """Test that we have a ``business_unit`` filter with a job profile manager"""

        self.login_as_admin()

        # Call the view with the admin user
        response = self.client.get(reverse('trainings:session-list'))

        self.assertEqual(
            response.status_code,
            200
        )

        # The business unit field is present
        self.assertIn(
            'id_training__business_unit',
            response.content.decode('utf-8')
        )


class TrainingSessionEditViewsTestCase(MTPBaseViewsTestCase, MTPTestCase):
    """The tests for the edit views of the ``TrainingSession`` model"""

    def setUp(self):
        """Login as a trainer"""

        super().setUp()

        self.login_as_trainer()

    def test_all_routes_should_work(self):
        """Test the response_code of all the session edit routes"""

        urls_to_test = [
            self.training_session.get_edit_url(),
            self.training_session.get_delete_url(),
            self.training_session.get_whoswho_url(),
            self.training_session.get_close_url(),
            self.training_session.get_create_url(),
            self.training.get_session_create_url(),
        ]

        for url in urls_to_test:
            response = self.client.get(url)
            self.assertEqual(response.status_code, 200)

    def test_session_create_view_without_a_training_as_a_trainer(self):
        """Test the behaviour of the session create view, as a trainer"""

        view = self.get_view(view=TrainingSessionCreate(), user=self.trainer)

        self.assertEqual(
            view.get_form_kwargs().get('remove_trainer_field'),
            True
        )
        self.assertEqual(
            view.get_form_kwargs().get('current_user'),
            self.trainer
        )

    def test_session_create_view_without_a_training_as_an_admin(self):
        """Test the behaviour of the session create view, as an admin"""

        view = self.get_view(view=TrainingSessionCreate(), user=self.admin)

        self.assertEqual(
            view.get_form_kwargs().get('remove_trainer_field'),
            False
        )
        self.assertEqual(
            view.get_form_kwargs().get('current_user'),
            self.admin
        )

    def test_save_the_session_should_set_the_trainer_as_a_trainer(self):
        """Test that the create session, as a trainer, should set the trainer"""

        # One already created session
        self.assertEqual(
            TrainingSession.objects.filter(trainer=self.trainer).count(),
            1
        )

        response = self.client.post(self.training.get_session_create_url(), {
            'training': self.training.pk,
            'start_date': '2015-01-01 18:30',
            'end_date': '2015-01-01 18:30',
            'state': 'op',
            'permissions': 'pu'
        }, follow=True)

        self.assertEqual(
            response.status_code,
            200  # Redirected after the save
        )

        # The trainer now got two sessions
        self.assertEqual(
            TrainingSession.objects.filter(trainer=self.trainer).count(),
            2
        )

    def test_create_session_end_date_before_start_date(self):
        """Test that a session cannot be created if end_date is before start_date."""
        self.login_as_trainer()
        response = self.client.post(self.training.get_session_create_url(), {
            'training': self.training.pk,
            'start_date': '2015-01-01 19:30',
            'end_date': '2015-01-01 18:30',
            'state': 'op',
            'permissions': 'pu'
        })
        self.assertEqual(response.status_code, 200)
        self.assertEqual(
            response.context['form'].errors['__all__'],
            ['End date cannot be earlier than start date']
        )

    def test_save_the_session_should_not_set_the_trainer_as_an_admin(self):
        """Test that editing a session, as an admin, should not modify the trainer"""

        # The session is associated with the trainer
        self.assertEqual(
            self.training_session.trainer_id,
            self.trainer.id
        )

        self.login_as_admin()

        # Modify the session
        response = self.client.post(self.training_session.get_edit_url(), {
            'start_date': '2015-01-01 18:30',
            'end_date': '2015-01-01 18:30',
            'state': 'du',
            'permissions': 'op'
        }, follow=True)

        self.assertEqual(
            response.status_code,
            200  # Redirected after the save
        )

        self.training_session.refresh_from_db()

        # The session is still associated with the trainer
        self.assertEqual(
            self.training_session.trainer_id,
            self.trainer.id
        )

    def test_save_session_view_should_redirect_to_next_url(self):
        """Test that the redirect url with ?next mecanism works"""

        next_url = '/cms/trainings/sessions/?page=2'

        response = self.client.post(
            '%s?next=%s' % (self.training_session.get_edit_url(), next_url),
            {
                'trainer': self.training_session.trainer.pk,
                'start_date': '2015-01-01 18:30',
                'end_date': '2015-01-01 18:30',
                'state': 'op',
                'permissions': 'pu'
            },
            follow=True
        )

        self.assertIn(
            (next_url, 302),
            response.redirect_chain
        )

    @override_settings(INSTANCE_IS_MASTER=True)
    def test_close_session_view_should_close_the_session(self):
        """Test that the close session view should correctly close the session"""

        self.assertEqual(self.training_session.state,
                         self.training_session.STATE_CHOICES.OPEN)

        self.client.post(self.training_session.get_close_url())

        self.training_session.refresh_from_db()  # Necessary

        self.assertEqual(self.training_session.state,
                         self.training_session.STATE_CHOICES.CLOSED)

    @override_settings(INSTANCE_IS_MASTER=True)
    def test_close_session_view_should_redirect_to_next_url(self):
        """Test that the redirect url with ?next mecanism works"""

        next_url = '/cms/trainings/sessions/?page=2'

        response = self.client.post(
            '%s?next=%s' % (self.training_session.get_close_url(), next_url),
            follow=True
        )

        self.assertIn(
            (next_url, 302),
            response.redirect_chain
        )


class ActivityQRCodeDownloadViewTestCase(TrainingWithActivitiesTestCase):
    """Test case that tests the launch QRCode download."""

    def test_get_qrcode_data(self):
        """Test get qrcode data which be stored in QRCode."""

        for activity in self.training.activities.non_deleted():
            if activity.__class__.__name__.upper() in MODULE_CHOICES.constants:
                with self.subTest(activity=activity):
                    qrcode_data = activity.get_qrcode_data()

                    self.assertEqual(qrcode_data['id'], activity.id)
                    self.assertEqual(
                        qrcode_data['type'],
                        getattr(MODULE_CHOICES, activity.__class__.__name__.upper())
                    )
                    self.assertEqual(qrcode_data['action'], ACTIVITY_LAUNCH_ACTION)
                    self.assertEqual(qrcode_data['original_version'], activity.id)

                    duplicated_activity = activity.duplicate()
                    qrcode_data = duplicated_activity.get_qrcode_data()

                    self.assertEqual(qrcode_data['id'], duplicated_activity.id)
                    self.assertEqual(
                        qrcode_data['type'],
                        getattr(MODULE_CHOICES, activity.__class__.__name__.upper())
                    )
                    self.assertEqual(qrcode_data['action'], ACTIVITY_LAUNCH_ACTION)
                    self.assertEqual(
                        qrcode_data['original_version'],
                        duplicated_activity.original_version_id
                    )

    def test_qrcode_download_detail_view_work(self):
        """Test that the download detail view of QR Code work."""

        self.login_as_trainer()

        for activity in self.training.activities.non_deleted():
            if activity.__class__.__name__.upper() in MODULE_CHOICES.constants:
                with self.subTest(activity=activity):
                    response = self.client.get(activity.get_qrcode_url())
                    self.assertEqual(response.status_code, 200)

    def test_qrcode_download_list_view_work(self):
        """Test that the download list view of QR Code work."""

        url = self.training.get_qrcodes_url()

        self.login_as_trainer()
        response = self.client.get(url)

        self.assertEqual(response.status_code, 200)


class TrainingSessionResultsExportViewTestCase(TrainingWithActivitiesTestCase):
    """Test case for the ``TrainingSessionResultsExportView`` view."""

    def test_get_view_should_work(self):
        """Test that the view is working correctly"""

        self.login_as_trainer()

        response = self.client.get(
            self.training_session.get_export_results_url()
        )
        self.assertEqual(response.status_code, 200)

        activity_list = [
            {
                'id': activity.id,
                'name': activity.name,
                'type': activity.verbose_activity_name
            }
            for activity in self.training.activities.non_deleted()
            if isinstance(activity, ResultsToExcelActivityMixin) and activity.is_exportable(
                self.training_session
            )
        ]

        self.assertEqual(
            response.context['exportable_activities'],
            sorted(activity_list, key=itemgetter('type'))
        )

    def test_view_should_call_the_results_to_excel_method(self):
        """Test the view should call the ``results_to_excel`` method"""

        self.login_as_trainer()

        with patch.object(Quiz, 'results_to_excel', return_value=io.BytesIO()) as mocked:
            with patch.object(Quiz, 'is_exportable', return_value=True):
                response = self.client.post(
                    self.training_session.get_export_results_url(),
                    {
                        'activities_to_export': [self.quiz.id]
                    }
                )

        mocked.assert_called_once_with(self.training_session)

        # I should get one excel file in the zip
        file = io.BytesIO(response.content)
        with zipfile.ZipFile(file, 'r') as zipped_file:
            self.assertIsNone(zipped_file.testzip())
            self.assertCountEqual(
                ['%d - %s.xlsx' % (0, self.quiz.name)],
                zipped_file.namelist()
            )

        # With another exportable activity (an evaluation)
        with patch.object(Evaluation, 'results_to_excel', return_value=io.BytesIO()) as mocked:
            with patch.object(Evaluation, 'is_exportable', return_value=True):
                response = self.client.post(
                    self.training_session.get_export_results_url(),
                    {
                        'activities_to_export': [self.evaluation.id]
                    }
                )

        mocked.assert_called_once_with(self.training_session)

        # I should get one excel file in the zip
        file = io.BytesIO(response.content)
        with zipfile.ZipFile(file, 'r') as zipped_file:
            self.assertIsNone(zipped_file.testzip())
            self.assertCountEqual(
                ['%d - %s.xlsx' % (0, self.evaluation.name)],
                zipped_file.namelist()
            )

        # It should also work with both activities selected
        with patch.object(Evaluation, 'results_to_excel', return_value=io.BytesIO()) as mocked_e:
            with patch.object(Quiz, 'results_to_excel', return_value=io.BytesIO()) as mocked_w:
                with patch.object(Evaluation, 'is_exportable', return_value=True):
                    with patch.object(Quiz, 'is_exportable', return_value=True):
                        response = self.client.post(
                            self.training_session.get_export_results_url(),
                            {
                                'activities_to_export': [self.evaluation.id, self.quiz.id]
                            }
                        )

        mocked_e.assert_called_once_with(self.training_session)
        mocked_w.assert_called_once_with(self.training_session)

        # I should get two excel files in the zip
        file = io.BytesIO(response.content)
        with zipfile.ZipFile(file, 'r') as zipped_file:
            self.assertIsNone(zipped_file.testzip())
            self.assertCountEqual(
                (
                    '%d - %s.xlsx' % (index, activity.name)
                    for index, activity in enumerate(self.training.activities.non_deleted().filter(
                        id__in=[self.evaluation.id, self.quiz.id]
                    ).order_by('sort_order'))
                ),
                zipped_file.namelist()
            )
