class MissionAnalytics(object):

    def __init__(self, mission_id):
        self.mission_id = mission_id

        # All analytics exclude hidden test cases so we don't report
        # numbers of tests greater than what the customer ultimately sees
        self.testcases = TestDetail.objects.filter(mission=self.mission_id).exclude(test_case_include_flag=False)

        self.test_case_types_by_mission_week = self._count_of_test_case_types_by_mission_week()

    def count_of_findings(self):
        count = self.testcases.exclude(findings=u'').count()
        return count

    def count_of_test_cases(self):
        count = self.testcases.count()
        return count

    def count_of_executed_test_cases(self):
        count = self.testcases.exclude(execution_status=u'N').count()
        return count

    def count_of_test_cases_approved(self):
        count = self.testcases.filter(test_case_status=u'FINAL').count()
        return count

    def mission_execution_percentage(self):

        if self.count_of_test_cases() == 0:
            # prevent division by 0
            percentage = 0
        else:
            percentage = self.count_of_executed_test_cases() / self.count_of_test_cases()
        return '{:.0%}'.format(percentage)

    def mission_completion_percentage(self):

        if self.count_of_test_cases() == 0:
            # prevent division by 0
            percentage = 0
        else:
            percentage = self.count_of_test_cases_approved() / self.count_of_test_cases()
        return '{:.0%}'.format(percentage)

    def count_of_test_cases_by_result(self):
        """
        Summation of test cases which have each result type.
        :return: A list containing 2 lists:
                    - a list of test result type identifiers
                    - an identical length list of integer quantities of TCs which have the corresponding result
        """

        # Get the test result type tuple & convert to list
        output_list = [[], [], []]  # Lookup value list, Display text list, Count list

        def nested_tuple_to_shallow_list(tup):
            output_list[0].append(tup[0])
            output_list[1].append(tup[1])
            output_list[2].append(0)

        for result in TestDetail.EXECUTION_STATUS_OPTIONS:
            nested_tuple_to_shallow_list(result)

        for tc_result in self.testcases.values('execution_status'):
            index = output_list[0].index(tc_result['execution_status'])
            output_list[2][index] += 1

        return output_list[1:]  # No need to return the lookup values list

    def count_of_test_cases_by_mission_week(self):
        """
        Counts total test cases executed per week.
        :return: A zero-indexed list of the number of test cases executed per week.
        """

        if self.count_of_executed_test_cases() == 0:
            return [0]

        # Get the execution date for each test case in the mission
        tc_dates = self.testcases.exclude(execution_status=u'N').values('attack_time_date')

        # Create a hashmap of the count of TCs per iso calendar week
        weekly_count = Counter()
        for tc_date in tc_dates:
            isocalendar_week = tc_date['attack_time_date'].isocalendar()[1]  # Grab the isocalendar Week #
            weekly_count[isocalendar_week] += 1

        # Get the lowest & highest key values - these are week 1 and the last week respectively
        # This allows for skipped weeks (just in case)
        first_week = min(weekly_count.keys())
        last_week = max(weekly_count.keys())
        week_delta = last_week - first_week + 1

        # Build a hashmap of week # and the associated count
        zero_indexed_weekly_count = [0] * week_delta
        for week, count in weekly_count.items():
            zero_indexed_weekly_count[week - first_week] = count

        return zero_indexed_weekly_count

    def _count_of_test_case_types_by_mission_week(self):
        """
        Weekly totals broken out by attack phase; includes total row.
        :return: A list of lists: Each list within the containing list is guaranteed to be the same length as all others
                 and will have the test case Phase at index 0, followed by the number of test cases of that type
                 completed each week of the mission's execution. The last inner list is a total row.
                 Example of 2 week execution:
                 [['R&D', 2, 0],['EXP', 1, 2],['TOTAL', 3, 2]]
        """

        if self.count_of_executed_test_cases() == 0:
            return [['No TCs have been executed yet!']]

        # Get the execution date & type for each executed test case in the mission
        tc_records = self.testcases.exclude(execution_status=u'N').values('attack_time_date', 'attack_phase')

        # Create a hashmap of the count of TCs per iso calendar week
        weekly_count = defaultdict(Counter)

        for tc_record in tc_records:
            isocalendar_week = tc_record['attack_time_date'].isocalendar()[1]  # Grab the isocalendar Week #
            attack_phase = tc_record['attack_phase']
            weekly_count[isocalendar_week][attack_phase] += 1

        # Get the lowest & highest key values - these are week 1 and the last week respectively
        # This allows for skipped weeks (just in case)
        first_week = min(weekly_count.keys())
        last_week = max(weekly_count.keys())
        week_delta = last_week - first_week + 1

        # Build a hashmap of week # and the associated count
        zero_indexed_phase_count_by_week = []

        header_row = list()
        header_row.append('')
        header_row.extend(range(1, week_delta))

        total_row = list()
        total_row.append('TOTAL')
        total_row.extend([0] * week_delta)

        for phase_tuple in TestDetail.ATTACK_PHASES:
            phase = phase_tuple[0]
            phase_row = [0] * (week_delta + 1)
            phase_row[0] = phase
            for week, attack_phase_counter in weekly_count.items():
                column = week - first_week + 1
                phase_row[column] = attack_phase_counter[phase]
                total_row[column] += attack_phase_counter[phase]
            zero_indexed_phase_count_by_week.append(phase_row)

        zero_indexed_phase_count_by_week.append(total_row)

        logger.debug(zero_indexed_phase_count_by_week)

        return zero_indexed_phase_count_by_week

    def count_of_test_case_types_by_mission_week(self):
        return self.test_case_types_by_mission_week class TestSortingHelper(object):

    @staticmethod
    def deconflict_and_update(mission_id, tests=None):
        """ Ensures the mission's testdetail_sort_order is accurate and returns an ordered array of current test Ids """

        logger.debug('Performing TC deconflict and update (mission {mission})'.format(mission=mission_id))

        mission_model = Mission.objects.get(pk=mission_id)
        sort_order = json.loads(mission_model.testdetail_sort_order)
        deconflicted_sort_order = []
        sort_order_dirty = False

        if not tests:
            try:
                tests = TestDetail.objects.filter(mission=mission_id)
            except TestDetail.DoesNotExist:
                tests = ()

        # If tests exist, but there's no sort order, this may be the first run of this DART version;
        # We should preserve existing sort order and build the testdetail_sort_order field
        if tests and not sort_order:
            logger.debug('TC Deconflict found TCs but no existing order (mission {mission}); ' +
                        'Building sort order from test case numbers'.format(mission=mission_id))
            tests.order_by('test_number')
            sort_order = [test.id for test in tests]

        # Perform a reconcile to catch edge cases where tests have been added or deleted
        logger.debug('TC Deconflict is reconciling sort order with the database (mission {mission})'
                    .format(mission=mission_id))
        test_ids_from_db = [test.id for test in tests]
        for x in sort_order:
            if x in test_ids_from_db:
                deconflicted_sort_order.append(x)
            else:
                logger.info('Sort order is dirty (TC has been deleted from DB) (mission {mission}, tc {tc})'
                            .format(mission=mission_id, tc=x))
                sort_order_dirty = True  # Id in mission's sort order, but not database (it's been deleted)

        for x in test_ids_from_db:
            if x not in deconflicted_sort_order:
                deconflicted_sort_order.append(x)
                logger.info('Sort order is dirty (TC has been added to DB) (mission {mission}, tc {tc})'
                            .format(mission=mission_id, tc=x))
                sort_order_dirty = True  # Id in the database, but not in the mission's sort order (it's been added)

        # Save the reconciled sort order
        if sort_order_dirty:
            mission_model.testdetail_sort_order = json.dumps(deconflicted_sort_order)
            mission_model.save(update_fields=['testdetail_sort_order'])
            logger.info('Reconciliation Performed (Mission %s): '
                        'Mission Sort Order: %s; '
                        'Database TC Records: %s; '
                        'Result of Deconflict: %s' % (mission_id, sort_order, test_ids_from_db, deconflicted_sort_order))

        return deconflicted_sort_order

    @staticmethod
    def deconflict_and_update_supporting_data_sort_order(testdetail_id, testdata=None):
        """Ensures the supporting_data_sort_order of a test is accurate. Returns ordered array of supporting_data ids"""

        logger.debug('Performing Supporting Data deconflict and update (testdetail {testdetail})'
                     .format(testdetail=testdetail_id))

        testdetail_model = TestDetail.objects.get(pk=testdetail_id)
        sort_order = json.loads(testdetail_model.supporting_data_sort_order)
        deconflicted_sort_order = []
        sort_order_dirty = False

        # Preserve existing sort order and build the testdetail_sort_order field
        if testdata and not sort_order:
            logger.debug('Supporting data for test found but no existing order (testdetail {testdetail}); ' +
                         'Building default sort order'.format(testdetail=testdetail_id))
            sort_order = [data.id for data in testdata]

        # Perform a reconcile to catch edge cases where testdata have been added or deleted
        logger.debug('Supporting Data Deconflict is reconciling sort order with the database (testdetail {testdetail})'
                     .format(testdetail=testdetail_id))
        data_ids_from_db = [data.id for data in testdata]
        for x in sort_order:
            if x in data_ids_from_db:
                deconflicted_sort_order.append(x)
            else:
                logger.info('Sort order is dirty (Supporting data has been deleted from DB) '
                            '(testdetail {testdetail}, sd {sd})'.format(testdetail=testdetail_id, sd=x))
                sort_order_dirty = True  # Id in mission's sort order, but not database (it's been deleted)

        for x in data_ids_from_db:
            if x not in deconflicted_sort_order:
                deconflicted_sort_order.append(x)
                logger.info('Sort order is dirty (Supporting data has been added to DB) '
                            '(testdetail {testdetail}, sd {sd})'.format(testdetail=testdetail_id, sd=x))
                sort_order_dirty = True  # Id in the database, but not in the testdetail sort order (it's been added)

        # Save the reconciled sort order
        if sort_order_dirty:
            testdetail_model.supporting_data_sort_order = json.dumps(deconflicted_sort_order)
            testdetail_model.save(update_fields=['supporting_data_sort_order'])
            logger.info('Reconciliation Performed (TestDetail %s): '
                        'TestDetail Supporting Data Sort Order: %s; '
                        'Suppporting Data from Database: %s; '
                        'Result of Deconflict: %s' % (testdetail_id, sort_order, data_ids_from_db, deconflicted_sort_order))

        return deconflicted_sort_order

    @classmethod
    def get_ordered_testdetails(cls, mission_id, reportable_tests_only=False):

        try:
            tests = TestDetail.objects.filter(mission=mission_id)
        except TestDetail.DoesNotExist:
            tests = ()

        sort_order = cls.deconflict_and_update(mission_id, tests)

        # Order
        excluded_tests = []
        if reportable_tests_only:
            excluded_tests = tests.filter(test_case_include_flag=False).values_list('id', flat=True)

        test_dict = dict([(test.id, test) for test in tests])
        ordered_tests = [test_dict[test_id] for test_id in sort_order if test_id not in excluded_tests]

        return ordered_tests

    @classmethod
    def get_ordered_supporting_data(cls, test_detail_id, reportable_supporting_data_only=False):

        try:
            testdata = SupportingData.objects.filter(test_detail=test_detail_id)
        except TestDetail.DoesNotExist:
            testdata = ()

        sort_order = cls.deconflict_and_update_supporting_data_sort_order(test_detail_id, testdata)

        # Order
        excluded_data = []
        if reportable_supporting_data_only:
            excluded_data = testdata.filter(include_flag=False).values_list('id', flat=True)

        testdata_dict = dict([(data.id, data) for data in testdata])
        ordered_testdata = [testdata_dict[testdata_id] for testdata_id in sort_order if testdata_id not in excluded_data]

        return ordered_testdata class ReturnStatus(object):
    def __init__(self, success=True, message='', **kwargs):
        self.success = success
        self.message = message
        self.data = kwargs.get('data') or {}

    def to_json(self):
        return json.dumps(self.__dict__)

    def to_dict(self):
        return self.__dict__

    def __str__(self):
        return str(self.success) + ': ' + str(self.message) def generate_report_or_attachments(mission_id, zip_attachments=False):
    '''
    Generates the report docx or attachments zip.
    :param mission_id: The id of the mission
    :param zip_attachments: True to return a zip of attachments, False to get the docx report as an IOStream
    :return: Returns StringIO if returning a report, a zip object otherwise
    '''
    system_classification = DARTDynamicSettings.objects.get_as_object().system_classification
    system_classification_verbose = system_classification.verbose_legend
    system_classification_short = system_classification.short_legend

    mission = Mission.objects.get(id=mission_id)
    tests = TestSortingHelper.get_ordered_testdetails(
        mission_id=mission_id,
        reportable_tests_only=True)

    # Set some values we'll use throughout this section
    total_reportable_tests = len(tests)
    total_tests_with_findings = TestDetail.objects.filter(
        mission=mission,
        has_findings=True).count()
    total_tests_without_findings = TestDetail.objects.filter(
        mission=mission,
        has_findings=False).count()
    LIGHTEST_PERMISSIBLE_CLASSIFICATION_LABEL_COLOR = 0xbbbbbb
    DARKEN_OVERLY_LIGHT_CLASSIFICATION_LABEL_COLOR_BY = 0x444444
    mission_data_dir = None
    report_has_attachments = False

    def replace_document_slugs(doc):
        """Cycle through the runs in each paragraph in the template & replace handlebar slugs"""

        logger.debug('> replace_document_slugs')

        handlebar_slugs = {
            r'{{AREA}}': str(mission.business_area),
            r'{{MISSION}}': str(mission.mission_name),
            r'{{GENERATION_DATE}}': now().strftime('%x'),
            r'{{TOTAL_TESTS}}': str(total_reportable_tests),
            r'{{TESTS_WITH_FINDINGS}}': str(total_tests_with_findings),
            r'{{TESTS_WITHOUT_FINDINGS}}': str(total_tests_without_findings),
        }

        for p in doc.paragraphs:
            for r in p.runs:
                for pattern in handlebar_slugs.keys():
                    if re.search(pattern, r.text):
                        logger.debug('>> Replaced: {old} With: {new}'.format(
                            old=r.text.encode('utf-8'),
                            new=handlebar_slugs[pattern].encode('utf-8')
                            )
                        )
                        r.text = re.sub(pattern, handlebar_slugs[pattern], r.text)

    def get_or_create_mission_data_dir(mission_data_dir):
        if mission_data_dir is None :
            mission_data_dir = os.path.join(
                settings.BASE_DIR,
                'SUPPORTING_DATA_PACKAGE',
                str(mission.id) + "_" + str(now().strftime('%Y%m%d-%H%M%S')))
            if not os.path.isdir(mission_data_dir):
                os.makedirs(mission_data_dir)
        return mission_data_dir

    def add_to_data_dir(mission_data_dir, test_case_number, supporting_data):
        # If test case directory path is not created, create it
        path = os.path.join(mission_data_dir, str(test_case_number))

        # Shouldn't have race condition issues, but may need to add
        # try/catch if so later on
        if not os.path.isdir(path):
            os.makedirs(path)

        # Copy file to destination path
        shutil.copy(
            os.path.join(settings.MEDIA_ROOT, supporting_data.filename()),
            path
        )

    def prepend_classification(text):
        return '(' + system_classification_short + ') ' + text

    def portion_mark_and_insert(paragraphs, document):
        for paragraph in normalize_newlines(paragraphs).split('\n'):
            if len(paragraph) > 0:
                document.add_paragraph(prepend_classification(paragraph))

    # Load the template file to get the styles
    document = Document(settings.REPORT_TEMPLATE_PATH)

    # Get the table templates
    table_no_findings = document.tables[0]
    table_with_findings = document.tables[1]
    data_table = document.tables[2]

    # Set the classification legend color from the background color of the banner
    classification_style = document.styles['Table Classification']
    classification_font = classification_style.font

    # RGBColor doesn't handle shorthand hex codes, so let's just go ahead and expand it
    # if we come across a legacy or "misguided" entry
    if len(system_classification.background_color.hex_color_code) == 3:
        new_hex_code = ''
        for char in system_classification.background_color.hex_color_code:
            new_hex_code += char + char
        system_classification.background_color.hex_color_code = new_hex_code
        system_classification.background_color.save()

    if len(system_classification.text_color.hex_color_code) == 3:
        new_hex_code = ''
        for char in system_classification.text_color.hex_color_code:
            new_hex_code += char + char
        system_classification.text_color.hex_color_code = new_hex_code
        system_classification.text_color.save()

    classification_font.color.rgb = RGBColor.from_string(system_classification.get_report_label_color().hex_color_code)

    # Intro H1 and text
    document.add_heading('Introduction', level=1)
    portion_mark_and_insert(mission.introduction, document)

    # Scope H1 and text
    document.add_heading('Scope', level=1)
    portion_mark_and_insert(mission.scope, document)

    # Objectives H1 and text
    document.add_heading('Objectives', level=1)
    portion_mark_and_insert(mission.objectives, document)

    # Exec Summary H1 and text
    document.add_heading('Executive Summary', level=1)
    portion_mark_and_insert(mission.executive_summary, document)

    # Technical Assessment / Attack Architecture and text H1
    document.add_heading('Technical Assessment / Attack Architecture', level=1)
    portion_mark_and_insert(mission.technical_assessment_overview, document)

    # Technical Assessment / Test Cases and Results and loop
    document.add_heading('Technical Assessment / Test Cases and Results', level=1)

    # For each test, Test # - Objective  Attack Phase: H2

    """
    Note: If a test case is hidden from the report, the test_case number and the auto-generated paragraph
    number in Word won't align and it could cause confusion. The team may need to discuss how they want to
    handle this situation.
    """

    """
    WARNING

    Hidden test cases are NOT numbered in the report so the customer doesn't think something was
    accidentally left out. This will result in test case numbering differing between report and web when
    one or more test cases are hidden from the report.
    """

    test_case_number = 0
    tests_with_findings = 0
    tests_without_findings = 0

    # Let's just always craete a mission data dir when zipping attachments on report generation; this way
    # mission attachments will always return a zip file... it just may be empty - located here to account for times
    # when Get Attachments is actuated, but there are no test cases yet.
    if zip_attachments:
        mission_data_dir = get_or_create_mission_data_dir(mission_data_dir)

    for t in tests:

        if test_case_number > 0:
            document.add_page_break()

        test_case_number += 1

        if t.has_findings:
            test_title = "*"
            tests_with_findings += 1
        else:
            test_title = ""
            tests_without_findings += 1

        if t.enclave:
            test_title += "(%s) %s" % (
                t.enclave,
                t.test_objective
            )
        else:
            test_title += "%s" % (
                t.test_objective,
            )
        document.add_heading(test_title, level=2)

        # Duplicate one of the pre-made tables; if this is the last of a specific type of
        # test case (findings / no findings), use the cut operation to remove the blank
        # table.
        #TODO: convert to logging: print("T w/ F: {0}\nTotal: {1}\nT w/o F: {2}\nTotal: {3}".format(tests_with_findings, total_tests_with_findings, tests_without_findings, total_tests_without_findings))
        is_last_test_case = True if test_case_number == total_reportable_tests else False
        if t.has_findings:
            if tests_with_findings == total_tests_with_findings or is_last_test_case:
                table = copy_table(document, table_with_findings, cut=True)
            else:
                table = copy_table(document,table_with_findings, cut=False)
        else:
            if tests_without_findings == total_tests_without_findings or is_last_test_case:
                table = copy_table(document, table_no_findings, cut=True)
            else:
                table = copy_table(document,table_no_findings, cut=False)

        # Classification Marking - Top
        cell = table.cell(0, 0)
        get_cleared_paragraph(cell).text = system_classification_verbose

        # Classification Marking - Bottom
        cell = table.cell(16, 1)
        get_cleared_paragraph(cell).text = system_classification_verbose

        # Test Case Number (Table Header Row)
        cell = table.cell(1, 0)
        if mission.test_case_identifier:
            get_cleared_paragraph(cell).text = 'Test #{0}-{1}'.format(mission.test_case_identifier, test_case_number)
        else:
            get_cleared_paragraph(cell).text = 'Test #{0}'.format(test_case_number)

        row_number = 0

        #
        # Test Case Title (Table Header Row)
        #
        row_number += 1
        cell = table.cell(row_number, 1)
        get_cleared_paragraph(cell).text = t.test_objective

        #
        # Attack Phase / Type
        #
        row_number += 1

        include_attack_phase = False
        include_attack_type = False

        if mission.attack_phase_include_flag \
                and t.attack_phase_include_flag \
                and len(t.get_attack_phase_display())> 0:
            include_attack_phase = True

        if mission.attack_type_include_flag \
                and t.attack_type_include_flag \
                and len(t.attack_type) > 0:
            include_attack_type = True

        left_cell = table.cell(row_number, 0)
        right_cell = get_cleared_paragraph(table.cell(row_number, 1))

        if include_attack_phase or include_attack_type:
            if include_attack_phase and include_attack_type:
                # Table text in column 1 assumes both items are included already
                right_cell.text = ' - '.join([t.get_attack_phase_display(), t.attack_type])
            elif include_attack_phase:
                get_cleared_paragraph(left_cell).text = "Attack Phase:"
                right_cell.text = t.get_attack_phase_display()
            elif include_attack_type:
                get_cleared_paragraph(left_cell).text = "Attack Type:"
                right_cell.text = t.attack_type
        else:
            logger.debug('Removing Attack Phase/Type Row')
            remove_row(table, table.rows[row_number])
            row_number -= 1

        #
        # Assumptions
        #
        row_number += 1
        if mission.assumptions_include_flag and t.assumptions_include_flag:
            cell = get_cleared_paragraph(table.cell(row_number, 1))
            cell.text = standardize_report_output_field(t.assumptions)
        else:
            logger.debug('Removing Assumptions Row')
            remove_row(table, table.rows[row_number])
            row_number -= 1

        #
        # Description
        #
        row_number += 1
        if mission.test_description_include_flag and t.test_description_include_flag:
            cell = get_cleared_paragraph(table.cell(row_number, 1))
            if t.re_eval_test_case_number:
                cell.text = standardize_report_output_field('This is a reevaluation; reference previous test case #{0}\n\n{1}'.format(t.re_eval_test_case_number, t.test_description))
            else:
                cell.text = standardize_report_output_field(t.test_description)
        else:
            logger.debug('Removing Description Row')
            remove_row(table, table.rows[row_number])
            row_number -= 1

        #
        # Findings
        #
        row_number += 1
        if mission.findings_include_flag and t.findings_include_flag:
            cell = get_cleared_paragraph(table.cell(row_number, 1))
            cell.text = standardize_report_output_field(t.findings)
        else:
            logger.debug('Removing Findings Row')
            remove_row(table, table.rows[row_number])
            row_number -= 1

        #
        # Mitigations
        #
        row_number += 1
        if mission.mitigation_include_flag and t.mitigation_include_flag:
            cell = get_cleared_paragraph(table.cell(row_number, 1))
            cell.text = standardize_report_output_field(t.mitigation)
        else:
            logger.debug('Removing Mitigations Row')
            remove_row(table, table.rows[row_number])
            row_number -= 1

        #
        # Tools
        #
        row_number += 1
        if mission.tools_used_include_flag and t.tools_used_include_flag:
            cell = get_cleared_paragraph(table.cell(row_number, 1))
            cell.text = standardize_report_output_field(t.tools_used)
        else:
            logger.debug('Removing Tools Row')
            remove_row(table, table.rows[row_number])
            row_number -= 1

        #
        # Commands / Syntax
        #
        row_number += 1
        if mission.command_syntax_include_flag and t.command_syntax_include_flag:
            cell = get_cleared_paragraph(table.cell(row_number, 1))
            cell.text = standardize_report_output_field(t.command_syntax)
        else:
            logger.debug('Removing Commands/Syntax Row')
            remove_row(table, table.rows[row_number])
            row_number -= 1

        #
        # Targets
        #
        row_number += 1
        if mission.targets_include_flag and t.targets_include_flag:
            cell = get_cleared_paragraph(table.cell(row_number, 1))
            cell.text = '\n'.join([str(x) for x in t.target_hosts.all()])
        else:
            logger.debug('Removing Targets Row')
            remove_row(table, table.rows[row_number])
            row_number -= 1

        #
        # Sources
        #
        row_number += 1
        if mission.sources_include_flag and t.sources_include_flag:
            cell = get_cleared_paragraph(table.cell(row_number, 1))
            cell.text = '\n'.join([str(x) for x in t.source_hosts.all()])

        else:
            logger.debug('Removing Sources Row')
            remove_row(table, table.rows[row_number])
            row_number -= 1

        #
        # Date / Time
        #
        row_number += 1
        if mission.attack_time_date_include_flag and t.attack_time_date_include_flag:
            cell = get_cleared_paragraph(table.cell(row_number, 1))
            cell.text = localtime(t.attack_time_date).strftime('%b %d, %Y @ %I:%M %p')
        else:
            logger.debug('Removing Date/Time Row')
            remove_row(table, table.rows[row_number])
            row_number -= 1

        #
        # Side Effects
        #
        row_number += 1
        if mission.attack_side_effects_include_flag and t.attack_side_effects_include_flag:
            cell = get_cleared_paragraph(table.cell(row_number, 1))
            cell.text = standardize_report_output_field(t.attack_side_effects)
        else:
            logger.debug('Removing Side Effects Row')
            remove_row(table, table.rows[row_number])
            row_number -= 1

        #
        # Details
        #
        row_number += 1
        if mission.test_result_observation_include_flag and t.test_result_observation_include_flag:
            cell = get_cleared_paragraph(table.cell(row_number, 1))
            cell.text = standardize_report_output_field(t.test_result_observation)
        else:
            logger.debug('Removing Details Row')
            remove_row(table, table.rows[row_number])
            row_number -= 1

        #
        # Supporting Data
        #
        row_number += 1
        supporting_data_cell = None
        supporting_data_row = None
        if mission.supporting_data_include_flag:
            supporting_data_cell = get_cleared_paragraph(table.cell(row_number, 1))
            supporting_data_row = table.rows[row_number]
        else:
            logger.debug('Removing Supporting Data Row')
            remove_row(table, table.rows[row_number])
            row_number -= 1

        #
        # Notes - Used for post report generation notes / customer use
        #
        row_number += 1
        if False: #TODO: add mission-level toggle
            logger.debug('Removing Customer Notes Row')
            remove_row(table, table.rows[row_number])
            row_number -= 1

        # Attachments section

        supporting_data_cell_items = []

        if mission.supporting_data_include_flag:
            my_data = TestSortingHelper.get_ordered_supporting_data(
                test_detail_id=t.id,
                reportable_supporting_data_only=True)

            if len(my_data) > 0:

                is_first_screenshot = True

                for d in my_data:
                    allowed_image_types = [
                        'gif',
                        'tiff',
                        'jpeg',
                        'bmp',
                        'png',
                    ]
                    try:
                        file_path = os.path.join(settings.MEDIA_ROOT, d.filename())
                        logger.debug('>> Beginning processing of {} at {}.'
                                     .format(
                                        d.filename(),
                                        file_path,
                                        ))

                        if imghdr.what(file_path) not in allowed_image_types:
                            raise UnrecognizedImageError('File type is not in the allowed image types. '
                                                         'Handling as non-image.')

                        if is_first_screenshot:
                            document.add_heading('Screenshots / Diagrams', level=3)
                            logger.debug('This is the first screenshot of this test case.')
                            is_first_screenshot = False

                        image_table = copy_table(document, data_table)
                        logger.debug('Creating a new image table.')
                        document.add_paragraph()

                        # Classification Marking - Top
                        cell = image_table.cell(0, 0)
                        get_cleared_paragraph(cell).text = system_classification_verbose

                        # Classification Marking - Bottom
                        cell = image_table.cell(2, 0)
                        get_cleared_paragraph(cell).text = system_classification_verbose

                        content_cell = image_table.cell(1, 0)

                        get_cleared_paragraph(content_cell).add_run().add_picture(d.test_file, width=Inches(5))
                        content_cell.paragraphs[0].add_run("\r" + d.caption)

                    except UnrecognizedImageError as e:
                        logger.debug('>> Attachment {attachment_name} not recognized as an image; adding as file.'
                                     .format(attachment_name=d.filename()))
                        supporting_data_cell_items.append('- {filename}: {caption}'.format(
                            filename=d.filename(),
                            caption=d.caption,
                        ))

                        if zip_attachments:
                            add_to_data_dir(mission_data_dir, test_case_number, d)
                    except (InvalidImageStreamError,
                            UnexpectedEndOfFileError) as e:
                        logger.warning('>> Attempting to add {file_name} to the report output resulted in an error: '
                                       '\n{trace}'.format(file_name=d.filename, trace=traceback.format_exc(10)))
                    except OSError as e:
                        logger.warning('>> Attempting to add {file_name} to the report output resulted in an error: '
                                       '\n{trace}'.format(file_name=d.filename, trace=traceback.format_exc(10)))
                        try:
                            if not d.test_file.closed:
                                d.test_file.close()
                        except IOError as e:
                            logger.warning('>> Attempting to close {file_name} resulted in an error: '
                                           '\n{trace}'.format(file_name=d.filename, trace=traceback.format_exc(10)))
                            pass

                if len(supporting_data_cell_items) > 0:
                    logger.debug('There are {} data cell items for TC {}.'.format(
                        len(supporting_data_cell_items),
                        t.id)
                    )
                    supporting_data_cell.text = '\n'.join(supporting_data_cell_items)

        if len(supporting_data_cell_items) == 0:
            logger.debug('There are no supporting_data_cell_items; removing the supporting data row.')
            remove_row(table, supporting_data_row)

    # Conclusion H1 and text
    document.add_heading('Conclusion', level=1)
    portion_mark_and_insert(mission.conclusion, document)

    data_table.cell(0, 0).text = ""
    get_cleared_paragraph(data_table.cell(1, 0)).text = "This table is used during report generation and can be deleted in the final report output."
    data_table.cell(2, 0).text = ""

    # Replace document slugs
    replace_document_slugs(document)

    if zip_attachments:
        zip_file = shutil.make_archive(mission_data_dir, 'zip', mission_data_dir)
        with open(mission_data_dir + '.zip', 'rb') as f:
            return io.BytesIO(f.read())

    else:
        my_stream = StringIO()
        document.save(my_stream)
        return my_stream class Mission(models.Model):

    @staticmethod
    def get_default_text(file_name):
        TEMPALTE_DIR = os.path.join(settings.BASE_DIR, 'templates')
        with open(os.path.join(TEMPALTE_DIR, file_name), 'r') as template:
            output = join_as_compacted_paragraphs(template.readlines())
            return output

    # Model Fields

    mission_name = models.CharField(
        max_length=255,
        verbose_name="Mission Name",
    )

    mission_number = models.CharField(
        max_length=5,
        verbose_name="Mission Number",
    )

    test_case_identifier = models.CharField(
        max_length=20,
        blank=True,
        default="",
        verbose_name="Test Case Identifier",
    )

    business_area = models.ForeignKey(
        'BusinessArea',
        verbose_name="Business Area",
    )

    introduction = models.TextField(
        blank=True,
        verbose_name="Introduction",
        default=introduction_default,
    )

    executive_summary = models.TextField(
        blank=True,
        verbose_name="Executive Summary",
        default=executive_summary_default
    )

    scope = models.TextField(
        blank=True,
        verbose_name="Scope",
        default=scope_default
    )

    objectives = models.TextField(
        blank=True,
        verbose_name="Objectives",
        default=objectives_default
    )

    technical_assessment_overview = models.TextField(
        blank=True,
        verbose_name="Technical Assessment / Attack Architecture Overview",
        default=technical_assessment_overview_default
    )

    conclusion = models.TextField(
        blank=True,
        verbose_name="Conclusion",
        default=conclusion_default
    )

    # Mission-wide reporting include flags
    attack_phase_include_flag = models.BooleanField(
        default=True,
        verbose_name="Mission Option: Include Attack Phases in report?",
    )

    attack_type_include_flag = models.BooleanField(
        default=True,
        verbose_name="Mission Option: Include Attack Types in report?",
    )

    assumptions_include_flag = models.BooleanField(
        default=True,
        verbose_name="Mission Option: Include Assumptions in report?",
    )

    test_description_include_flag = models.BooleanField(
        default=True,
        verbose_name="Mission Option: Include Test Descriptions in report?",
    )

    findings_include_flag = models.BooleanField(
        default=True,
        verbose_name="Mission Option: Include Findings in report?",
    )

    mitigation_include_flag = models.BooleanField(
        default=True,
        verbose_name="Mission Option: Include Mitigations in report?",
    )

    tools_used_include_flag = models.BooleanField(
        default=True,
        verbose_name="Mission Option: Include Tools Used in report?",
    )

    command_syntax_include_flag = models.BooleanField(
        default=True,
        verbose_name="Mission Option: Include Command Syntaxes in report?",
    )

    targets_include_flag = models.BooleanField(
        default=True,
        verbose_name="Mission Option: Include Attack Targets in report?",
    )

    sources_include_flag = models.BooleanField(
        default=True,
        verbose_name="Mission Option: Include Attack Sources in report?",
    )

    attack_time_date_include_flag = models.BooleanField(
        default=True,
        verbose_name="Mission Option: Include Attack Times in report?",
    )

    attack_side_effects_include_flag = models.BooleanField(
        default=True,
        verbose_name="Mission Option: Include Attack Side Effects in report?",
    )

    test_result_observation_include_flag = models.BooleanField(
        default=True,
        verbose_name="Mission Option: Include Test Details in report?",
    )

    supporting_data_include_flag = models.BooleanField(
        default=True,
        verbose_name="Mission Option: Include Supporting Data Information in report?"
    )

    customer_notes_include_flag = models.BooleanField(
        default=True,
        verbose_name="Mission Option: Include customer notes section in report?"
    )

    testdetail_sort_order = models.TextField(
        blank=True,
        default="[]"
    )

    def __str__(self):
        return "%s (%s)" % (self.mission_name, self.mission_number) class TestDetail(models.Model):
    mission = models.ForeignKey(Mission, verbose_name="Mission")

    test_number = models.IntegerField(
        blank=False,
        verbose_name="Test Number",
        default=0
    )

    test_case_include_flag = models.BooleanField(
        default=True,
        verbose_name="Include Test Case in report?"
    )

    TEST_CASE_STATUSES = (
        ('NEW', 'Not started'),
        ('IN_WORK', 'In work'),
        ('REVIEW', 'Ready for review'),
        ('FINAL', 'Approved / Final'),
    )

    test_case_status = models.CharField(
        choices=TEST_CASE_STATUSES,
        max_length=100,
        verbose_name="Test Case Status",
        default='NEW',
    )

    enclave = models.CharField(
        blank=True,
        max_length=100,
        verbose_name='Enclave Test Executed From'
    )

    test_objective = models.CharField(
        blank=False,
        max_length=255,
        verbose_name="Test Objective / Title",
        help_text="Brief objective (ex.Port Scan against xyz)",
    )

    # Note: Attack Phases based on the Lockheed Martin Kill Chain(R)
    ATTACK_PHASES = (
        ('RECON', 'Reconnaissance'),
        ('WEP', 'Weaponization'),
        ('DEL', 'Delivery'),
        ('EXP', 'Exploitation'),
        ('INS', 'Installation'),
        ('C2', 'Command & Control'),
        ('AOO', 'Actions on Objectives'),
    )

    attack_phase = models.CharField(
        choices=ATTACK_PHASES,
        max_length=20,
        verbose_name="Attack Phase",
    )

    attack_phase_include_flag = models.BooleanField(
        default=True,
        verbose_name="Include Attack Phase in report?",
    )

    attack_type = models.CharField(
        blank=True,
        max_length=255,
        verbose_name="Attack Type",
        help_text="Example: SYN flood, UDP flood, malformed packets, web, fuzz, etc.",
    )

    attack_type_include_flag = models.BooleanField(
        default=True,
        verbose_name="Include Attack Type in report?",
    )

    assumptions = models.TextField(
        blank=True,
        verbose_name="Assumptions",
        help_text="Example: Attacker has a presence in xyz segment, etc."
    )

    assumptions_include_flag = models.BooleanField(
        default=True,
        verbose_name="Include Assumptions in report?",
    )

    test_description = models.TextField(
        blank=True,
        verbose_name="Description",
        help_text="Describe test case to be performed, what is the objective of this test case (Is it to deny, disrupt, penetrate, modify etc.)"
    )

    test_description_include_flag = models.BooleanField(
        default=True,
        verbose_name="Include Test Description in report?",
    )

    source_hosts = models.ManyToManyField(
        'Host',
        related_name='source_set',
    )

    sources_include_flag = models.BooleanField(
        default=True,
        verbose_name="Include Attack Sources in report?",
    )

    target_hosts = models.ManyToManyField(
        'Host',
        related_name='target_set',
    )

    targets_include_flag = models.BooleanField(
        default=True,
        verbose_name="Include Attack Targets in report?",
    )

    attack_time_date = models.DateTimeField(
        blank=True,
        default=timezone.now,
        verbose_name="Attack Date / Time",
        help_text="Date/time attack was launched"
    )

    attack_time_date_include_flag = models.BooleanField(
        default=True,
        verbose_name="Include Attack Time in report?",
    )

    tools_used = models.TextField(
        blank=True,
        verbose_name="Tools Used",
        help_text="Example: Burp Suite, Wireshark, NMap, etc."
    )

    tools_used_include_flag = models.BooleanField(
        default=True,
        verbose_name="Include Tools Used in report?",
    )

    command_syntax = models.TextField(
        blank=True,
        verbose_name="Command/Syntax",
        help_text="Include sample command/syntax used if possible. If a script was used/created, what commands will run it?"
    )

    command_syntax_include_flag = models.BooleanField(
        default=True,
        verbose_name="Include Command Syntax in report?",
    )

    test_result_observation = models.TextField(
        blank=True,
        verbose_name="Test Result Details",
        help_text="Example: An average of 8756 SYN-ACK/sec were received at the attack laptop over the 30 second attack "
                  "period. Plots of traffic flows showed degradation of all flow performance during time 5s – 40s "
                  "after which they recovered."
    )

    test_result_observation_include_flag = models.BooleanField(
        default=True,
        verbose_name="Include Test Result in report?",
    )

    attack_side_effects = models.TextField(
        blank=True,
        verbose_name="Attack Side Effects",
        help_text="List any observed side effects, if any. Example: Firewall X froze and subsequently crashed."
    )

    attack_side_effects_include_flag = models.BooleanField(
        default=True,
        verbose_name="Include Attack Side Effects in report?",
    )

    EXECUTION_STATUS_OPTIONS = (
        ('N', 'Not Run'),
        ('R', 'Run'),
        ('C', 'Cancelled'),
        ('NA', 'N/A'),
    )

    execution_status = models.CharField(
        choices=EXECUTION_STATUS_OPTIONS,
        max_length=2,
        verbose_name="Execution Status",
        default='N',
        help_text="Execution status of the test case."
    )

    has_findings = models.BooleanField(
        default=False,
    )

    findings = models.TextField(
        blank=True,
        verbose_name="Findings",
        help_text="Document the specific finding related to this test or against the main test objectives if applicable"
    )

    findings_include_flag = models.BooleanField(
        default=True,
        verbose_name="Include Findings in report?",
    )

    mitigation = models.TextField(
        blank=True,
        verbose_name="Mitigation",
        help_text="Example: Configure xyz, implement xyz, etc."
    )

    mitigation_include_flag = models.BooleanField(
        default=True,
        verbose_name="Include Mitigations in report?",
    )

    point_of_contact = models.CharField(
        max_length=20,
        blank=True,
        default="",
        verbose_name="POC",
        help_text="Individual working or most familiar with this test case."
    )

    re_eval_test_case_number = models.CharField(
        max_length=25,
        blank=True,
        default="",
        verbose_name="Re-Evaluate Test Case #",
        help_text="Adds previous test case reference to description in report."
    )

    supporting_data_sort_order = models.TextField(
        blank=True,
        default="[]"
    )

    def count_of_supporting_data(self):
        return len(SupportingData.objects.filter(test_detail=self.pk))

    def __str__(self):
        return "%s (%s)" % (self.test_objective, self.id)

    def save(self, *args, **kwargs):
        self.has_findings = True if len(self.findings) > 0 else False
        return super(TestDetail, self).save(*args, **kwargs) class SupportingData(models.Model):
    test_detail = models.ForeignKey(TestDetail, verbose_name="Test Details")

    caption = models.TextField(
        blank=True,
        verbose_name="Caption",
    )

    include_flag = models.BooleanField(
        default=True,
        verbose_name="Include attachment in report"
    )

    test_file = models.FileField(
        verbose_name="Supporting Data"
    )

    def filename(self):
        return os.path.basename(self.test_file.name)

    def get_absolute_url(self):
        return reverse_lazy('data-view', {'supportingdata': self.pk}) class DARTDynamicSettings(models.Model):
    system_classification = models.ForeignKey(
        'ClassificationLegend',
    )

    host_output_format = models.CharField(
        max_length=50,
        default='{ip} ({name})',
        help_text='Use "{ip}" and "{name}" to specify how you want hosts to be displayed.',
        validators=[validate_host_format_string]
    )

    # Since we're treating Dynamic Settings as a singleton,
    # override the default manager to enforce this
    objects = DARTDynamicSettingsManager() class Host(models.Model):
    mission = models.ForeignKey(
        Mission
    )

    host_name = models.CharField(
        blank=True,
        default="",
        max_length=100,
    )

    ip_address = models.GenericIPAddressField(
        blank=True,
        null=True,
    )

    is_no_hit = models.BooleanField(
        default=False,
    )

    @staticmethod
    def get_host_output_format_string():
        format_string = str(DARTDynamicSettings.objects.get_as_object().host_output_format)
        return format_string

    def __str__(self):
        format_string = cache.get('host_output_format_string')
        if format_string is None:
            cache.set('host_output_format_string', Host.get_host_output_format_string(), 300)
            format_string = cache.get('host_output_format_string')
        return format_string.format(name=self.host_name, ip=self.ip_address)

    def get_absolute_url(self):
        return class BusinessArea(models.Model):
    name = models.TextField()

    def __str__(self):
        return self.name class ClassificationLegend(models.Model):
    verbose_legend = models.CharField(
        blank=False,
        max_length=200,
        default="",
    )

    short_legend = models.CharField(
        blank=False,
        max_length=100,
        default="",
    )

    text_color = models.ForeignKey(
        'Color',
        related_name='classificationlegend_text_set',
    )

    background_color = models.ForeignKey(
        'Color',
        related_name='classificationlegend_background_set',
    )

    REPORT_LABEL_COLOR_OPTIONS = (
        ('T', 'Text Color'),
        ('B', 'Back Color'),
    )

    report_label_color_selection = models.CharField(
        choices=REPORT_LABEL_COLOR_OPTIONS,
        max_length=1,
        default='B',
        blank=False,
    )

    def get_report_label_color(self):
        if self.report_label_color_selection == 'T':
            return self.text_color
        else:
            return self.background_color

    def __str__(self):
        return '{0.verbose_legend} ({0.short_legend})'.format(self) class Color(models.Model):
    display_text = models.CharField(
        blank=False,
        max_length=30,
        default="",
    )

    hex_color_code = models.CharField(
        blank=False,
        max_length=6,
        default="",
    )

    def __str__(self):
        return '{0.display_text}'.format(self)
import json
import logging
import time
from calendar import timegm
from httplib import BAD_REQUEST, NOT_ACCEPTABLE
from itertools import chain
from django.contrib import messages
from django.contrib.auth import get_user_model
from django.core.cache import cache
from django.core.cache.utils import make_template_fragment_key
from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage
from django.core.signing import Signer
from django.core.urlresolvers import reverse, reverse_lazy, resolve
from django.http import HttpResponse
from django.http.response import JsonResponse
from django.shortcuts import redirect
from django.utils.html import conditional_escape
from django.utils.timezone import now
from django.views.decorators.http import require_http_methods
from django.views.generic import TemplateView, ListView, CreateView, UpdateView, View, DeleteView
from .extras.helpers.analytics import MissionAnalytics
from .extras.helpers.sorters import TestSortingHelper
from .extras.utils import ReturnStatus, generate_report_or_attachments
from .models import Mission, TestDetail, SupportingData, DARTDynamicSettings, Host, BusinessArea, \
    ClassificationLegend, Color
        paginator = Paginator(missions, 10)
        page = self.request.GET.get('page')
        try:
            show_missions = paginator.page(page)
        except PageNotAnInteger:
            # If page is not an integer, deliver first page.
            show_missions = paginator.page(1)
        except EmptyPage:
            # If page is out of range (e.g. 9999), deliver last page of results.
            show_missions = paginator.page(paginator.num_pages)
        context['missions'] = show_missions
        return context


class CreateMissionView(CreateView):
    model = Mission
    template_name = 'edit_mission.html'
    fields = [
        'mission_name',
        'mission_number',
        'test_case_identifier',
        'business_area',
        'introduction',
        'scope',
        'objectives',
        'executive_summary',
        'technical_assessment_overview',
        'conclusion',
        'attack_phase_include_flag',
        'attack_type_include_flag',
        'assumptions_include_flag',
        'test_description_include_flag',
        'findings_include_flag',
        'mitigation_include_flag',
        'tools_used_include_flag',
        'command_syntax_include_flag',
        'targets_include_flag',
        'sources_include_flag',
        'attack_time_date_include_flag',
        'attack_side_effects_include_flag',
        'test_result_observation_include_flag',
        'supporting_data_include_flag',
        'customer_notes_include_flag',
    ]

    def get_success_url(self):
        logger.debug('Created mission {mission_id}'.format(mission_id=self.object.id))
        return reverse('missions-list')

    def get_context_data(self, **kwargs):
        logger.debug('GET: CreateMissionView')
        context = super(CreateMissionView, self).get_context_data(**kwargs)
        context['action'] = reverse('missions-new')
        return context


class EditMissionView(UpdateView):
    model = Mission
    template_name = 'edit_mission.html'
    fields = [
        'mission_name',
        'mission_number',
        'test_case_identifier',
        'business_area',
        'introduction',
        'scope',
        'objectives',
        'executive_summary',
        'technical_assessment_overview',
        'conclusion',
        'attack_phase_include_flag',
        'attack_type_include_flag',
        'assumptions_include_flag',
        'test_description_include_flag',
        'findings_include_flag',
        'mitigation_include_flag',
        'tools_used_include_flag',
        'command_syntax_include_flag',
        'targets_include_flag',
        'sources_include_flag',
        'attack_time_date_include_flag',
        'attack_side_effects_include_flag',
        'test_result_observation_include_flag',
        'supporting_data_include_flag',
        'customer_notes_include_flag',
    ]

    def get_success_url(self):
        logger.debug('POST: EditMissionView (Saved {mission_id})'.format(mission_id=self.object.id))
        return reverse('missions-list')

    def get_context_data(self, **kwargs):
        logger.debug('GET: EditMissionView (Edit {mission_id})'.format(mission_id=self.get_object().id))
        context = super(EditMissionView, self).get_context_data(**kwargs)
        context['action'] = reverse('missions-edit', kwargs={'pk': self.get_object().id})
        return context


class DeleteMissionView(DeleteView):
    model = Mission
    template_name = 'delete_mission.html'

    def get_context_data(self, **kwargs):
        logger.debug('GET: DeleteMissionView (Confirm delete {mission_id})'
                     .format(mission_id=self.object.id))
        return super(DeleteMissionView, self).get_context_data(**kwargs)

    def get_success_url(self):
        logger.debug('POST: DeleteMisisonView (Deleted {mission_id})'.format(mission_id=self.object.id))
        return reverse('missions-list')


class ReportMissionView(View):

    def get(self, request, *args, **kwargs):

        mission_id = kwargs.get('mission')

        logger.debug('GET: ReportMissionView ({mission_id})'.format(mission_id=mission_id))


        io_stream = generate_report_or_attachments(mission_id, zip_attachments=False)