From b81479ccb17a87b560787c7173e66a092c3077de Mon Sep 17 00:00:00 2001 From: "Jeffrey A. Clark (Alex)" Date: Thu, 2 May 2024 19:32:55 -0400 Subject: [PATCH 1/5] Add/update project-makefile files --- Makefile | 2545 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 2545 insertions(+) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..7181ec1 --- /dev/null +++ b/Makefile @@ -0,0 +1,2545 @@ +# Project Makefile +# +# A generic makefile for projects. +# +# https://github.com/project-makefile/project-makefile + +# -------------------------------------------------------------------------------- +# Variables (override) +# -------------------------------------------------------------------------------- + +.DEFAULT_GOAL := git-commit-push + +UNAME := $(shell uname) +RANDIR := $(shell openssl rand -base64 12 | sed 's/\///g') +TMPDIR := $(shell mktemp -d) + +PROJECT_EMAIL := aclark@aclark.net +PROJECT_MAKEFILE := project.mk +PROJECT_NAME = project-makefile +PROJECT_DIRS = backend contactpage home privacy siteuser + +WAGTAIL_CLEAN_DIRS = home search backend sitepage siteuser privacy frontend contactpage modelformtest +WAGTAIL_CLEAN_FILES = README.rst .dockerignore Dockerfile manage.py requirements.txt + +REVIEW_EDITOR = subl + +GIT_BRANCHES = $(shell git branch -a | grep remote | grep -v HEAD | grep -v main |\ + grep -v master) +GIT_MESSAGE = "Update $(PROJECT_NAME)" +GIT_COMMIT = git commit -a -m $(GIT_MESSAGE) +GIT_PUSH = git push +GIT_PUSH_FORCE = git push --force-with-lease + +GET_DATABASE_URL = eb ssh -c "source /opt/elasticbeanstalk/deployment/custom_env_var;\ + env | grep DATABASE_URL" +DATABASE_AWK = awk -F\= '{print $$2}' +DATABASE_HOST = $(shell $(GET_DATABASE_URL) | $(DATABASE_AWK) |\ + python -c 'import dj_database_url; url = input(); url = dj_database_url.parse(url); print(url["HOST"])') +DATABASE_NAME = $(shell $(GET_DATABASE_URL) | $(DATABASE_AWK) |\ + python -c 'import dj_database_url; url = input(); url = dj_database_url.parse(url); print(url["NAME"])') +DATABASE_PASS = $(shell $(GET_DATABASE_URL) | $(DATABASE_AWK) |\ + python -c 'import dj_database_url; url = input(); url = dj_database_url.parse(url); print(url["PASSWORD"])') +DATABASE_USER = $(shell $(GET_DATABASE_URL) | $(DATABASE_AWK) |\ + python -c 'import dj_database_url; url = input(); url = dj_database_url.parse(url); print(url["USER"])') + +ENV_NAME ?= $(PROJECT_NAME)-$(GIT_BRANCH)-$(GIT_REV) +INSTANCE_MAX ?= 1 +INSTANCE_MIN ?= 1 +INSTANCE_TYPE ?= t4g.small +INSTANCE_PROFILE ?= aws-elasticbeanstalk-ec2-role +PLATFORM ?= "Python 3.11 running on 64bit Amazon Linux 2023" +LB_TYPE ?= application + +ifneq ($(wildcard $(PROJECT_MAKEFILE)),) + include $(PROJECT_MAKEFILE) +endif + +# -------------------------------------------------------------------------------- +# Variables (no override) +# -------------------------------------------------------------------------------- + +GIT_REV := $(shell git rev-parse --short HEAD) +GIT_BRANCH := $(shell git branch --show-current) + +ADD_DIR := mkdir -pv +ADD_FILE := touch +COPY_DIR := cp -rv +COPY_FILE := cp -v +DEL_DIR := rm -rv +DEL_FILE := rm -v +GIT_ADD := -git add + +ENSURE_PIP := python -m ensurepip + +# -------------------------------------------------------------------------------- +# Multi-line variables +# -------------------------------------------------------------------------------- + +define ALLAUTH_LAYOUT_BASE +{% extends 'base.html' %} +endef + +define AUTHENTICATION_BACKENDS +AUTHENTICATION_BACKENDS = [ + 'django.contrib.auth.backends.ModelBackend', + 'allauth.account.auth_backends.AuthenticationBackend', +] +endef + +define BABELRC +{ + "presets": [ + [ + "@babel/preset-react", + ], + [ + "@babel/preset-env", + { + "useBuiltIns": "usage", + "corejs": "3.0.0" + } + ] + ], + "plugins": [ + "@babel/plugin-syntax-dynamic-import", + "@babel/plugin-transform-class-properties" + ] +} +endef + +define BASE_TEMPLATE +{% load static wagtailcore_tags wagtailuserbar webpack_loader %} + + + + + + + {% block title %} + {% if page.seo_title %}{{ page.seo_title }}{% else %}{{ page.title }}{% endif %} + {% endblock %} + {% block title_suffix %} + {% wagtail_site as current_site %} + {% if current_site and current_site.site_name %}- {{ current_site.site_name }}{% endif %} + {% endblock %} + + {% if page.search_description %} + + {% endif %} + + + {# Force all links in the live preview panel to be opened in a new tab #} + {% if request.in_preview_panel %} + + {% endif %} + + {% stylesheet_pack 'app' %} + + {% block extra_css %} + {# Override this in templates to add extra stylesheets #} + {% endblock %} + + + {% include 'favicon.html' %} + {% csrf_token %} + + +
+ {% wagtailuserbar %} +
+ {% include 'header.html' %} + {% if messages %} +
+ {% for message in messages %} + + {% endfor %} +
+ {% endif %} +
+ {% block content %}{% endblock %} +
+
+ {% include 'footer.html' %} + {% include 'offcanvas.html' %} + {% javascript_pack 'app' %} + {% block extra_js %} + {# Override this in templates to add extra javascript #} + {% endblock %} + + +endef + + +define BLOCK_CAROUSEL + +endef + +define BLOCK_MARKETING +{% load wagtailcore_tags %} +
+ {% if block.value.images.0 %} + {% include 'blocks/carousel_block.html' %} + {% else %} + {{ self.title }} + {{ self.content }} + {% endif %} +
+endef + +define CONTACT_PAGE_TEST +from wagtail.models import Page, Site +from wagtail.rich_text import RichText +from wagtail.test.utils import WagtailPageTestCase + +from home.models import HomePage +from contactpage.models import ContactPage + + +class ContactPageTest(WagtailPageTestCase): + @classmethod + def setUpTestData(cls): + root = Page.get_first_root_node() + Site.objects.create( + hostname="testserver", + root_page=root, + is_default_site=True, + site_name="testserver", + ) + home = HomePage(title="Home") + root.add_child(instance=home) + cls.page = ContactPage( + title="Contact Us", + slug="contact-us", + ) + home.add_child(instance=cls.page) + + def test_get(self): + response = self.client.get(self.page.url) + self.assertEqual(response.status_code, 200) +endef + +define COMPONENT_CLOCK +// Via ChatGPT +import React, { useState, useEffect, useCallback, useRef } from 'react'; +import PropTypes from 'prop-types'; + +const Clock = ({ color = '#fff' }) => { + const [date, setDate] = useState(new Date()); + const [blink, setBlink] = useState(true); + const timerID = useRef(); + + const tick = useCallback(() => { + setDate(new Date()); + setBlink(prevBlink => !prevBlink); + }, []); + + useEffect(() => { + timerID.current = setInterval(() => tick(), 1000); + + // Return a cleanup function to be run on component unmount + return () => clearInterval(timerID.current); + }, [tick]); + + const formattedDate = date.toLocaleDateString(undefined, { + weekday: 'short', + year: 'numeric', + month: 'short', + day: 'numeric', + }); + + const formattedTime = date.toLocaleTimeString(undefined, { + hour: 'numeric', + minute: 'numeric', + }); + + return ( + <> +
{formattedDate} {formattedTime}
+ + ); +}; + +Clock.propTypes = { + color: PropTypes.string, +}; + +export default Clock; +endef + +define COMPONENT_ERROR +import { Component } from 'react'; +import PropTypes from 'prop-types'; + +class ErrorBoundary extends Component { + constructor (props) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError () { + return { hasError: true }; + } + + componentDidCatch (error, info) { + const { onError } = this.props; + console.error(error); + onError && onError(error, info); + } + + render () { + const { children = null } = this.props; + const { hasError } = this.state; + + return hasError ? null : children; + } +} + +ErrorBoundary.propTypes = { + onError: PropTypes.func, + children: PropTypes.node, +}; + +export default ErrorBoundary; +endef + +define COMPONENT_USER_MENU +// UserMenu.js +import React from 'react'; +import PropTypes from 'prop-types'; + +function handleLogout() { + window.location.href = '/accounts/logout'; +} + +const UserMenu = ({ isAuthenticated, isSuperuser, textColor }) => { + return ( +
+ {isAuthenticated ? ( +
  • + + +
  • + ) : ( +
  • + +
  • + )} +
    + ); +}; + +UserMenu.propTypes = { + isAuthenticated: PropTypes.bool.isRequired, + isSuperuser: PropTypes.bool.isRequired, + textColor: PropTypes.string, +}; + +export default UserMenu; +endef + + +define CONTACT_PAGE_TEMPLATE +{% extends 'base.html' %} +{% load crispy_forms_tags static wagtailcore_tags %} +{% block content %} +

    {{ page.title }}

    + {{ page.intro|richtext }} +
    + {% csrf_token %} + {{ form.as_p }} + +
    +{% endblock %} +endef + +define CONTACT_PAGE_TEST +from django.test import TestCase +from wagtail.test.utils import WagtailPageTestCase +from wagtail.models import Page + +from contactpage.models import ContactPage, FormField + +class ContactPageTest(TestCase, WagtailPageTestCase): + def test_contact_page_creation(self): + # Create a ContactPage instance + contact_page = ContactPage( + title='Contact', + intro='Welcome to our contact page!', + thank_you_text='Thank you for reaching out.' + ) + + # Save the ContactPage instance + self.assertEqual(contact_page.save_revision().publish().get_latest_revision_as_page(), contact_page) + + def test_form_field_creation(self): + # Create a ContactPage instance + contact_page = ContactPage( + title='Contact', + intro='Welcome to our contact page!', + thank_you_text='Thank you for reaching out.' + ) + # Save the ContactPage instance + contact_page_revision = contact_page.save_revision() + contact_page_revision.publish() + + # Create a FormField associated with the ContactPage + form_field = FormField( + page=contact_page, + label='Your Name', + field_type='singleline', + required=True + ) + form_field.save() + + # Retrieve the ContactPage from the database + contact_page_from_db = Page.objects.get(id=contact_page.id).specific + + # Check if the FormField is associated with the ContactPage + self.assertEqual(contact_page_from_db.form_fields.first(), form_field) + + def test_contact_page_form_submission(self): + # Create a ContactPage instance + contact_page = ContactPage( + title='Contact', + intro='Welcome to our contact page!', + thank_you_text='Thank you for reaching out.' + ) + # Save the ContactPage instance + contact_page_revision = contact_page.save_revision() + contact_page_revision.publish() + + # Simulate a form submission + form_data = { + 'your_name': 'John Doe', + # Add other form fields as needed + } + + response = self.client.post(contact_page.url, form_data) + + # Check if the form submission is successful (assuming a 302 redirect) + self.assertEqual(response.status_code, 302) + + # You may add more assertions based on your specific requirements +endef + +define CONTACT_PAGE_MODEL +from django.db import models +from modelcluster.fields import ParentalKey +from wagtail.admin.panels import ( + FieldPanel, FieldRowPanel, + InlinePanel, MultiFieldPanel +) +from wagtail.fields import RichTextField +from wagtail.contrib.forms.models import AbstractEmailForm, AbstractFormField + + +class FormField(AbstractFormField): + page = ParentalKey('ContactPage', on_delete=models.CASCADE, related_name='form_fields') + + +class ContactPage(AbstractEmailForm): + intro = RichTextField(blank=True) + thank_you_text = RichTextField(blank=True) + + content_panels = AbstractEmailForm.content_panels + [ + FieldPanel('intro'), + InlinePanel('form_fields', label="Form fields"), + FieldPanel('thank_you_text'), + MultiFieldPanel([ + FieldRowPanel([ + FieldPanel('from_address', classname="col6"), + FieldPanel('to_address', classname="col6"), + ]), + FieldPanel('subject'), + ], "Email"), + ] + + class Meta: + verbose_name = "Contact Page" +endef + +define CONTACT_PAGE_LANDING +{% extends 'base.html' %} +{% block content %}

    Thank you!

    {% endblock %} +endef + +define DOCKERFILE +FROM amazonlinux:2023 +RUN dnf install -y shadow-utils python3.11 python3.11-pip make nodejs20-npm nodejs postgresql15 postgresql15-server +USER postgres +RUN initdb -D /var/lib/pgsql/data +USER root +RUN useradd wagtail +EXPOSE 8000 +ENV PYTHONUNBUFFERED=1 PORT=8000 +COPY requirements.txt / +RUN python3.11 -m pip install -r /requirements.txt +WORKDIR /app +RUN chown wagtail:wagtail /app +COPY --chown=wagtail:wagtail . . +USER wagtail +RUN cd frontend; npm-20 install; npm-20 run build +RUN python3.11 manage.py collectstatic --noinput --clear +CMD set -xe; pg_ctl -D /var/lib/pgsql/data -l /tmp/logfile start; python3.11 manage.py migrate --noinput; gunicorn backend.wsgi:application +endef + +define DOCKERCOMPOSE +version: '3' + +services: + db: + image: postgres:latest + volumes: + - postgres_data:/var/lib/postgresql/data + environment: + POSTGRES_DB: project + POSTGRES_USER: admin + POSTGRES_PASSWORD: admin + + web: + build: . + command: sh -c "python manage.py migrate && gunicorn project.wsgi:application -b 0.0.0.0:8000" + volumes: + - .:/app + ports: + - "8000:8000" + depends_on: + - db + environment: + DATABASE_URL: postgres://admin:admin@db:5432/project + +volumes: + postgres_data: +endef + +define INTERNAL_IPS +INTERNAL_IPS = ["127.0.0.1",] +endef + + +define ESLINTRC +{ + "env": { + "browser": true, + "es2021": true, + "node": true + }, + "extends": [ + "eslint:recommended", + "plugin:react/recommended" + ], + "overrides": [ + { + "env": { + "node": true + }, + "files": [ + ".eslintrc.{js,cjs}" + ], + "parserOptions": { + "sourceType": "script" + } + } + ], + "parserOptions": { + "ecmaVersion": "latest", + "sourceType": "module" + }, + "plugins": [ + "react" + ], + "rules": { + "no-unused-vars": "off" + }, + settings: { + react: { + version: 'detect', + }, + }, +} +endef + +define FAVICON_TEMPLATE +{% load static %} + +endef + + +define HOME_PAGE_MODEL +from django.db import models +from wagtail.models import Page +from wagtail.fields import RichTextField, StreamField +from wagtail import blocks +from wagtail.admin.panels import FieldPanel +from wagtail.images.blocks import ImageChooserBlock +from wagtail_color_panel.fields import ColorField +from wagtail_color_panel.edit_handlers import NativeColorPanel + + +class MarketingBlock(blocks.StructBlock): + title = blocks.CharBlock(required=False, help_text='Enter the block title') + content = blocks.RichTextBlock(required=False, help_text='Enter the block content') + images = blocks.ListBlock(ImageChooserBlock(required=False), help_text="Select one or two images for column display. Select three or more images for carousel display.") + image = ImageChooserBlock(required=False, help_text="Select one image for background display.") + block_class = blocks.CharBlock( + required=False, + help_text='Enter a CSS class for styling the marketing block', + classname='full title', + default='vh-100 bg-secondary', + ) + image_class = blocks.CharBlock( + required=False, + help_text='Enter a CSS class for styling the column display image(s)', + classname='full title', + default='img-thumbnail p-5', + ) + layout_class = blocks.CharBlock( + required=False, + help_text='Enter a CSS class for styling the layout.', + classname='full title', + default='d-flex flex-row', + ) + + class Meta: + icon = 'placeholder' + template = 'blocks/marketing_block.html' + + +class HomePage(Page): + template = 'home/home_page.html' # Create a template for rendering the home page + marketing_blocks = StreamField([ + ('marketing_block', MarketingBlock()), + ], blank=True, null=True, use_json_field=True) + content_panels = Page.content_panels + [ + FieldPanel('marketing_blocks'), + ] + + class Meta: + verbose_name = 'Home Page' +endef + +define HOME_PAGE_TEMPLATE +{% extends "base.html" %} +{% load wagtailcore_tags %} +{% block content %} +
    + {% for block in page.marketing_blocks %} + {% include_block block %} + {% endfor %} +
    +{% endblock %} +endef + +define INDEX_HTML +

    Hello world

    +endef + +define JENKINS_FILE +pipeline { + agent any + stages { + stage('') { + steps { + echo '' + } + } + } +} +endef + +define SITEPAGE_MODEL +from wagtail.models import Page + + +class SitePage(Page): + template = "sitepage/site_page.html" + + class Meta: + verbose_name = "Site Page" +endef + +define SEARCH_TEMPLATE +{% extends "base.html" %} +{% load static wagtailcore_tags %} +{% block body_class %}template-searchresults{% endblock %} +{% block title %}Search{% endblock %} +{% block content %} +

    Search

    +
    + + +
    + {% if search_results %} + + {% if search_results.has_previous %} + Previous + {% endif %} + {% if search_results.has_next %} + Next + {% endif %} + {% elif search_query %} + No results found + {% else %} + No results found. Try a test query? + {% endif %} +{% endblock %} +endef + +define SEARCH_URLS +from django.urls import path +from .views import search + +urlpatterns = [ + path("", search, name="search") +] +endef + +define SITEUSER_URLS +from django.urls import path +from .views import UserProfileView, UpdateThemePreferenceView, UserEditView + +urlpatterns = [ + path('profile/', UserProfileView.as_view(), name='user-profile'), + path('update_theme_preference/', UpdateThemePreferenceView.as_view(), name='update_theme_preference'), + path('/edit/', UserEditView.as_view(), name='user-edit'), +] +endef + +define BACKEND_URLS +from django.conf import settings +from django.urls import include, path +from django.contrib import admin + +from wagtail.admin import urls as wagtailadmin_urls +from wagtail import urls as wagtail_urls +from wagtail.documents import urls as wagtaildocs_urls + +from rest_framework import routers, serializers, viewsets +# from dj_rest_auth.registration.views import RegisterView + +from siteuser.models import User + +urlpatterns = [ + path('accounts/', include('allauth.urls')), + path('django/', admin.site.urls), + path('wagtail/', include(wagtailadmin_urls)), + path('user/', include('siteuser.urls')), + path('search/', include('search.urls')), + path('modelformtest/', include('modelformtest.urls')), +] + +if settings.DEBUG: + from django.conf.urls.static import static + from django.contrib.staticfiles.urls import staticfiles_urlpatterns + + # Serve static and media files from development server + urlpatterns += staticfiles_urlpatterns() + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + + import debug_toolbar + urlpatterns += [ + path("__debug__/", include(debug_toolbar.urls)), + ] + +# https://www.django-rest-framework.org/#example +class UserSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = User + fields = ['url', 'username', 'email', 'is_staff'] + +class UserViewSet(viewsets.ModelViewSet): + queryset = User.objects.all() + serializer_class = UserSerializer + +router = routers.DefaultRouter() +router.register(r'users', UserViewSet) + +urlpatterns += [ + path("api/", include(router.urls)), + path("api/", include("rest_framework.urls", namespace="rest_framework")), + # path("api/", include("dj_rest_auth.urls")), + # path("api/register/", RegisterView.as_view(), name="register"), +] + +urlpatterns += [ + path("hijack/", include("hijack.urls")), +] + +urlpatterns += [ + # For anything not caught by a more specific rule above, hand over to + # Wagtail's page serving mechanism. This should be the last pattern in + # the list: + path("", include(wagtail_urls)), + + # Alternatively, if you want Wagtail pages to be served from a subpath + # of your site, rather than the site root: + # path("pages/", include(wagtail_urls)), +] +endef + +define REST_FRAMEWORK +REST_FRAMEWORK = { + # Use Django's standard `django.contrib.auth` permissions, + # or allow read-only access for unauthenticated users. + 'DEFAULT_PERMISSION_CLASSES': [ + 'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly' + ] +} +endef + +define FRONTEND_APP_CONFIG +import '../utils/themeToggler.js'; +import '../utils/tinymce.js'; +endef + +define FRONTEND_PORTAL +// Via pwellever +import React from 'react'; +import { createPortal } from 'react-dom'; + +const parseProps = data => Object.entries(data).reduce((result, [key, value]) => { + if (value.toLowerCase() === 'true') { + value = true; + } else if (value.toLowerCase() === 'false') { + value = false; + } else if (value.toLowerCase() === 'null') { + value = null; + } else if (!isNaN(parseFloat(value)) && isFinite(value)) { + // Parse numeric value + value = parseFloat(value); + } else if ( + (value[0] === '[' && value.slice(-1) === ']') || (value[0] === '{' && value.slice(-1) === '}') + ) { + // Parse JSON strings + value = JSON.parse(value); + } + + result[key] = value; + return result; +}, {}); + +// This method of using portals instead of calling ReactDOM.render on individual components +// ensures that all components are mounted under a single React tree, and are therefore able +// to share context. + +export default function getPageComponents (components) { + const getPortalComponent = domEl => { + // The element's "data-component" attribute is used to determine which component to render. + // All other "data-*" attributes are passed as props. + const { component: componentName, ...rest } = domEl.dataset; + const Component = components[componentName]; + if (!Component) { + console.error(`Component "$${componentName}" not found.`); + return null; + } + const props = parseProps(rest); + domEl.innerHTML = ''; + + // eslint-disable-next-line no-unused-vars + const { ErrorBoundary } = components; + return createPortal( + + + , + domEl, + ); + }; + + return Array.from(document.querySelectorAll('[data-component]')).map(getPortalComponent); +} +endef + +define FRONTEND_COMPONENTS +export { default as ErrorBoundary } from './ErrorBoundary'; +export { default as UserMenu } from './UserMenu'; +endef + +define FRONTEND_CONTEXT_INDEX +export { UserContextProvider as default } from './UserContextProvider'; +endef + +define FRONTEND_CONTEXT_USER_PROVIDER +// UserContextProvider.js +import React, { createContext, useContext, useState } from 'react'; +import PropTypes from 'prop-types'; + +const UserContext = createContext(); + +export const UserContextProvider = ({ children }) => { + const [isAuthenticated, setIsAuthenticated] = useState(false); + + const login = () => { + try { + // Add logic to handle login, set isAuthenticated to true + setIsAuthenticated(true); + } catch (error) { + console.error('Login error:', error); + // Handle error, e.g., show an error message to the user + } + }; + + const logout = () => { + try { + // Add logic to handle logout, set isAuthenticated to false + setIsAuthenticated(false); + } catch (error) { + console.error('Logout error:', error); + // Handle error, e.g., show an error message to the user + } + }; + + return ( + + {children} + + ); +}; + +UserContextProvider.propTypes = { + children: PropTypes.node.isRequired, +}; + +export const useUserContext = () => { + const context = useContext(UserContext); + + if (!context) { + throw new Error('useUserContext must be used within a UserContextProvider'); + } + + return context; +}; + +// Add PropTypes for the return value of useUserContext +useUserContext.propTypes = { + isAuthenticated: PropTypes.bool.isRequired, + login: PropTypes.func.isRequired, + logout: PropTypes.func.isRequired, +}; +endef + +define FRONTEND_STYLES +// If you comment out code below, bootstrap will use red as primary color +// and btn-primary will become red + +// $primary: red; + +@import "~bootstrap/scss/bootstrap.scss"; + +.jumbotron { + // should be relative path of the entry scss file + background-image: url("../../vendors/images/sample.jpg"); + background-size: cover; +} + +#theme-toggler-authenticated:hover { + cursor: pointer; /* Change cursor to pointer on hover */ + color: #007bff; /* Change color on hover */ +} + +#theme-toggler-anonymous:hover { + cursor: pointer; /* Change cursor to pointer on hover */ + color: #007bff; /* Change color on hover */ +} +endef + +define FRONTEND_APP +import React from 'react'; +import { createRoot } from 'react-dom/client'; +import 'bootstrap'; +import '@fortawesome/fontawesome-free/js/fontawesome'; +import '@fortawesome/fontawesome-free/js/solid'; +import '@fortawesome/fontawesome-free/js/regular'; +import '@fortawesome/fontawesome-free/js/brands'; +import getDataComponents from '../dataComponents'; +import UserContextProvider from '../context'; +import * as components from '../components'; +import "../styles/index.scss"; +import "../styles/theme-blue.scss"; +import "./config"; + +const { ErrorBoundary } = components; +const dataComponents = getDataComponents(components); +const container = document.getElementById('app'); +const root = createRoot(container); +const App = () => ( + + + {dataComponents} + + +) +root.render(); +endef + +define GIT_IGNORE +__pycache__ +*.pyc +dist/ +node_modules/ +_build/ +.elasticbeanstalk/ +endef + +define HTML_FOOTER +{% load wagtailcore_tags %} +
    + {% wagtail_site as current_site %} +

    © {% now "Y" %} {{ current_site.site_name|default:"Project Makefile" }}

    +
      +
    • Home
    • + {% for child in current_site.root_page.get_children %} +
    • {{ child }}
    • + {% endfor %} +
    +
    +endef + + +define HTML_HEADER +{% load wagtailcore_tags %} +{% wagtail_site as current_site %} +
    +
    + +
    +
    +endef + +define HTML_OFFCANVAS +{% load wagtailcore_tags %} +{% wagtail_site as current_site %} +
    + +
    + {% wagtail_site as current_site %} + +
    +
    +endef + +define MODEL_FORM_TEST_MODEL +from django.db import models +from django.shortcuts import reverse + +class TestModel(models.Model): + name = models.CharField(max_length=100, blank=True, null=True) + email = models.EmailField(blank=True, null=True) + age = models.IntegerField(blank=True, null=True) + is_active = models.BooleanField(default=True) + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return self.name or f"test-model-{self.pk}" + + def get_absolute_url(self): + return reverse('test_model_detail', kwargs={'pk': self.pk}) +endef + +define MODEL_FORM_TEST_ADMIN +from django.contrib import admin +from .models import TestModel + +@admin.register(TestModel) +class TestModelAdmin(admin.ModelAdmin): + pass +endef + +define MODEL_FORM_TEST_VIEWS +from django.views.generic import ListView, CreateView, UpdateView, DetailView +from .models import TestModel +from .forms import TestModelForm + + +class TestModelListView(ListView): + model = TestModel + template_name = "test_model_list.html" + context_object_name = "test_models" + + +class TestModelCreateView(CreateView): + model = TestModel + form_class = TestModelForm + template_name = "test_model_form.html" + + def form_valid(self, form): + form.instance.created_by = self.request.user + return super().form_valid(form) + + +class TestModelUpdateView(UpdateView): + model = TestModel + form_class = TestModelForm + template_name = "test_model_form.html" + + +class TestModelDetailView(DetailView): + model = TestModel + template_name = "test_model_detail.html" + context_object_name = "test_model" +endef + +define MODEL_FORM_TEST_FORMS +from django import forms +from .models import TestModel + +class TestModelForm(forms.ModelForm): + class Meta: + model = TestModel + fields = ['name', 'email', 'age', 'is_active'] # Add or remove fields as needed +endef + +define MODEL_FORM_TEST_TEMPLATE_FORM +{% extends 'base.html' %} +{% block content %} +

    {% if form.instance.pk %}Update Test Model{% else %}Create Test Model{% endif %}

    +
    + {% csrf_token %} + {{ form.as_p }} + +
    +{% endblock %} +endef + +define MODEL_FORM_TEST_TEMPLATE_DETAIL +{% extends 'base.html' %} +{% block content %} +

    Test Model Detail: {{ test_model.name }}

    +

    Name: {{ test_model.name }}

    +

    Email: {{ test_model.email }}

    +

    Age: {{ test_model.age }}

    +

    Active: {{ test_model.is_active }}

    +

    Created At: {{ test_model.created_at }}

    + Edit Test Model +{% endblock %} +endef + +define MODEL_FORM_TEST_TEMPLATE_LIST +{% extends 'base.html' %} +{% block content %} +

    Test Models List

    + + Create New Test Model +{% endblock %} +endef + +define MODEL_FORM_TEST_URLS +from django.urls import path +from .views import ( + TestModelListView, + TestModelCreateView, + TestModelUpdateView, + TestModelDetailView, +) + +urlpatterns = [ + path('test-models/', TestModelListView.as_view(), name='test_model_list'), + path('test-models/create/', TestModelCreateView.as_view(), name='test_model_create'), + path('test-models//update/', TestModelUpdateView.as_view(), name='test_model_update'), + path('test-models//', TestModelDetailView.as_view(), name='test_model_detail'), +] +endef + +define PRIVACY_PAGE_MODEL +from wagtail.models import Page +from wagtail.admin.panels import FieldPanel +from wagtailmarkdown.fields import MarkdownField + + +class PrivacyPage(Page): + """ + A Wagtail Page model for the Privacy Policy page. + """ + + template = "privacy_page.html" + + body = MarkdownField() + + content_panels = Page.content_panels + [ + FieldPanel("body", classname="full"), + ] + + class Meta: + verbose_name = "Privacy Page" +endef + +define PRIVACY_PAGE_TEMPLATE +{% extends 'base.html' %} +{% load wagtailmarkdown %} +{% block content %}
    {{ page.body|markdown }}
    {% endblock %} +endef + +define REQUIREMENTS_TEST +pytest +pytest-runner +coverage +pytest-mock +pytest-cov +hypothesis +selenium +pytest-django +factory-boy +flake8 +tox +endef + +define SITEUSER_FORM +from django import forms +from django.contrib.auth.forms import UserChangeForm +from .models import User + +class SiteUserForm(UserChangeForm): + class Meta(UserChangeForm.Meta): + model = User + fields = ("username", "user_theme_preference", "bio", "rate") + + bio = forms.CharField(widget=forms.Textarea(attrs={"id": "editor"}), required=False) +endef + +define SITEUSER_MODEL +from django.db import models +from django.contrib.auth.models import AbstractUser, Group, Permission +from django.conf import settings + +class User(AbstractUser): + groups = models.ManyToManyField(Group, related_name='siteuser_set', blank=True) + user_permissions = models.ManyToManyField( + Permission, related_name='siteuser_set', blank=True + ) + + user_theme_preference = models.CharField(max_length=10, choices=settings.THEMES, default='light') + + bio = models.TextField(blank=True, null=True) + rate = models.FloatField(blank=True, null=True) +endef + +define SETTINGS_THEMES +THEMES = [ + ('light', 'Light Theme'), + ('dark', 'Dark Theme'), +] +endef + +define SITEUSER_ADMIN +from django.contrib.auth.admin import UserAdmin +from django.contrib import admin + +from .models import User + +admin.site.register(User, UserAdmin) +endef + +define SITEUSER_EDIT_TEMPLATE +{% extends 'base.html' %} + +{% block content %} +

    Edit User

    +
    + {% csrf_token %} + {{ form }} +
    + + Cancel +
    +
    +{% endblock %} +endef + +define SITEUSER_VIEW_TEMPLATE +{% extends 'base.html' %} + +{% block content %} +

    User Profile

    +
    + Edit +
    +

    Username: {{ user.username }}

    +

    Theme: {{ user.user_theme_preference }}

    +

    Bio: {{ user.bio|default:""|safe }}

    +

    Rate: {{ user.rate|default:"" }}

    +{% endblock %} +endef + +define SITEUSER_VIEW +import json + +from django.contrib.auth.mixins import LoginRequiredMixin +from django.http import JsonResponse +from django.utils.decorators import method_decorator +from django.views import View +from django.views.decorators.csrf import csrf_exempt +from django.views.generic import DetailView +from django.views.generic.edit import UpdateView +from django.urls import reverse_lazy + +from .models import User +from .forms import SiteUserForm + + +class UserProfileView(LoginRequiredMixin, DetailView): + model = User + template_name = "profile.html" + + def get_object(self, queryset=None): + return self.request.user + + +@method_decorator(csrf_exempt, name="dispatch") +class UpdateThemePreferenceView(View): + def post(self, request, *args, **kwargs): + try: + data = json.loads(request.body.decode("utf-8")) + new_theme = data.get("theme") + user = request.user + user.user_theme_preference = new_theme + user.save() + response_data = {"theme": new_theme} + return JsonResponse(response_data) + except json.JSONDecodeError as e: + return JsonResponse({"error": e}, status=400) + + def http_method_not_allowed(self, request, *args, **kwargs): + return JsonResponse({"error": "Invalid request method"}, status=405) + + +class UserEditView(LoginRequiredMixin, UpdateView): + model = User + template_name = 'user_edit.html' # Create this template in your templates folder + form_class = SiteUserForm + + def get_success_url(self): + # return reverse_lazy('user-profile', kwargs={'pk': self.object.pk}) + return reverse_lazy('user-profile') +endef + +define SITEPAGE_TEMPLATE +{% extends 'base.html' %} +{% block content %} +

    {{ page.title }}

    +{% endblock %} +endef + +define THEME_BLUE +@import "~bootstrap/scss/bootstrap.scss"; + +[data-bs-theme="blue"] { + --bs-body-color: var(--bs-white); + --bs-body-color-rgb: #{to-rgb($$white)}; + --bs-body-bg: var(--bs-blue); + --bs-body-bg-rgb: #{to-rgb($$blue)}; + --bs-tertiary-bg: #{$$blue-600}; + + .dropdown-menu { + --bs-dropdown-bg: #{color-mix($$blue-500, $$blue-600)}; + --bs-dropdown-link-active-bg: #{$$blue-700}; + } + + .btn-secondary { + --bs-btn-bg: #{color-mix($gray-600, $blue-400, .5)}; + --bs-btn-border-color: #{rgba($$white, .25)}; + --bs-btn-hover-bg: #{color-adjust(color-mix($gray-600, $blue-400, .5), 5%)}; + --bs-btn-hover-border-color: #{rgba($$white, .25)}; + --bs-btn-active-bg: #{color-adjust(color-mix($gray-600, $blue-400, .5), 10%)}; + --bs-btn-active-border-color: #{rgba($$white, .5)}; + --bs-btn-focus-border-color: #{rgba($$white, .5)}; + + // --bs-btn-focus-box-shadow: 0 0 0 .25rem rgba(255, 255, 255, 20%); + } +} +endef + +define THEME_TOGGLER +document.addEventListener('DOMContentLoaded', function () { + const rootElement = document.documentElement; + const anonThemeToggle = document.getElementById('theme-toggler-anonymous'); + const authThemeToggle = document.getElementById('theme-toggler-authenticated'); + if (authThemeToggle) { + localStorage.removeItem('data-bs-theme'); + } + const anonSavedTheme = localStorage.getItem('data-bs-theme'); + if (anonSavedTheme) { + rootElement.setAttribute('data-bs-theme', anonSavedTheme); + } + if (anonThemeToggle) { + anonThemeToggle.addEventListener('click', function () { + const currentTheme = rootElement.getAttribute('data-bs-theme') || 'light'; + const newTheme = currentTheme === 'light' ? 'dark' : 'light'; + rootElement.setAttribute('data-bs-theme', newTheme); + localStorage.setItem('data-bs-theme', newTheme); + }); + } + if (authThemeToggle) { + const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]').value; + authThemeToggle.addEventListener('click', function () { + const currentTheme = rootElement.getAttribute('data-bs-theme') || 'light'; + const newTheme = currentTheme === 'light' ? 'dark' : 'light'; + fetch('/user/update_theme_preference/', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': csrfToken, // Include the CSRF token in the headers + }, + body: JSON.stringify({ theme: newTheme }), + }) + .then(response => response.json()) + .then(data => { + rootElement.setAttribute('data-bs-theme', newTheme); + }) + .catch(error => { + console.error('Error updating theme preference:', error); + }); + }); + } +}); +endef + +define TINYMCE_JS +import tinymce from 'tinymce'; +import 'tinymce/icons/default'; +import 'tinymce/themes/silver'; +import 'tinymce/skins/ui/oxide/skin.css'; +import 'tinymce/plugins/advlist'; +import 'tinymce/plugins/code'; +import 'tinymce/plugins/emoticons'; +import 'tinymce/plugins/emoticons/js/emojis'; +import 'tinymce/plugins/link'; +import 'tinymce/plugins/lists'; +import 'tinymce/plugins/table'; +import 'tinymce/models/dom'; + +tinymce.init({ + selector: 'textarea#editor', + plugins: 'advlist code emoticons link lists table', + toolbar: 'bold italic | bullist numlist | link emoticons', + skin: false, + content_css: false, +}); +endef + +define WEBPACK_CONFIG_JS +const path = require('path'); + +module.exports = { + mode: 'development', + entry: './src/index.js', + output: { + filename: 'bundle.js', + path: path.resolve(__dirname, 'dist'), + }, +}; +endef + +define WEBPACK_REVEAL_CONFIG_JS +const path = require('path'); +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); + +module.exports = { + mode: 'development', + entry: './src/index.js', + output: { + filename: 'bundle.js', + path: path.resolve(__dirname, 'dist'), + }, + module: { + rules: [ + { + test: /\.css$$/, + use: [MiniCssExtractPlugin.loader, 'css-loader'], + }, + ], + }, + plugins: [ + new MiniCssExtractPlugin({ + filename: 'bundle.css', + }), + ], +}; +endef + +define WEBPACK_INDEX_HTML + + + + + + Hello, Webpack! + + + + + +endef + +define WEBPACK_REVEAL_INDEX_HTML + + + + + + Project Makefile + + +
    +
    +
    + Slide 1: Draw some circles +
    +
    + Slide 2: Draw the rest of the owl +
    +
    +
    + + +endef + +define WEBPACK_INDEX_JS +const message = "Hello, World!"; +console.log(message); +endef + +define WEBPACK_REVEAL_INDEX_JS +import 'reveal.js/dist/reveal.css'; +import 'reveal.js/dist/theme/black.css'; +import Reveal from 'reveal.js'; +import RevealNotes from 'reveal.js/plugin/notes/notes.js'; +Reveal.initialize({ slideNumber: true, plugins: [ RevealNotes ]}); +endef + +# ------------------------------------------------------------------------------ +# Export variables +# ------------------------------------------------------------------------------ + +export ALLAUTH_LAYOUT_BASE +export AUTHENTICATION_BACKENDS +export BABELRC +export BACKEND_URLS +export BASE_TEMPLATE +export BLOCK_CAROUSEL +export BLOCK_MARKETING +export COMPONENT_CLOCK +export COMPONENT_ERROR +export COMPONENT_USER_MENU +export CONTACT_PAGE_MODEL +export CONTACT_PAGE_TEMPLATE +export CONTACT_PAGE_LANDING +export CONTACT_PAGE_TEST +export DOCKERFILE +export DOCKERCOMPOSE +export ESLINTRC +export FAVICON_TEMPLATE +export FRONTEND_APP +export FRONTEND_APP_CONFIG +export FRONTEND_COMPONENTS +export FRONTEND_PORTAL +export FRONTEND_STYLES +export GIT_IGNORE +export HOME_PAGE_MODEL +export HOME_PAGE_TEMPLATE +export HTML_FOOTER +export HTML_HEADER +export HTML_OFFCANVAS +export INDEX_HTML +export INTERNAL_IPS +export JENKINS_FILE +export MODEL_FORM_TEST_ADMIN +export MODEL_FORM_TEST_FORMS +export MODEL_FORM_TEST_MODEL +export MODEL_FORM_TEST_URLS +export MODEL_FORM_TEST_VIEWS +export MODEL_FORM_TEST_TEMPLATE_DETAIL +export MODEL_FORM_TEST_TEMPLATE_FORM +export MODEL_FORM_TEST_TEMPLATE_LIST +export PRIVACY_PAGE_MODEL +export REST_FRAMEWORK +export FRONTEND_CONTEXT_INDEX +export FRONTEND_CONTEXT_USER_PROVIDER +export PRIVACY_PAGE_MODEL +export PRIVACY_PAGE_TEMPLATE +export REQUIREMENTS_TEST +export SETTINGS_THEMES +export SITEPAGE_MODEL +export SITEPAGE_TEMPLATE +export SITEUSER_FORM +export SITEUSER_MODEL +export SITEUSER_ADMIN +export SITEUSER_URLS +export SITEUSER_VIEW +export SITEUSER_VIEW_TEMPLATE +export SITEUSER_EDIT_TEMPLATE +export SEARCH_TEMPLATE +export SEARCH_URLS +export THEME_BLUE +export THEME_TOGGLER +export TINYMCE_JS +export WEBPACK_CONFIG_JS +export WEBPACK_INDEX_HTML +export WEBPACK_INDEX_JS +export WEBPACK_REVEAL_CONFIG_JS +export WEBPACK_REVEAL_INDEX_HTML +export WEBPACK_REVEAL_INDEX_JS + +# ------------------------------------------------------------------------------ +# Rules +# ------------------------------------------------------------------------------ + +aws-ssm-describe-parameters-default: + @aws ssm describe-parameters | cat + +aws-ssm-default: +ifdef AWS_PROFILE + @echo "Environment variable is set: $(AWS_PROFILE)" + aws ssm describe-parameters | cat + @echo "Get parameter values with: aws ssm getparameter --name ." +else + @echo "Environment variable not set. Set AWS_PROFILE before running this target." +endif + +docker-build-default: + podman build -t $(PROJECT_NAME) . + +docker-compose-default: + podman compose up + +docker-serve-default: + podman run -p 8000:8000 $(PROJECT_NAME) + +eb-check-env-default: # https://stackoverflow.com/a/4731504/185820 +ifndef SSH_KEY + $(error SSH_KEY is undefined) +endif +ifndef VPC_ID + $(error VPC_ID is undefined) +endif +ifndef VPC_SG + $(error VPC_SG is undefined) +endif +ifndef VPC_SUBNET_EC2 + $(error VPC_SUBNET_EC2 is undefined) +endif +ifndef VPC_SUBNET_ELB + $(error VPC_SUBNET_ELB is undefined) +endif + +eb-create-default: eb-check-env + eb create $(ENV_NAME) \ + -im $(INSTANCE_MIN) \ + -ix $(INSTANCE_MAX) \ + -ip $(INSTANCE_PROFILE) \ + -i $(INSTANCE_TYPE) \ + -k $(SSH_KEY) \ + -p $(PLATFORM) \ + --elb-type $(LB_TYPE) \ + --vpc \ + --vpc.id $(VPC_ID) \ + --vpc.elbpublic \ + --vpc.publicip \ + --vpc.ec2subnets $(VPC_SUBNET_EC2) \ + --vpc.elbsubnets $(VPC_SUBNET_ELB) \ + --vpc.securitygroups $(VPC_SG) + +eb-deploy-default: + eb deploy + +eb-restart-default: + eb ssh -c "systemctl restart web" + +eb-upgrade-default: + eb upgrade + +eb-init-default: + eb init + +eb-list-platforms-default: + aws elasticbeanstalk list-platform-versions + +eb-list-databases-default: + @eb ssh --quiet -c "export PGPASSWORD=$(DATABASE_PASS); psql -l -U $(DATABASE_USER) -h $(DATABASE_HOST) $(DATABASE_NAME)" + +eb-logs-default: + eb logs + +eb-secret-default: + @SECRET_KEY=$$(openssl rand -base64 48); \ + aws ssm put-parameter --name "SECRET_KEY" --value "$$SECRET_KEY" --type String + +npm-init-default: + npm init -y + $(GIT_ADD) package.json + -$(GIT_ADD) package-lock.json + +npm-build-default: + npm run build + +npm-install-default: + npm install + $(GIT_ADD) package-lock.json + +npm-clean-default: + $(DEL_DIR) dist/ + $(DEL_DIR) node_modules/ + $(DEL_FILE) package-lock.json + +npm-serve-default: + npm run start + +db-mysql-init-default: + -mysqladmin -u root drop $(PROJECT_NAME) + -mysqladmin -u root create $(PROJECT_NAME) + +db-pg-init-default: + -dropdb $(PROJECT_NAME) + -createdb $(PROJECT_NAME) + +db-pg-init-test-default: + -dropdb test_$(PROJECT_NAME) + -createdb test_$(PROJECT_NAME) + +db-pg-export-default: + @eb ssh --quiet -c "export PGPASSWORD=$(DATABASE_PASS); pg_dump -U $(DATABASE_USER) -h $(DATABASE_HOST) $(DATABASE_NAME)" > $(DATABASE_NAME).sql + @echo "Wrote $(DATABASE_NAME).sql" + +db-pg-import-default: + @psql $(DATABASE_NAME) < $(DATABASE_NAME).sql + +django-frontend-app-default: python-webpack-init + $(ADD_DIR) frontend/src/context + $(ADD_DIR) frontend/src/images + $(ADD_DIR) frontend/src/utils + @echo "$$COMPONENT_CLOCK" > frontend/src/components/Clock.js + @echo "$$COMPONENT_ERROR" > frontend/src/components/ErrorBoundary.js + @echo "$$FRONTEND_CONTEXT_INDEX" > frontend/src/context/index.js + @echo "$$FRONTEND_CONTEXT_USER_PROVIDER" > frontend/src/context/UserContextProvider.js + @echo "$$COMPONENT_USER_MENU" > frontend/src/components/UserMenu.js + @echo "$$FRONTEND_APP" > frontend/src/application/app.js + @echo "$$FRONTEND_APP_CONFIG" > frontend/src/application/config.js + @echo "$$FRONTEND_COMPONENTS" > frontend/src/components/index.js + @echo "$$FRONTEND_PORTAL" > frontend/src/dataComponents.js + @echo "$$FRONTEND_STYLES" > frontend/src/styles/index.scss + @echo "$$BABELRC" > frontend/.babelrc + @echo "$$ESLINTRC" > frontend/.eslintrc + @echo "$$THEME_BLUE" > frontend/src/styles/theme-blue.scss + @echo "$$THEME_TOGGLER" > frontend/src/utils/themeToggler.js + @echo "$$TINYMCE_JS" > frontend/src/utils/tinymce.js + -$(GIT_ADD) home + -$(GIT_ADD) frontend + +django-secret-default: + @python -c "from secrets import token_urlsafe; print(token_urlsafe(50))" + +django-siteuser-default: + python manage.py startapp siteuser + @echo "$$SITEUSER_FORM" > siteuser/forms.py + @echo "$$SITEUSER_MODEL" > siteuser/models.py + @echo "$$SITEUSER_ADMIN" > siteuser/admin.py + @echo "$$SITEUSER_VIEW" > siteuser/views.py + @echo "$$SITEUSER_URLS" > siteuser/urls.py + $(ADD_DIR) siteuser/templates/ + $(ADD_DIR) siteuser/management/commands + @echo "$$SITEUSER_VIEW_TEMPLATE" > siteuser/templates/profile.html + @echo "$$SITEUSER_EDIT_TEMPLATE" > siteuser/templates/user_edit.html + @echo "INSTALLED_APPS.append('siteuser')" >> $(SETTINGS) + @echo "AUTH_USER_MODEL = 'siteuser.User'" >> $(SETTINGS) + python manage.py makemigrations siteuser + $(GIT_ADD) siteuser/ + +django-graph-default: + python manage.py graph_models -a -o $(PROJECT_NAME).png + +django-show-urls-default: + python manage.py show_urls + +django-loaddata-default: + python manage.py loaddata + +django-migrate-default: + python manage.py migrate + +django-migrations-default: + python manage.py makemigrations + +django-migrations-show-default: + python manage.py showmigrations + +django-model-form-test-default: + python manage.py startapp modelformtest + @echo "$$MODEL_FORM_TEST_ADMIN" > modelformtest/admin.py + @echo "$$MODEL_FORM_TEST_FORMS" > modelformtest/forms.py + @echo "$$MODEL_FORM_TEST_MODEL" > modelformtest/models.py + @echo "$$MODEL_FORM_TEST_URLS" > modelformtest/urls.py + @echo "$$MODEL_FORM_TEST_VIEWS" > modelformtest/views.py + $(ADD_DIR) modelformtest/templates/modelformtest + @echo "$$MODEL_FORM_TEST_TEMPLATE_DETAIL" > modelformtest/templates/test_model_detail.html + @echo "$$MODEL_FORM_TEST_TEMPLATE_FORM" > modelformtest/templates/test_model_form.html + @echo "$$MODEL_FORM_TEST_TEMPLATE_LIST" > modelformtest/templates/modelformtest/testmodel_list.html + @echo "INSTALLED_APPS.append('modelformtest')" >> $(SETTINGS) + python manage.py makemigrations + $(GIT_ADD) modelformtest + +django-serve-default: + cd frontend; npm run watch & + python manage.py runserver 0.0.0.0:8000 + +django-settings-default: + echo "# $(PROJECT_NAME)" >> $(SETTINGS) + echo "ALLOWED_HOSTS = ['*']" >> $(SETTINGS) + echo "import dj_database_url # noqa" >> $(SETTINGS) + echo "DATABASE_URL = os.environ.get('DATABASE_URL', \ + 'postgres://$(DB_USER):$(DB_PASS)@$(DB_HOST):$(DB_PORT)/$(PROJECT_NAME)')" >> $(SETTINGS) + echo "DATABASES['default'] = dj_database_url.parse(DATABASE_URL)" >> $(SETTINGS) + echo "INSTALLED_APPS.append('webpack_boilerplate')" >> $(SETTINGS) + echo "INSTALLED_APPS.append('rest_framework')" >> $(SETTINGS) + echo "INSTALLED_APPS.append('rest_framework.authtoken')" >> $(SETTINGS) + echo "INSTALLED_APPS.append('allauth')" >> $(SETTINGS) + echo "INSTALLED_APPS.append('allauth.account')" >> $(SETTINGS) + echo "INSTALLED_APPS.append('allauth.socialaccount')" >> $(SETTINGS) + echo "INSTALLED_APPS.append('wagtailmenus')" >> $(SETTINGS) + echo "INSTALLED_APPS.append('wagtailmarkdown')" >> $(SETTINGS) + echo "INSTALLED_APPS.append('wagtail_modeladmin')" >> $(SETTINGS) + echo "INSTALLED_APPS.append('wagtailseo')" >> $(SETTINGS) + echo "INSTALLED_APPS.append('wagtail_color_panel')" >> $(SETTINGS) + echo "INSTALLED_APPS.append('wagtail.contrib.settings')" >> $(SETTINGS) + echo "INSTALLED_APPS.append('django_extensions')" >> $(SETTINGS) + echo "INSTALLED_APPS.append('debug_toolbar')" >> $(DEV_SETTINGS) + echo "INSTALLED_APPS.append('crispy_forms')" >> $(SETTINGS) + echo "INSTALLED_APPS.append('crispy_bootstrap5')" >> $(SETTINGS) + echo "INSTALLED_APPS.append('django_recaptcha')" >> $(SETTINGS) + echo "MIDDLEWARE.append('allauth.account.middleware.AccountMiddleware')" >> $(SETTINGS) + echo "MIDDLEWARE.append('debug_toolbar.middleware.DebugToolbarMiddleware')" >> $(DEV_SETTINGS) + echo "MIDDLEWARE.append('hijack.middleware.HijackUserMiddleware')" >> $(DEV_SETTINGS) + echo "STATICFILES_DIRS.append(os.path.join(BASE_DIR, 'frontend/build'))" >> $(SETTINGS) + echo "WEBPACK_LOADER = { 'MANIFEST_FILE': os.path.join(BASE_DIR, 'frontend/build/manifest.json'), }" >> $(SETTINGS) + echo "$$REST_FRAMEWORK" >> $(SETTINGS) + echo "$$SETTINGS_THEMES" >> $(SETTINGS) + echo "$$INTERNAL_IPS" >> $(DEV_SETTINGS) + echo "LOGIN_REDIRECT_URL = '/'" >> $(SETTINGS) + echo "DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'" >> $(SETTINGS) + echo "$$AUTHENTICATION_BACKENDS" >> $(SETTINGS) + echo "TEMPLATES[0]['OPTIONS']['context_processors'].append('wagtail.contrib.settings.context_processors.settings')" >> $(SETTINGS) + echo "TEMPLATES[0]['OPTIONS']['context_processors'].append('wagtailmenus.context_processors.wagtailmenus')">> $(SETTINGS) + echo "SILENCED_SYSTEM_CHECKS = ['django_recaptcha.recaptcha_test_key_error']" >> $(SETTINGS) + +django-crispy-default: + @echo "CRISPY_TEMPLATE_PACK = 'bootstrap5'" >> $(SETTINGS) + @echo "CRISPY_ALLOWED_TEMPLATE_PACKS = 'bootstrap5'" >> $(SETTINGS) + +django-shell-default: + python manage.py shell + +django-static-default: + python manage.py collectstatic --noinput + +django-su-default: + DJANGO_SUPERUSER_PASSWORD=admin python manage.py createsuperuser --noinput --username=admin --email=$(PROJECT_EMAIL) + +django-test-default: django-npm-install django-npm-build django-static + -$(MAKE) pip-install-test + python manage.py test + +django-user-default: + python manage.py shell -c "from django.contrib.auth.models import User; \ + User.objects.create_user('user', '', 'user')" + +django-url-patterns-default: + echo "$$BACKEND_URLS" > backend/$(URLS) + +django-npm-install-default: + cd frontend; npm install + +django-npm-install-save-default: + cd frontend; npm install \ + @fortawesome/fontawesome-free \ + @fortawesome/fontawesome-svg-core \ + @fortawesome/free-brands-svg-icons \ + @fortawesome/free-solid-svg-icons \ + @fortawesome/react-fontawesome \ + camelize \ + date-fns \ + history \ + mapbox-gl \ + query-string \ + react-animate-height \ + react-chartjs-2 \ + react-copy-to-clipboard \ + react-date-range \ + react-dom \ + react-dropzone \ + react-hook-form \ + react-image-crop \ + react-map-gl \ + react-modal \ + react-resize-detector \ + react-select \ + react-swipeable \ + snakeize \ + striptags \ + tinymce \ + url-join \ + viewport-mercator-project + +django-npm-install-save-dev-default: + cd frontend; npm install \ + eslint-plugin-react \ + eslint-config-standard \ + eslint-config-standard-jsx \ + @babel/core \ + @babel/preset-env \ + @babel/preset-react \ + --save-dev + +django-npm-test-default: + cd frontend; npm run test + +django-npm-build-default: + cd frontend; npm run build + +django-open-default: +ifeq ($(UNAME), Linux) + @echo "Opening on Linux." + xdg-open http://0.0.0.0:8000 +else ifeq ($(UNAME), Darwin) + @echo "Opening on macOS (Darwin)." + open http://0.0.0.0:8000 +else + @echo "Unable to open on: $(UNAME)" +endif + +favicon-default: + dd if=/dev/urandom bs=64 count=1 status=none | base64 | convert -size 16x16 -depth 8 -background none -fill white label:@- favicon.png + convert favicon.png favicon.ico + $(GIT_ADD) favicon.ico + $(DEL_FILE) favicon.png + +gh-default: + gh browse + +git-ignore-default: + echo "$$GIT_IGNORE" > .gitignore + $(GIT_ADD) .gitignore + +git-branches-default: + -for i in $(GIT_BRANCHES) ; do \ + git checkout -t $$i ; done + +git-commit-default: + -@$(GIT_COMMIT) + +git-push-default: + -@$(GIT_PUSH) + +git-push-force-default: + -@$(GIT_PUSH_FORCE) + +git-commit-edit-default: + -git commit -a + +git-prune-default: + git remote update origin --prune + +git-set-upstream-default: + git push --set-upstream origin main + +git-commit-empty-default: + git commit --allow-empty -m "Empty-Commit" + +help-default: + @for makefile in $(MAKEFILE_LIST); do \ + $(MAKE) -pRrq -f $$makefile : 2>/dev/null \ + | awk -v RS= -F: '/^# File/,/^# Finished Make data base/ {if ($$1 !~ "^[#.]") {print $$1}}' \ + | sort | egrep -v -e '^[^[:alnum:]]' -e '^$@$$' \ + | xargs | tr ' ' '\n' \ + | awk '{printf "%s\n", $$0}' ; done | less # http://stackoverflow.com/a/26339924 + +index-default: + @echo "$$INDEX_HTML" > index.html + +jenkins-init-default: + @echo "$$JENKINS_FILE" > Jenkinsfile + +lint-default: + -ruff check -v --fix + -ruff format -v + -djlint --reformat --format-css --format-js . + +make-default: + $(GIT_ADD) Makefile + -git commit Makefile -m "Add/update project-makefile files" + -git push + +pip-freeze-default: + $(ENSURE_PIP) + python -m pip freeze | sort > $(TMPDIR)/requirements.txt + mv -f $(TMPDIR)/requirements.txt . + $(GIT_ADD) requirements.txt + +pip-init-default: + touch requirements.txt + $(GIT_ADD) requirements.txt + +pip-init-test-default: + @echo "$$REQUIREMENTS_TEST" > requirements-test.txt + $(GIT_ADD) requirements-test.txt + +pip-install-default: + $(ENSURE_PIP) + $(MAKE) pip-upgrade + python -m pip install wheel + python -m pip install -r requirements.txt + +pip-install-dev-default: + $(ENSURE_PIP) + python -m pip install -r requirements-dev.txt + +pip-install-test-default: + $(ENSURE_PIP) + python -m pip install -r requirements-test.txt + +pip-install-upgrade-default: + cat requirements.txt | awk -F\= '{print $$1}' > $(TMPDIR)/requirements.txt + mv -f $(TMPDIR)/requirements.txt . + $(ENSURE_PIP) + python -m pip install -U -r requirements.txt + python -m pip freeze | sort > $(TMPDIR)/requirements.txt + mv -f $(TMPDIR)/requirements.txt . + +pip-upgrade-default: + $(ENSURE_PIP) + python -m pip install -U pip + +pip-uninstall-default: + $(ENSURE_PIP) + python -m pip freeze | xargs python -m pip uninstall -y + +project-mk-default: + touch project.mk + $(GIT_ADD) project.mk + +python-serve-default: + @echo "\n\tServing HTTP on http://0.0.0.0:8000\n" + python3 -m http.server + +python-setup-sdist-default: + python3 setup.py sdist --format=zip + +python-webpack-init-default: + python manage.py webpack_init --no-input + +rand-default: + @openssl rand -base64 12 | sed 's/\///g' + +readme-init-rst-default: + @echo "$(PROJECT_NAME)" > README.rst + @echo "================================================================================" >> README.rst + -@git add README.rst + +readme-init-md-default: + @echo "# $(PROJECT_NAME)" > README.md + -@git add README.md + +readme-edit-rst-default: + vi README.rst + +readme-edit-md-default: + vi README.md + +readme-open-default: + open README.pdf + +readme-build-default: + rst2pdf README.rst + +reveal-build-default: + npm run build + +reveal-init-default: webpack-reveal-init + npm install \ + css-loader \ + mini-css-extract-plugin \ + reveal.js \ + style-loader + jq '.scripts += {"build": "webpack"}' package.json > \ + $(TMPDIR)/tmp.json && mv $(TMPDIR)/tmp.json package.json + jq '.scripts += {"start": "webpack serve --mode development --port 8000 --static"}' package.json > \ + $(TMPDIR)/tmp.json && mv $(TMPDIR)/tmp.json package.json + jq '.scripts += {"watch": "webpack watch --mode development"}' package.json > \ + $(TMPDIR)/tmp.json && mv $(TMPDIR)/tmp.json package.json + +reveal-serve-default: + npm run watch & + python -m http.server + +sphinx-init-default: sphinx-install + sphinx-quickstart -q -p $(PROJECT_NAME) -a $(USER) -v 0.0.1 $(RANDIR) + $(COPY_DIR) $(RANDIR)/* . + $(DEL_DIR) $(RANDIR) + $(GIT_ADD) index.rst + $(GIT_ADD) conf.py + $(DEL_FILE) make.bat + git checkout Makefile + $(MAKE) gitignore + +sphinx-theme-init-default: + export THEME_NAME=$(PROJECT_NAME)_theme; \ + $(ADD_DIR) $$THEME_NAME ; \ + $(ADD_FILE) $$THEME_NAME/__init__.py ; \ + $(GIT_ADD) $$THEME_NAME/__init__.py ; \ + $(ADD_FILE) $$THEME_NAME/theme.conf ; \ + $(GIT_ADD) $$THEME_NAME/theme.conf ; \ + $(ADD_FILE) $$THEME_NAME/layout.html ; \ + $(GIT_ADD) $$THEME_NAME/layout.html ; \ + $(ADD_DIR) $$THEME_NAME/static/css ; \ + $(ADD_FILE) $$THEME_NAME/static/css/style.css ; \ + $(ADD_DIR) $$THEME_NAME/static/js ; \ + $(ADD_FILE) $$THEME_NAME/static/js/script.js ; \ + $(GIT_ADD) $$THEME_NAME/static + +review-default: +ifeq ($(UNAME), Darwin) + $(REVIEW_EDITOR) `find backend/ -name \*.py` `find backend/ -name \*.html` `find frontend/ -name \*.js` `find frontend/ -name \*.js` +else + @echo "Unsupported" +endif + +sphinx-install-default: + echo "Sphinx\n" > requirements.txt + @$(MAKE) pip-install + @$(MAKE) pip-freeze + -$(GIT_ADD) requirements.txt + +sphinx-build-default: + sphinx-build -b html -d _build/doctrees . _build/html + +sphinx-build-pdf-default: + sphinx-build -b rinoh . _build/rinoh + +sphinx-serve-default: + cd _build/html;python3 -m http.server + +usage-default: + @echo "Project Makefile 🤷" + @echo "Usage: make [options] [target] ..." + @echo "Examples:" + @echo " make help Print all targets" + @echo " make usage Print this message" + +wagtail-search-urls: + @echo "$$SEARCH_URLS" > search/urls.py + $(GIT_ADD) search + +wagtail-search-template: + @echo "$$SEARCH_TEMPLATE" > search/templates/search/search.html + +wagtail-privacy-default: + python manage.py startapp privacy + @echo "$$PRIVACY_PAGE_MODEL" > privacy/models.py + $(ADD_DIR) privacy/templates + @echo "$$PRIVACY_PAGE_TEMPLATE" > privacy/templates/privacy_page.html + @echo "INSTALLED_APPS.append('privacy')" >> $(SETTINGS) + python manage.py makemigrations privacy + $(GIT_ADD) privacy/ + +wagtail-base-default: + @echo "$$BASE_TEMPLATE" > backend/templates/base.html + +wagtail-header-default: + @echo "$$HTML_HEADER" > backend/templates/header.html + +wagtail-clean-default: + -@for dir in "$(WAGTAIL_CLEAN_DIRS)"; do \ + $(DEL_DIR) $$dir; \ + done + -@for file in "$(WAGTAIL_CLEAN_FILES)"; do \ + $(DEL_FILE) $$file; \ + done + +wagtail-homepage-default: + @echo "$$HOME_PAGE_MODEL" > home/models.py + @echo "$$HOME_PAGE_TEMPLATE" > home/templates/home/home_page.html + $(ADD_DIR) home/templates/blocks + @echo "$$BLOCK_MARKETING" > home/templates/blocks/marketing_block.html + @echo "$$BLOCK_CAROUSEL" > home/templates/blocks/carousel_block.html + -$(GIT_ADD) home + +wagtail-backend-templates-default: + $(ADD_DIR) backend/templates/allauth/layouts + @echo "$$ALLAUTH_LAYOUT_BASE" > backend/templates/allauth/layouts/base.html + @echo "$$BASE_TEMPLATE" > backend/templates/base.html + @echo "$$FAVICON_TEMPLATE" > backend/templates/favicon.html + @echo "$$HTML_HEADER" > backend/templates/header.html + @echo "$$HTML_FOOTER" > backend/templates/footer.html + @echo "$$HTML_OFFCANVAS" > backend/templates/offcanvas.html + $(GIT_ADD) backend/templates/ + +wagtail-start-default: + wagtail start backend . + +wagtail-init-default: db-init wagtail-install wagtail-start + @echo "$$DOCKERFILE" > Dockerfile + @echo "$$DOCKERCOMPOSE" > docker-compose.yml + export SETTINGS=backend/settings/base.py DEV_SETTINGS=backend/settings/dev.py; \ + $(MAKE) django-settings + export SETTINGS=backend/settings/base.py; \ + $(MAKE) django-model-form-test + export URLS=urls.py; \ + $(MAKE) django-url-patterns + $(GIT_ADD) backend + $(GIT_ADD) requirements.txt + $(GIT_ADD) manage.py + $(GIT_ADD) Dockerfile + $(GIT_ADD) .dockerignore + $(MAKE) wagtail-homepage + $(MAKE) wagtail-search-template + $(MAKE) wagtail-search-urls + export SETTINGS=backend/settings/base.py; \ + $(MAKE) django-siteuser + export SETTINGS=backend/settings/base.py; \ + $(MAKE) wagtail-privacy + export SETTINGS=backend/settings/base.py; \ + $(MAKE) wagtail-contactpage + export SETTINGS=backend/settings/base.py; \ + $(MAKE) wagtail-sitepage + export SETTINGS=backend/settings/base.py; \ + $(MAKE) django-crispy + $(MAKE) django-migrations + $(MAKE) django-migrate + $(MAKE) su + $(MAKE) wagtail-backend-templates + @$(MAKE) django-frontend-app + @$(MAKE) django-npm-install + @$(MAKE) django-npm-install-save + @$(MAKE) django-npm-install-save-dev + @$(MAKE) pip-init-test + @$(MAKE) readme + @$(MAKE) gitignore + @$(MAKE) freeze + @$(MAKE) serve + +wagtail-install-default: + $(ENSURE_PIP) + python -m pip install \ + Faker \ + boto3 \ + crispy-bootstrap5 \ + djangorestframework \ + django-allauth \ + django-after-response \ + django-ckeditor \ + django-colorful \ + django-cors-headers \ + django-countries \ + django-crispy-forms \ + django-debug-toolbar \ + django-extensions \ + django-hijack \ + django-honeypot \ + django-imagekit \ + django-import-export \ + django-ipware \ + django-multiselectfield \ + django-phonenumber-field \ + django-recurrence \ + django-recaptcha \ + django-registration \ + django-richtextfield \ + django-sendgrid-v5 \ + django-social-share \ + django-storages \ + django-tables2 \ + django-timezone-field \ + django-widget-tweaks \ + dj-database-url \ + dj-stripe \ + enmerkar \ + gunicorn \ + html2docx \ + icalendar \ + mailchimp-marketing \ + mailchimp-transactional \ + phonenumbers \ + psycopg2-binary \ + python-webpack-boilerplate \ + python-docx \ + reportlab \ + texttable \ + wagtail \ + wagtailmenus \ + wagtail-color-panel \ + wagtail-django-recaptcha \ + wagtail-markdown \ + wagtail_modeladmin \ + wagtail-seo \ + weasyprint \ + whitenoise \ + xhtml2pdf + +webpack-init-default: npm-init + @echo "$$WEBPACK_CONFIG_JS" > webpack.config.js + $(GIT_ADD) webpack.config.js + npm install --save-dev webpack webpack-cli webpack-dev-server + $(ADD_DIR) src/ + @echo "$$WEBPACK_INDEX_JS" > src/index.js + $(GIT_ADD) src/index.js + @echo "$$WEBPACK_INDEX_HTML" > index.html + $(GIT_ADD) index.html + $(MAKE) gitignore + +webpack-reveal-init-default: npm-init + @echo "$$WEBPACK_REVEAL_CONFIG_JS" > webpack.config.js + $(GIT_ADD) webpack.config.js + npm install --save-dev webpack webpack-cli webpack-dev-server + $(ADD_DIR) src/ + @echo "$$WEBPACK_REVEAL_INDEX_JS" > src/index.js + $(GIT_ADD) src/index.js + @echo "$$WEBPACK_REVEAL_INDEX_HTML" > index.html + $(GIT_ADD) index.html + $(MAKE) gitignore + +wagtail-contactpage-default: + python manage.py startapp contactpage + @echo "$$CONTACT_PAGE_MODEL" > contactpage/models.py + @echo "$$CONTACT_PAGE_TEST" > contactpage/tests.py + $(ADD_DIR) contactpage/templates/contactpage/ + @echo "$$CONTACT_PAGE_TEMPLATE" > contactpage/templates/contactpage/contact_page.html + @echo "$$CONTACT_PAGE_LANDING" > contactpage/templates/contactpage/contact_page_landing.html + @echo "INSTALLED_APPS.append('contactpage')" >> $(SETTINGS) + python manage.py makemigrations contactpage + $(GIT_ADD) contactpage/ + +wagtail-sitepage-default: + python manage.py startapp sitepage + @echo "$$SITEPAGE_MODEL" > sitepage/models.py + $(ADD_DIR) sitepage/templates/sitepage/ + @echo "$$SITEPAGE_TEMPLATE" > sitepage/templates/sitepage/site_page.html + @echo "INSTALLED_APPS.append('sitepage')" >> $(SETTINGS) + python manage.py makemigrations sitepage + $(GIT_ADD) sitepage/ + +# ------------------------------------------------------------------------------ +# More rules +# ------------------------------------------------------------------------------ + +b-default: build +build-default: pip-install +c-default: clean +ce-default: git-commit-edit-push +clean-default: wagtail-clean +cp-default: git-commit-push +d-default: deploy +db-export-default: db-pg-export +db-import-default: db-pg-import +db-init-default: db-pg-init +db-init-test-default: db-pg-init-test +deploy-default: eb-deploy +django-clean-default: wagtail-clean +django-init-default: wagtail-init +djlint-default: lint-djlint +e-default: edit +edit-default: readme-edit-md +empty-default: git-commit-empty +force-push-default: git-push-force +freeze-default: pip-freeze +git-commit-edit-push-default: git-commit-edit git-push +git-commit-push-default: git-commit git-push +gitignore-default: git-ignore +h-default: help +i-default: install +init-default: wagtail-init +install-default: pip-install +install-dev-default: pip-install-dev +install-test-default: pip-install-test +l-default: lint +logs-default: eb-logs +migrate-default: django-migrate +migrations-default: django-migrations +migrations-show-default: django-migrations-show +mk-default: project-mk +o-default: open +open-default: django-open +p-default: git-push +pack-default: django-npm-build +readme-default: readme-init-md +restart-default: eb-restart +reveal-default: reveal-init +s-default: serve +sdist-default: python-setup-sdist +secret-default: django-secret +serve-default: django-serve +shell-default: django-shell +show-urls-default: django-show-urls +ssm-list-default: aws-ssm-describe-parameters +static-default: django-static +su-default: django-su +test-default: django-test +u-default: usage +up-default: eb-upgrade +urls-default: django-show-urls +webpack-default: webpack-init + +# -------------------------------------------------------------------------------- +# Overrides +# -------------------------------------------------------------------------------- + +%: %-default # https://stackoverflow.com/a/49804748 + @ true From 458b0d5fd45aa54b9547127f228fef87a460b38c Mon Sep 17 00:00:00 2001 From: "Jeffrey A. Clark (Alex)" Date: Thu, 2 May 2024 19:38:29 -0400 Subject: [PATCH 2/5] Lint --- base/templates/base.html | 402 +-- dart/settings.py | 171 +- dart/urls.py | 126 +- dart/wsgi.py | 2 + missions/apps.py | 5 +- .../contextprocessors/context_processors.py | 2 +- missions/extras/helpers/analytics.py | 57 +- missions/extras/helpers/formatters.py | 6 +- missions/extras/helpers/sorters.py | 134 +- missions/extras/utils.py | 313 ++- missions/extras/validators.py | 8 +- .../management/commands/removeallusers.py | 11 +- missions/management/commands/setup-db.py | 29 +- missions/middleware.py | 27 +- missions/migrations/0001_initial.py | 691 ++++- .../0002_dataload_minimal_classification.py | 24 +- missions/migrations/0003_auto_event-id.py | 26 +- ...4_testdetail_supporting_data_sort_order.py | 9 +- .../migrations/0005_auto_20230708_1306.py | 770 ++++-- missions/models.py | 191 +- missions/templates/about.html | 39 +- missions/templates/business_area_list.html | 169 +- missions/templates/classification_list.html | 378 +-- missions/templates/color_list.html | 181 +- missions/templates/create_account.html | 74 +- missions/templates/delete_mission.html | 29 +- missions/templates/delete_test.html | 33 +- missions/templates/delete_test_data.html | 47 +- missions/templates/edit_mission.html | 35 +- missions/templates/edit_mission_hosts.html | 118 +- missions/templates/edit_mission_test.html | 179 +- missions/templates/edit_test_data.html | 37 +- .../templates/edit_testhosts_partial.html | 13 +- missions/templates/login.html | 29 +- missions/templates/login_interstitial.html | 59 +- missions/templates/mission_list.html | 64 +- missions/templates/mission_list_tests.html | 280 +-- missions/templates/mission_stats_partial.html | 75 +- .../modals/edit_missionhosts_modal.html | 222 +- .../modals/edit_testhosts_modal.html | 293 +-- .../tags_classification_legend_partial.html | 18 +- missions/templates/tags_managedata.html | 26 +- .../templates/tags_preloader_partial.html | 12 +- .../templates/test_supporting_data_list.html | 134 +- .../templates/update_dynamic_settings.html | 46 +- .../dart_bootstrap_formatting_helpers.py | 10 +- missions/templatetags/quickparts.py | 36 +- missions/urls.py | 151 +- missions/views.py | 2235 +++++++++-------- 49 files changed, 4538 insertions(+), 3488 deletions(-) diff --git a/base/templates/base.html b/base/templates/base.html index a156083..04aa2cf 100644 --- a/base/templates/base.html +++ b/base/templates/base.html @@ -16,194 +16,214 @@ # --> {% endcomment %} -{% load static %} -{% load bootstrap3 %} -{% load quickparts %} -{% load cache %} - - - - - - - - - - DART - {% block title %}{% endblock %} - - {% bootstrap_css %} - - - - - - - - - - - - - - - - - - - - {% block additional_head_content %}{% endblock %} - - - - - -
    - {% bootstrap_messages %} -
    - {% block main_heading %}{% endblock %} - - {% block content %}{% endblock %} -
    -
    - -
    - -
    -
    -
    -

    © {% now "Y" %} Lockheed Martin Corporation | About - v{{ DART_VERSION_NUMBER }} -

    -
    -
    -
    - - {% cache 600 legend_partial_bottom %}{% legend_partial 'bottom' %}{% endcache %} - - - - {% bootstrap_javascript %} - - - - - - - - - - - - +{% load static %} +{% load bootstrap3 %} +{% load quickparts %} +{% load cache %} + + + + + + + DART - + {% block title %}{% endblock %} + + {% bootstrap_css %} + + + + + + + + + + + + + + + {% block additional_head_content %}{% endblock %} + + + +
    + {% bootstrap_messages %} +
    + {% block main_heading %}{% endblock %} + {% block content %}{% endblock %} +
    +
    +
    + +
    +
    +
    +

    + © {% now "Y" %} Lockheed Martin Corporation | About + v{{ DART_VERSION_NUMBER }} +

    +
    +
    +
    + + {% cache 600 legend_partial_bottom %}{% legend_partial 'bottom' %}{% endcache %} + + + {% bootstrap_javascript %} + + + + + + + + diff --git a/dart/settings.py b/dart/settings.py index 3f98db7..2296d9f 100644 --- a/dart/settings.py +++ b/dart/settings.py @@ -21,87 +21,88 @@ # Do NOT expose this application to an untrusted network. import os + BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -DART_VERSION_NUMBER = '2.1.1' +DART_VERSION_NUMBER = "2.1.1" # SECURITY WARNING # We are not randomizing this key for you. -SECRET_KEY = '5s9G+t##Trga48t594g1g8sret*(#*/rg-dfgs43wt)((dh/*d' +SECRET_KEY = "5s9G+t##Trga48t594g1g8sret*(#*/rg-dfgs43wt)((dh/*d" -ALLOWED_HOSTS = ['*'] +ALLOWED_HOSTS = ["*"] # Application definition INSTALLED_APPS = ( - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'django.contrib.admindocs', - 'bootstrap3', - 'base', - 'missions.apps.MissionsConfig', + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "django.contrib.admindocs", + "bootstrap3", + "base", + "missions.apps.MissionsConfig", ) MIDDLEWARE = ( - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", ) -DEBUG=True +DEBUG = True TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - #Hard coding path for now. Try without this path or use Base_dir as described here; #https://stackoverflow.com/questions/3038459/django-template-path - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.contrib.auth.context_processors.auth', - 'django.template.context_processors.debug', - 'django.template.context_processors.i18n', - 'django.template.context_processors.media', - 'django.template.context_processors.static', - 'django.template.context_processors.tz', - 'django.contrib.messages.context_processors.messages', - 'missions.contextprocessors.context_processors.version_number', - 'django.template.context_processors.request' + "BACKEND": "django.template.backends.django.DjangoTemplates", + # Hard coding path for now. Try without this path or use Base_dir as described here; #https://stackoverflow.com/questions/3038459/django-template-path + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.contrib.auth.context_processors.auth", + "django.template.context_processors.debug", + "django.template.context_processors.i18n", + "django.template.context_processors.media", + "django.template.context_processors.static", + "django.template.context_processors.tz", + "django.contrib.messages.context_processors.messages", + "missions.contextprocessors.context_processors.version_number", + "django.template.context_processors.request", ], - # SECURITY WARNING - # We run in debug mode so that static files are automatically served - # out by the built-in django webserver for ease of setup. Since you're already running - # this on a trusted network (remember the security warning at the top of this file) you - # are probably okay doing the same. - 'debug': DEBUG, - 'libraries' : { - 'staticfiles': 'django.templatetags.static', - } + # SECURITY WARNING + # We run in debug mode so that static files are automatically served + # out by the built-in django webserver for ease of setup. Since you're already running + # this on a trusted network (remember the security warning at the top of this file) you + # are probably okay doing the same. + "debug": DEBUG, + "libraries": { + "staticfiles": "django.templatetags.static", + }, }, }, ] -ROOT_URLCONF = 'dart.urls' +ROOT_URLCONF = "dart.urls" # WSGI_APPLICATION = 'dart.wsgi.application' DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': os.path.join(BASE_DIR, 'data/db.sqlite3'), + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": os.path.join(BASE_DIR, "data/db.sqlite3"), } } -LANGUAGE_CODE = 'en-us' +LANGUAGE_CODE = "en-us" -TIME_ZONE = 'America/New_York' +TIME_ZONE = "America/New_York" USE_I18N = True @@ -109,57 +110,53 @@ USE_TZ = True -STATIC_URL = '/static/' +STATIC_URL = "/static/" -MEDIA_URL = '/data/' -MEDIA_ROOT = os.path.join(BASE_DIR, 'supporting_data') +MEDIA_URL = "/data/" +MEDIA_ROOT = os.path.join(BASE_DIR, "supporting_data") # Bootstrap Settings BOOTSTRAP3 = { - 'css_url': STATIC_URL + 'vendor/css/bootstrap.min.css', - 'javascript_url': STATIC_URL + 'vendor/js/bootstrap.min.js', - 'jquery_url': STATIC_URL + 'vendor/js/jquery.min.js', - 'required_css_class': 'required', + "css_url": STATIC_URL + "vendor/css/bootstrap.min.css", + "javascript_url": STATIC_URL + "vendor/js/bootstrap.min.js", + "jquery_url": STATIC_URL + "vendor/js/jquery.min.js", + "required_css_class": "required", } LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'handlers': { - 'debug_file': { - 'level': 'DEBUG', - 'class': 'logging.handlers.RotatingFileHandler', - 'filename': os.path.join(BASE_DIR, 'logs/debug.log'), - 'maxBytes': 20000000, # 20MB - 'backupCount': 10, - 'formatter': 'verbose', + "version": 1, + "disable_existing_loggers": False, + "handlers": { + "debug_file": { + "level": "DEBUG", + "class": "logging.handlers.RotatingFileHandler", + "filename": os.path.join(BASE_DIR, "logs/debug.log"), + "maxBytes": 20000000, # 20MB + "backupCount": 10, + "formatter": "verbose", }, - 'info_file': { - 'level': 'INFO', - 'class': 'logging.FileHandler', - 'filename': os.path.join(BASE_DIR, 'logs/info.log'), - 'formatter': 'verbose', + "info_file": { + "level": "INFO", + "class": "logging.FileHandler", + "filename": os.path.join(BASE_DIR, "logs/info.log"), + "formatter": "verbose", }, }, - 'loggers': { - 'missions': { - 'handlers': ['debug_file', 'info_file'], - 'level': 'DEBUG', - 'propagate': True, + "loggers": { + "missions": { + "handlers": ["debug_file", "info_file"], + "level": "DEBUG", + "propagate": True, }, }, - 'formatters': { - 'verbose': { - 'format': '%(levelname)s [%(asctime)s] %(name)s: %(message)s' - }, - 'simple': { - 'format': '%(levelname)s %(message)s' - }, + "formatters": { + "verbose": {"format": "%(levelname)s [%(asctime)s] %(name)s: %(message)s"}, + "simple": {"format": "%(levelname)s %(message)s"}, }, } -REPORT_TEMPLATE_PATH = os.path.join(BASE_DIR, '2016_Template.docx') -DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' +REPORT_TEMPLATE_PATH = os.path.join(BASE_DIR, "2016_Template.docx") +DEFAULT_AUTO_FIELD = "django.db.models.AutoField" # Require an interstitial message to be displayed # @@ -168,4 +165,4 @@ # in hours as a positive integer or 0 to indicate it should be displayed once per application logon. # Omitting this setting will bypass the interstitial. # -#REQUIRED_INTERSTITIAL_DISPLAY_INTERVAL = 0 # In hours, or 0 for once per login +# REQUIRED_INTERSTITIAL_DISPLAY_INTERVAL = 0 # In hours, or 0 for once per login diff --git a/dart/urls.py b/dart/urls.py index 47dfb73..c95a66e 100644 --- a/dart/urls.py +++ b/dart/urls.py @@ -19,55 +19,93 @@ from django.contrib.staticfiles.urls import staticfiles_urlpatterns import missions.views from django.contrib.auth.decorators import login_required -from django.contrib.auth import logout, login from django.views.static import serve from django.conf import settings from django.contrib.auth.views import LoginView, LogoutView urlpatterns = [ - url(r'^missions/', include('missions.urls')), - - url(r'^$', RedirectView.as_view(url='/missions/', permanent=False)), - - url(r'^admin/', admin.site.urls), - - url(r'^accounts/logout/$', LogoutView.as_view(template_name='login.html'), name='logout'), - #url(r'^accounts/logout/$', logout, name='logout'), - #url(r'^accounts/login/$', login, {'template_name': 'login.html'}, name='login'), - url(r'^accounts/login/$', LoginView.as_view(template_name='login.html'), name='login'), - url(r'^accounts/$', RedirectView.as_view(url='/', permanent=False)), - url(r'^accounts/profile/$', RedirectView.as_view(url='/', permanent=False)), - - url(r'^settings/$', - login_required(missions.views.UpdateDynamicSettingsView.as_view()), name='update-settings'), - url(r'^settings/business-areas/$', - login_required(missions.views.BusinessAreaListView.as_view()), name='list-business-areas'), - url(r'^settings/update-business-area(?:/(?P\d+))?/$', - login_required(missions.views.business_area_handler), name='update-business-area'), - url(r'^settings/classifications/$', - login_required(missions.views.ClassificationListView.as_view()), name='list-classifications'), - url(r'^settings/update-classification(?:/(?P\d+))?/$', - login_required(missions.views.classification_handler), name='update-classification'), - url(r'^settings/colors/$', - login_required(missions.views.ColorListView.as_view()), name='list-colors'), - url(r'^settings/update-color(?:/(?P\d+))?/$', - login_required(missions.views.color_handler), name='update-color'), - url(r'^settings/create-account/$', - missions.views.CreateAccountView.as_view(), name='create-account'), - - url(r'^login-interstitial/$', - login_required(missions.views.LoginInterstitialView.as_view()), name='login-interstitial'), - - url(r'^about/$', - login_required(missions.views.AboutTemplateView.as_view()), name='about'), - - url(r'^hosts/(?P\d+)/$', - login_required(missions.views.mission_host_handler), name='host-detail'), - - url(r'^data/(?P\d+)/$', - login_required(missions.views.DownloadSupportingDataView.as_view()), name='data-view'), - url(r'^data/(?P.*)$', - serve,{'document_root': settings.MEDIA_ROOT, 'show_indexes': False}), + url(r"^missions/", include("missions.urls")), + url(r"^$", RedirectView.as_view(url="/missions/", permanent=False)), + url(r"^admin/", admin.site.urls), + url( + r"^accounts/logout/$", + LogoutView.as_view(template_name="login.html"), + name="logout", + ), + # url(r'^accounts/logout/$', logout, name='logout'), + # url(r'^accounts/login/$', login, {'template_name': 'login.html'}, name='login'), + url( + r"^accounts/login/$", + LoginView.as_view(template_name="login.html"), + name="login", + ), + url(r"^accounts/$", RedirectView.as_view(url="/", permanent=False)), + url(r"^accounts/profile/$", RedirectView.as_view(url="/", permanent=False)), + url( + r"^settings/$", + login_required(missions.views.UpdateDynamicSettingsView.as_view()), + name="update-settings", + ), + url( + r"^settings/business-areas/$", + login_required(missions.views.BusinessAreaListView.as_view()), + name="list-business-areas", + ), + url( + r"^settings/update-business-area(?:/(?P\d+))?/$", + login_required(missions.views.business_area_handler), + name="update-business-area", + ), + url( + r"^settings/classifications/$", + login_required(missions.views.ClassificationListView.as_view()), + name="list-classifications", + ), + url( + r"^settings/update-classification(?:/(?P\d+))?/$", + login_required(missions.views.classification_handler), + name="update-classification", + ), + url( + r"^settings/colors/$", + login_required(missions.views.ColorListView.as_view()), + name="list-colors", + ), + url( + r"^settings/update-color(?:/(?P\d+))?/$", + login_required(missions.views.color_handler), + name="update-color", + ), + url( + r"^settings/create-account/$", + missions.views.CreateAccountView.as_view(), + name="create-account", + ), + url( + r"^login-interstitial/$", + login_required(missions.views.LoginInterstitialView.as_view()), + name="login-interstitial", + ), + url( + r"^about/$", + login_required(missions.views.AboutTemplateView.as_view()), + name="about", + ), + url( + r"^hosts/(?P\d+)/$", + login_required(missions.views.mission_host_handler), + name="host-detail", + ), + url( + r"^data/(?P\d+)/$", + login_required(missions.views.DownloadSupportingDataView.as_view()), + name="data-view", + ), + url( + r"^data/(?P.*)$", + serve, + {"document_root": settings.MEDIA_ROOT, "show_indexes": False}, + ), ] urlpatterns += staticfiles_urlpatterns() diff --git a/dart/wsgi.py b/dart/wsgi.py index 94f8cf0..32db63b 100644 --- a/dart/wsgi.py +++ b/dart/wsgi.py @@ -20,7 +20,9 @@ """ import os + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dart.settings") from django.core.wsgi import get_wsgi_application + application = get_wsgi_application() diff --git a/missions/apps.py b/missions/apps.py index e73f13c..5b3adcd 100644 --- a/missions/apps.py +++ b/missions/apps.py @@ -16,12 +16,11 @@ import logging from django.apps import AppConfig -from django.conf import settings logger = logging.getLogger(__name__) class MissionsConfig(AppConfig): - name = 'missions' - verbose_name = 'Missions' + name = "missions" + verbose_name = "Missions" diff --git a/missions/contextprocessors/context_processors.py b/missions/contextprocessors/context_processors.py index 4456263..ba1a54e 100644 --- a/missions/contextprocessors/context_processors.py +++ b/missions/contextprocessors/context_processors.py @@ -17,4 +17,4 @@ def version_number(request): - return {'DART_VERSION_NUMBER': settings.DART_VERSION_NUMBER} + return {"DART_VERSION_NUMBER": settings.DART_VERSION_NUMBER} diff --git a/missions/extras/helpers/analytics.py b/missions/extras/helpers/analytics.py index 976a6cb..3e6c320 100644 --- a/missions/extras/helpers/analytics.py +++ b/missions/extras/helpers/analytics.py @@ -23,18 +23,21 @@ 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.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() + 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='').count() + count = self.testcases.exclude(findings="").count() return count def count_of_test_cases(self): @@ -42,30 +45,32 @@ def count_of_test_cases(self): return count def count_of_executed_test_cases(self): - count = self.testcases.exclude(execution_status='N').count() + count = self.testcases.exclude(execution_status="N").count() return count def count_of_test_cases_approved(self): - count = self.testcases.filter(test_case_status='FINAL').count() + count = self.testcases.filter(test_case_status="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) + 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) + percentage = ( + self.count_of_test_cases_approved() / self.count_of_test_cases() + ) + return "{:.0%}".format(percentage) def count_of_test_cases_by_result(self): """ @@ -86,8 +91,8 @@ def nested_tuple_to_shallow_list(tup): 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']) + 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 @@ -102,12 +107,16 @@ def count_of_test_cases_by_mission_week(self): return [0] # Get the execution date for each test case in the mission - tc_dates = self.testcases.exclude(execution_status='N').values('attack_time_date') + tc_dates = self.testcases.exclude(execution_status="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 # + 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 @@ -134,17 +143,21 @@ def _count_of_test_case_types_by_mission_week(self): """ if self.count_of_executed_test_cases() == 0: - return [['No TCs have been executed yet!']] + 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='N').values('attack_time_date', 'attack_phase') + tc_records = self.testcases.exclude(execution_status="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'] + 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 @@ -157,11 +170,11 @@ def _count_of_test_case_types_by_mission_week(self): zero_indexed_phase_count_by_week = [] header_row = list() - header_row.append('') + header_row.append("") header_row.extend(list(range(1, week_delta))) total_row = list() - total_row.append('TOTAL') + total_row.append("TOTAL") total_row.extend([0] * week_delta) for phase_tuple in TestDetail.ATTACK_PHASES: diff --git a/missions/extras/helpers/formatters.py b/missions/extras/helpers/formatters.py index 6f64611..3f82b3b 100644 --- a/missions/extras/helpers/formatters.py +++ b/missions/extras/helpers/formatters.py @@ -33,6 +33,8 @@ def join_as_compacted_paragraphs(paragraphs): :return: String with \n separated paragraphs and no extra whitespace """ - paragraphs[:] = [' '.join(p.split()) for p in paragraphs] # Remove extra whitespace & newlines + paragraphs[:] = [ + " ".join(p.split()) for p in paragraphs + ] # Remove extra whitespace & newlines - return '\n'.join(paragraphs) + return "\n".join(paragraphs) diff --git a/missions/extras/helpers/sorters.py b/missions/extras/helpers/sorters.py index 1847b71..e264d74 100644 --- a/missions/extras/helpers/sorters.py +++ b/missions/extras/helpers/sorters.py @@ -22,12 +22,15 @@ 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 """ + """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)) + 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) @@ -43,38 +46,52 @@ def deconflict_and_update(mission_id, tests=None): # 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') + logger.debug( + "TC Deconflict found TCs but no existing order (mission {mission}); " + + "Building sort order from test case numbers".format() + ) + 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)) + 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)) + 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)) + 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)) + 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 @@ -82,8 +99,11 @@ def deconflict_and_update(mission_id, tests=None): 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)) + 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) @@ -92,43 +112,60 @@ def deconflict_and_update_supporting_data_sort_order(testdetail_id, testdata=Non # 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)) + logger.debug( + "Supporting data for test found but no existing order (testdetail {testdetail}); " + + "Building default sort order".format() + ) 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)) + 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)) + 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)) + 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)) + 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: @@ -139,29 +176,44 @@ def get_ordered_testdetails(cls, mission_id, reportable_tests_only=False): # Order excluded_tests = [] if reportable_tests_only: - excluded_tests = tests.filter(test_case_include_flag=False).values_list('id', flat=True) + 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] + 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): - + 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) + 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) + 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] + ordered_testdata = [ + testdata_dict[testdata_id] + for testdata_id in sort_order + if testdata_id not in excluded_data + ] return ordered_testdata diff --git a/missions/extras/utils.py b/missions/extras/utils.py index 8a54595..1aabd1b 100644 --- a/missions/extras/utils.py +++ b/missions/extras/utils.py @@ -31,7 +31,11 @@ from docx import Document from docx.shared import Inches, RGBColor -from docx.image.exceptions import UnrecognizedImageError, UnexpectedEndOfFileError, InvalidImageStreamError +from docx.image.exceptions import ( + UnrecognizedImageError, + UnexpectedEndOfFileError, + InvalidImageStreamError, +) from missions.models import Mission, DARTDynamicSettings, TestDetail from .helpers.sorters import TestSortingHelper @@ -42,10 +46,10 @@ class ReturnStatus(object): - def __init__(self, success=True, message='', **kwargs): + def __init__(self, success=True, message="", **kwargs): self.success = success self.message = message - self.data = kwargs.get('data') or {} + self.data = kwargs.get("data") or {} def to_json(self): return json.dumps(self.__dict__) @@ -54,7 +58,7 @@ def to_dict(self): return self.__dict__ def __str__(self): - return str(self.success) + ': ' + str(self.message) + return str(self.success) + ": " + str(self.message) def copy_table(document, table, cut=False): @@ -86,30 +90,32 @@ def get_cleared_paragraph(cell): 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 = ( + 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) + 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() + 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 + 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 @@ -117,34 +123,36 @@ def generate_report_or_attachments(mission_id, zip_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') + 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), + 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 list(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') + 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 : + 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'))) + "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 @@ -159,16 +167,13 @@ def add_to_data_dir(mission_data_dir, test_case_number, supporting_data): os.makedirs(path) # Copy file to destination path - shutil.copy( - os.path.join(settings.MEDIA_ROOT, supporting_data.filename()), - path - ) + shutil.copy(os.path.join(settings.MEDIA_ROOT, supporting_data.filename()), path) def prepend_classification(text): - return '(' + system_classification_short + ') ' + text + return "(" + system_classification_short + ") " + text def portion_mark_and_insert(paragraphs, document): - for paragraph in normalize_newlines(paragraphs).split('\n'): + for paragraph in normalize_newlines(paragraphs).split("\n"): if len(paragraph) > 0: document.add_paragraph(prepend_classification(paragraph)) @@ -181,49 +186,51 @@ def portion_mark_and_insert(paragraphs, document): data_table = document.tables[2] # Set the classification legend color from the background color of the banner - classification_style = document.styles['Table Classification'] + 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 = '' + 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 = '' + 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) + 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) + document.add_heading("Introduction", level=1) portion_mark_and_insert(mission.introduction, document) # Scope H1 and text - document.add_heading('Scope', level=1) + document.add_heading("Scope", level=1) portion_mark_and_insert(mission.scope, document) # Objectives H1 and text - document.add_heading('Objectives', level=1) + 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) + 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) + 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) + document.add_heading("Technical Assessment / Test Cases and Results", level=1) # For each test, Test # - Objective Attack Phase: H2 @@ -252,7 +259,6 @@ def portion_mark_and_insert(paragraphs, document): 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() @@ -266,31 +272,31 @@ def portion_mark_and_insert(paragraphs, document): tests_without_findings += 1 if t.enclave: - test_title += "(%s) %s" % ( - t.enclave, - t.test_objective - ) + test_title += "(%s) %s" % (t.enclave, t.test_objective) else: - test_title += "%s" % ( - t.test_objective, - ) + 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 + # 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) + table = copy_table(document, table_with_findings, cut=False) else: - if tests_without_findings == total_tests_without_findings or is_last_test_case: + 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) + table = copy_table(document, table_no_findings, cut=False) # Classification Marking - Top cell = table.cell(0, 0) @@ -303,9 +309,11 @@ def portion_mark_and_insert(paragraphs, document): # 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) + 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) + get_cleared_paragraph(cell).text = "Test #{0}".format(test_case_number) row_number = 0 @@ -324,14 +332,18 @@ def portion_mark_and_insert(paragraphs, document): 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: + 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: + 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) @@ -340,7 +352,9 @@ def portion_mark_and_insert(paragraphs, document): 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]) + 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() @@ -348,7 +362,7 @@ def portion_mark_and_insert(paragraphs, document): get_cleared_paragraph(left_cell).text = "Attack Type:" right_cell.text = t.attack_type else: - logger.debug('Removing Attack Phase/Type Row') + logger.debug("Removing Attack Phase/Type Row") remove_row(table, table.rows[row_number]) row_number -= 1 @@ -360,7 +374,7 @@ def portion_mark_and_insert(paragraphs, document): cell = get_cleared_paragraph(table.cell(row_number, 1)) cell.text = standardize_report_output_field(t.assumptions) else: - logger.debug('Removing Assumptions Row') + logger.debug("Removing Assumptions Row") remove_row(table, table.rows[row_number]) row_number -= 1 @@ -371,11 +385,15 @@ def portion_mark_and_insert(paragraphs, document): 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)) + 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') + logger.debug("Removing Description Row") remove_row(table, table.rows[row_number]) row_number -= 1 @@ -387,7 +405,7 @@ def portion_mark_and_insert(paragraphs, document): cell = get_cleared_paragraph(table.cell(row_number, 1)) cell.text = standardize_report_output_field(t.findings) else: - logger.debug('Removing Findings Row') + logger.debug("Removing Findings Row") remove_row(table, table.rows[row_number]) row_number -= 1 @@ -399,7 +417,7 @@ def portion_mark_and_insert(paragraphs, document): cell = get_cleared_paragraph(table.cell(row_number, 1)) cell.text = standardize_report_output_field(t.mitigation) else: - logger.debug('Removing Mitigations Row') + logger.debug("Removing Mitigations Row") remove_row(table, table.rows[row_number]) row_number -= 1 @@ -411,7 +429,7 @@ def portion_mark_and_insert(paragraphs, document): cell = get_cleared_paragraph(table.cell(row_number, 1)) cell.text = standardize_report_output_field(t.tools_used) else: - logger.debug('Removing Tools Row') + logger.debug("Removing Tools Row") remove_row(table, table.rows[row_number]) row_number -= 1 @@ -423,7 +441,7 @@ def portion_mark_and_insert(paragraphs, document): 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') + logger.debug("Removing Commands/Syntax Row") remove_row(table, table.rows[row_number]) row_number -= 1 @@ -433,9 +451,9 @@ def portion_mark_and_insert(paragraphs, document): 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()]) + cell.text = "\n".join([str(x) for x in t.target_hosts.all()]) else: - logger.debug('Removing Targets Row') + logger.debug("Removing Targets Row") remove_row(table, table.rows[row_number]) row_number -= 1 @@ -445,10 +463,10 @@ def portion_mark_and_insert(paragraphs, document): 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()]) + cell.text = "\n".join([str(x) for x in t.source_hosts.all()]) else: - logger.debug('Removing Sources Row') + logger.debug("Removing Sources Row") remove_row(table, table.rows[row_number]) row_number -= 1 @@ -458,9 +476,9 @@ def portion_mark_and_insert(paragraphs, document): 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') + cell.text = localtime(t.attack_time_date).strftime("%b %d, %Y @ %I:%M %p") else: - logger.debug('Removing Date/Time Row') + logger.debug("Removing Date/Time Row") remove_row(table, table.rows[row_number]) row_number -= 1 @@ -468,11 +486,14 @@ def portion_mark_and_insert(paragraphs, document): # Side Effects # row_number += 1 - if mission.attack_side_effects_include_flag and t.attack_side_effects_include_flag: + 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') + logger.debug("Removing Side Effects Row") remove_row(table, table.rows[row_number]) row_number -= 1 @@ -480,11 +501,14 @@ def portion_mark_and_insert(paragraphs, document): # Details # row_number += 1 - if mission.test_result_observation_include_flag and t.test_result_observation_include_flag: + 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') + logger.debug("Removing Details Row") remove_row(table, table.rows[row_number]) row_number -= 1 @@ -498,7 +522,7 @@ def portion_mark_and_insert(paragraphs, document): 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') + logger.debug("Removing Supporting Data Row") remove_row(table, table.rows[row_number]) row_number -= 1 @@ -506,8 +530,8 @@ def portion_mark_and_insert(paragraphs, document): # 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') + if False: # TODO: add mission-level toggle + logger.debug("Removing Customer Notes Row") remove_row(table, table.rows[row_number]) row_number -= 1 @@ -517,40 +541,44 @@ def portion_mark_and_insert(paragraphs, document): if mission.supporting_data_include_flag: my_data = TestSortingHelper.get_ordered_supporting_data( - test_detail_id=t.id, - reportable_supporting_data_only=True) + 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', + "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, - )) + 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.') + 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.') + 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.') + logger.debug("Creating a new image table.") document.add_paragraph() # Classification Marking - Top @@ -563,59 +591,82 @@ def portion_mark_and_insert(paragraphs, document): content_cell = image_table.cell(1, 0) - get_cleared_paragraph(content_cell).add_run().add_picture(d.test_file, width=Inches(5)) + 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, - )) + except UnrecognizedImageError: + 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))) + except (InvalidImageStreamError, UnexpectedEndOfFileError): + 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: + 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))) + except IOError: + 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) + 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) + 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.') + 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) + 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." + 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) name = mission.mission_name if zip_attachments: - zip_file = shutil.make_archive(mission_data_dir, 'zip', mission_data_dir) - with open(mission_data_dir + '.zip', 'rb') as f: + 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()), name else: diff --git a/missions/extras/validators.py b/missions/extras/validators.py index 286d82d..25fa961 100644 --- a/missions/extras/validators.py +++ b/missions/extras/validators.py @@ -20,9 +20,11 @@ def validate_host_format_string(value): - regex_match = re.match(r'^((?:{name}|{ip}|-| |\(|\)|[a-zA-Z:]))+$', value) + regex_match = re.match(r"^((?:{name}|{ip}|-| |\(|\)|[a-zA-Z:]))+$", value) if not regex_match: raise ValidationError( - _('%(value)s includes invalid characters or tokens ({name}, {ip}, A-Z, a-z, :, (, and ) allowed.'), - params={'value': value}, + _( + "%(value)s includes invalid characters or tokens ({name}, {ip}, A-Z, a-z, :, (, and ) allowed." + ), + params={"value": value}, ) diff --git a/missions/management/commands/removeallusers.py b/missions/management/commands/removeallusers.py index 08f8b42..0d0ee25 100644 --- a/missions/management/commands/removeallusers.py +++ b/missions/management/commands/removeallusers.py @@ -13,18 +13,19 @@ # limitations under the License. # -from django.core.management.base import BaseCommand, CommandError +from django.core.management.base import BaseCommand from django.contrib.auth import get_user_model class Command(BaseCommand): - help = 'removes all users in the system, but leaves the data intact' + help = "removes all users in the system, but leaves the data intact" def handle(self, *args, **options): - User = get_user_model() users = User.objects.all() users.delete() - self.stdout.write('Successfully deleted all users. You should now be able to create a new user account via the ' - 'web interface.') + self.stdout.write( + "Successfully deleted all users. You should now be able to create a new user account via the " + "web interface." + ) diff --git a/missions/management/commands/setup-db.py b/missions/management/commands/setup-db.py index 15527c1..e2f36ab 100644 --- a/missions/management/commands/setup-db.py +++ b/missions/management/commands/setup-db.py @@ -27,36 +27,35 @@ import subprocess import sys -MANAGER = os.path.join(os. getcwd(), "manage.py") +MANAGER = os.path.join(os.getcwd(), "manage.py") PYTHON_INTERPRETER = sys.executable -print('\n\nDART First Run Script\nCreated by the Lockheed Martin Red Team') +print("\n\nDART First Run Script\nCreated by the Lockheed Martin Red Team") input("\n\nPress Enter to continue...") -print('\nEnsuring all migrations are made...') +print("\nEnsuring all migrations are made...") subprocess.call([PYTHON_INTERPRETER, MANAGER, "makemigrations"]) -print('\nExecuting migrations...') +print("\nExecuting migrations...") subprocess.call([PYTHON_INTERPRETER, MANAGER, "migrate"]) -print('\nSeeding the database...\nIf this is not the first time running this script. This step will reset to the default colors, classifications, or BAs from the initial data load. New entries will not be affected.') +print( + "\nSeeding the database...\nIf this is not the first time running this script. This step will reset to the default colors, classifications, or BAs from the initial data load. New entries will not be affected." +) seed_data = input("\nDo you want to seed the database? [y/N]") -if len(seed_data) and seed_data.strip().upper()[0] == 'Y': - print('\nRunning Fixture: common_classifications') - subprocess.call([PYTHON_INTERPRETER, MANAGER, "loaddata", - "common_classifications"]) +if len(seed_data) and seed_data.strip().upper()[0] == "Y": + print("\nRunning Fixture: common_classifications") + subprocess.call([PYTHON_INTERPRETER, MANAGER, "loaddata", "common_classifications"]) - print('\nRunning Fixture: common_bas') - subprocess.call([PYTHON_INTERPRETER, MANAGER, - "loaddata", "common_bas"]) + print("\nRunning Fixture: common_bas") + subprocess.call([PYTHON_INTERPRETER, MANAGER, "loaddata", "common_bas"]) add_user = input("\nDo you want add a super user? [y/N]") -while len(add_user) and add_user.strip().upper()[0] == 'Y': - subprocess.call([PYTHON_INTERPRETER, MANAGER, - "createsuperuser"]) +while len(add_user) and add_user.strip().upper()[0] == "Y": + subprocess.call([PYTHON_INTERPRETER, MANAGER, "createsuperuser"]) add_user = input("\nDo you want add a user? [y/N]") diff --git a/missions/middleware.py b/missions/middleware.py index 340c249..b292e9d 100644 --- a/missions/middleware.py +++ b/missions/middleware.py @@ -21,9 +21,6 @@ from django.core.urlresolvers import reverse_lazy from django.shortcuts import redirect from django.utils.timezone import timedelta, now -from django.contrib.auth import login -from django.contrib.auth.models import User -from django.http.response import HttpResponseServerError logger = logging.getLogger(__name__) @@ -48,22 +45,30 @@ def process_request(self, request): # Setting not defined, so assume we don't want the interstitial to display return None try: - if display_interval == 0 \ - and request.session['last_acknowledged_interstitial']: + if ( + display_interval == 0 + and request.session["last_acknowledged_interstitial"] + ): return None else: max_age = timedelta(hours=display_interval).total_seconds() - if timegm(now().timetuple()) - request.session['last_acknowledged_interstitial'] < max_age: + if ( + timegm(now().timetuple()) + - request.session["last_acknowledged_interstitial"] + < max_age + ): return None except KeyError: pass path = request.get_full_path() - if re.match(str(reverse_lazy('login-interstitial')), path) or \ - re.match(str(reverse_lazy('login')), path) or \ - re.match(str(reverse_lazy('logout')), path) or \ - re.match(settings.STATIC_URL + r'.+', path): + if ( + re.match(str(reverse_lazy("login-interstitial")), path) + or re.match(str(reverse_lazy("login")), path) + or re.match(str(reverse_lazy("logout")), path) + or re.match(settings.STATIC_URL + r".+", path) + ): return None - return redirect('login-interstitial') + return redirect("login-interstitial") diff --git a/missions/migrations/0001_initial.py b/missions/migrations/0001_initial.py index 2ca9262..f947b5e 100644 --- a/missions/migrations/0001_initial.py +++ b/missions/migrations/0001_initial.py @@ -23,151 +23,632 @@ class Migration(migrations.Migration): - - dependencies = [ - ] + dependencies = [] operations = [ migrations.CreateModel( - name='BusinessArea', + name="BusinessArea", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('name', models.TextField()), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("name", models.TextField()), ], ), migrations.CreateModel( - name='ClassificationLegend', + name="ClassificationLegend", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('verbose_legend', models.CharField(default=b'', max_length=200)), - ('short_legend', models.CharField(default=b'', max_length=100)), - ('report_label_color_selection', models.CharField(default=b'B', max_length=1, choices=[(b'T', b'Text Color'), (b'B', b'Back Color')])), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("verbose_legend", models.CharField(default=b"", max_length=200)), + ("short_legend", models.CharField(default=b"", max_length=100)), + ( + "report_label_color_selection", + models.CharField( + default=b"B", + max_length=1, + choices=[(b"T", b"Text Color"), (b"B", b"Back Color")], + ), + ), ], ), migrations.CreateModel( - name='Color', + name="Color", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('display_text', models.CharField(default=b'', max_length=30)), - ('hex_color_code', models.CharField(default=b'', max_length=6)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("display_text", models.CharField(default=b"", max_length=30)), + ("hex_color_code", models.CharField(default=b"", max_length=6)), ], ), migrations.CreateModel( - name='DARTDynamicSettings', + name="DARTDynamicSettings", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('host_output_format', models.CharField(default=b'{ip} ({name})', help_text=b'Use "{ip}" and "{name}" to specify how you want hosts to be displayed.', max_length=50, validators=[missions.extras.validators.validate_host_format_string])), - ('system_classification', models.ForeignKey(to='missions.ClassificationLegend', on_delete=models.CASCADE)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ( + "host_output_format", + models.CharField( + default=b"{ip} ({name})", + help_text=b'Use "{ip}" and "{name}" to specify how you want hosts to be displayed.', + max_length=50, + validators=[ + missions.extras.validators.validate_host_format_string + ], + ), + ), + ( + "system_classification", + models.ForeignKey( + to="missions.ClassificationLegend", on_delete=models.CASCADE + ), + ), ], ), migrations.CreateModel( - name='Host', + name="Host", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('host_name', models.CharField(default=b'', max_length=100, blank=True)), - ('ip_address', models.GenericIPAddressField(null=True, blank=True)), - ('is_no_hit', models.BooleanField(default=False)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ( + "host_name", + models.CharField(default=b"", max_length=100, blank=True), + ), + ("ip_address", models.GenericIPAddressField(null=True, blank=True)), + ("is_no_hit", models.BooleanField(default=False)), ], ), migrations.CreateModel( - name='Mission', + name="Mission", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('mission_name', models.CharField(max_length=255, verbose_name=b'Mission Name')), - ('mission_number', models.CharField(max_length=5, verbose_name=b'Mission Number')), - ('introduction', models.TextField(default=missions.models.introduction_default, verbose_name=b'Introduction', blank=True)), - ('executive_summary', models.TextField(default=missions.models.executive_summary_default, verbose_name=b'Executive Summary', blank=True)), - ('scope', models.TextField(default=missions.models.scope_default, verbose_name=b'Scope', blank=True)), - ('objectives', models.TextField(default=missions.models.objectives_default, verbose_name=b'Objectives', blank=True)), - ('technical_assessment_overview', models.TextField(default=missions.models.technical_assessment_overview_default, verbose_name=b'Technical Assessment / Attack Architecture Overview', blank=True)), - ('conclusion', models.TextField(default=missions.models.conclusion_default, verbose_name=b'Conclusion', blank=True)), - ('attack_phase_include_flag', models.BooleanField(default=True, verbose_name=b'Mission Option: Include Attack Phases in report?')), - ('attack_type_include_flag', models.BooleanField(default=True, verbose_name=b'Mission Option: Include Attack Types in report?')), - ('assumptions_include_flag', models.BooleanField(default=True, verbose_name=b'Mission Option: Include Assumptions in report?')), - ('test_description_include_flag', models.BooleanField(default=True, verbose_name=b'Mission Option: Include Test Descriptions in report?')), - ('findings_include_flag', models.BooleanField(default=True, verbose_name=b'Mission Option: Include Findings in report?')), - ('mitigation_include_flag', models.BooleanField(default=True, verbose_name=b'Mission Option: Include Mitigations in report?')), - ('tools_used_include_flag', models.BooleanField(default=True, verbose_name=b'Mission Option: Include Tools Used in report?')), - ('command_syntax_include_flag', models.BooleanField(default=True, verbose_name=b'Mission Option: Include Command Syntaxes in report?')), - ('targets_include_flag', models.BooleanField(default=True, verbose_name=b'Mission Option: Include Attack Targets in report?')), - ('sources_include_flag', models.BooleanField(default=True, verbose_name=b'Mission Option: Include Attack Sources in report?')), - ('attack_time_date_include_flag', models.BooleanField(default=True, verbose_name=b'Mission Option: Include Attack Times in report?')), - ('attack_side_effects_include_flag', models.BooleanField(default=True, verbose_name=b'Mission Option: Include Attack Side Effects in report?')), - ('test_result_observation_include_flag', models.BooleanField(default=True, verbose_name=b'Mission Option: Include Test Details in report?')), - ('supporting_data_include_flag', models.BooleanField(default=True, verbose_name=b'Mission Option: Include Supporting Data Information in report?')), - ('customer_notes_include_flag', models.BooleanField(default=True, verbose_name=b'Mission Option: Include customer notes section in report?')), - ('testdetail_sort_order', models.TextField(default=b'[]', blank=True)), - ('business_area', models.ForeignKey(verbose_name=b'Business Area', to='missions.BusinessArea',on_delete=models.CASCADE)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ( + "mission_name", + models.CharField(max_length=255, verbose_name=b"Mission Name"), + ), + ( + "mission_number", + models.CharField(max_length=5, verbose_name=b"Mission Number"), + ), + ( + "introduction", + models.TextField( + default=missions.models.introduction_default, + verbose_name=b"Introduction", + blank=True, + ), + ), + ( + "executive_summary", + models.TextField( + default=missions.models.executive_summary_default, + verbose_name=b"Executive Summary", + blank=True, + ), + ), + ( + "scope", + models.TextField( + default=missions.models.scope_default, + verbose_name=b"Scope", + blank=True, + ), + ), + ( + "objectives", + models.TextField( + default=missions.models.objectives_default, + verbose_name=b"Objectives", + blank=True, + ), + ), + ( + "technical_assessment_overview", + models.TextField( + default=missions.models.technical_assessment_overview_default, + verbose_name=b"Technical Assessment / Attack Architecture Overview", + blank=True, + ), + ), + ( + "conclusion", + models.TextField( + default=missions.models.conclusion_default, + verbose_name=b"Conclusion", + blank=True, + ), + ), + ( + "attack_phase_include_flag", + models.BooleanField( + default=True, + verbose_name=b"Mission Option: Include Attack Phases in report?", + ), + ), + ( + "attack_type_include_flag", + models.BooleanField( + default=True, + verbose_name=b"Mission Option: Include Attack Types in report?", + ), + ), + ( + "assumptions_include_flag", + models.BooleanField( + default=True, + verbose_name=b"Mission Option: Include Assumptions in report?", + ), + ), + ( + "test_description_include_flag", + models.BooleanField( + default=True, + verbose_name=b"Mission Option: Include Test Descriptions in report?", + ), + ), + ( + "findings_include_flag", + models.BooleanField( + default=True, + verbose_name=b"Mission Option: Include Findings in report?", + ), + ), + ( + "mitigation_include_flag", + models.BooleanField( + default=True, + verbose_name=b"Mission Option: Include Mitigations in report?", + ), + ), + ( + "tools_used_include_flag", + models.BooleanField( + default=True, + verbose_name=b"Mission Option: Include Tools Used in report?", + ), + ), + ( + "command_syntax_include_flag", + models.BooleanField( + default=True, + verbose_name=b"Mission Option: Include Command Syntaxes in report?", + ), + ), + ( + "targets_include_flag", + models.BooleanField( + default=True, + verbose_name=b"Mission Option: Include Attack Targets in report?", + ), + ), + ( + "sources_include_flag", + models.BooleanField( + default=True, + verbose_name=b"Mission Option: Include Attack Sources in report?", + ), + ), + ( + "attack_time_date_include_flag", + models.BooleanField( + default=True, + verbose_name=b"Mission Option: Include Attack Times in report?", + ), + ), + ( + "attack_side_effects_include_flag", + models.BooleanField( + default=True, + verbose_name=b"Mission Option: Include Attack Side Effects in report?", + ), + ), + ( + "test_result_observation_include_flag", + models.BooleanField( + default=True, + verbose_name=b"Mission Option: Include Test Details in report?", + ), + ), + ( + "supporting_data_include_flag", + models.BooleanField( + default=True, + verbose_name=b"Mission Option: Include Supporting Data Information in report?", + ), + ), + ( + "customer_notes_include_flag", + models.BooleanField( + default=True, + verbose_name=b"Mission Option: Include customer notes section in report?", + ), + ), + ("testdetail_sort_order", models.TextField(default=b"[]", blank=True)), + ( + "business_area", + models.ForeignKey( + verbose_name=b"Business Area", + to="missions.BusinessArea", + on_delete=models.CASCADE, + ), + ), ], ), migrations.CreateModel( - name='SupportingData', + name="SupportingData", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('caption', models.TextField(verbose_name=b'Caption', blank=True)), - ('include_flag', models.BooleanField(default=True, verbose_name=b'Include attachment in report')), - ('test_file', models.FileField(upload_to=b'', verbose_name=b'Supporting Data')), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("caption", models.TextField(verbose_name=b"Caption", blank=True)), + ( + "include_flag", + models.BooleanField( + default=True, verbose_name=b"Include attachment in report" + ), + ), + ( + "test_file", + models.FileField(upload_to=b"", verbose_name=b"Supporting Data"), + ), ], ), migrations.CreateModel( - name='TestDetail', + name="TestDetail", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('test_number', models.IntegerField(default=0, verbose_name=b'Test Number')), - ('test_case_include_flag', models.BooleanField(default=True, verbose_name=b'Include Test Case in report?')), - ('test_case_status', models.CharField(default=b'NEW', max_length=100, verbose_name=b'Test Case Status', choices=[(b'NEW', b'Not started'), (b'IN_WORK', b'In work'), (b'REVIEW', b'Ready for review'), (b'FINAL', b'Approved / Final')])), - ('enclave', models.CharField(max_length=100, verbose_name=b'Enclave Test Executed From', blank=True)), - ('test_objective', models.CharField(help_text=b'Brief objective (ex.Port Scan against xyz)', max_length=255, verbose_name=b'Test Objective / Title')), - ('attack_phase', models.CharField(max_length=20, verbose_name=b'Attack Phase', choices=[(b'RECON', b'Reconnaissance'), (b'WEP', b'Weaponization'), (b'DEL', b'Delivery'), (b'EXP', b'Exploitation'), (b'INS', b'Installation'), (b'C2', b'Command & Control'), (b'AOO', b'Actions on Objectives')])), - ('attack_phase_include_flag', models.BooleanField(default=True, verbose_name=b'Include Attack Phase in report?')), - ('attack_type', models.CharField(help_text=b'Example: SYN flood, UDP flood, malformed packets, web, fuzz, etc.', max_length=255, verbose_name=b'Attack Type', blank=True)), - ('attack_type_include_flag', models.BooleanField(default=True, verbose_name=b'Include Attack Type in report?')), - ('assumptions', models.TextField(help_text=b'Example: Attacker has a presence in xyz segment, etc.', verbose_name=b'Assumptions', blank=True)), - ('assumptions_include_flag', models.BooleanField(default=True, verbose_name=b'Include Assumptions in report?')), - ('test_description', models.TextField(help_text=b'Describe test case to be performed, what is the objective of this test case (Is it to deny, disrupt, penetrate, modify etc.)', verbose_name=b'Description', blank=True)), - ('test_description_include_flag', models.BooleanField(default=True, verbose_name=b'Include Test Description in report?')), - ('sources_include_flag', models.BooleanField(default=True, verbose_name=b'Include Attack Sources in report?')), - ('targets_include_flag', models.BooleanField(default=True, verbose_name=b'Include Attack Targets in report?')), - ('attack_time_date', models.DateTimeField(default=django.utils.timezone.now, help_text=b'Date/time attack was launched', verbose_name=b'Attack Date / Time', blank=True)), - ('attack_time_date_include_flag', models.BooleanField(default=True, verbose_name=b'Include Attack Time in report?')), - ('tools_used', models.TextField(help_text=b'Example: Burp Suite, Wireshark, NMap, etc.', verbose_name=b'Tools Used', blank=True)), - ('tools_used_include_flag', models.BooleanField(default=True, verbose_name=b'Include Tools Used in report?')), - ('command_syntax', models.TextField(help_text=b'Include sample command/syntax used if possible. If a script was used/created, what commands will run it?', verbose_name=b'Command/Syntax', blank=True)), - ('command_syntax_include_flag', models.BooleanField(default=True, verbose_name=b'Include Command Syntax in report?')), - ('test_result_observation', models.TextField(help_text=b'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 \xe2\x80\x93 40s after which they recovered.', verbose_name=b'Test Result Details', blank=True)), - ('test_result_observation_include_flag', models.BooleanField(default=True, verbose_name=b'Include Test Result in report?')), - ('attack_side_effects', models.TextField(help_text=b'List any observed side effects, if any. Example: Firewall X froze and subsequently crashed.', verbose_name=b'Attack Side Effects', blank=True)), - ('attack_side_effects_include_flag', models.BooleanField(default=True, verbose_name=b'Include Attack Side Effects in report?')), - ('execution_status', models.CharField(default=b'N', help_text=b'Execution status of the test case.', max_length=2, verbose_name=b'Execution Status', choices=[(b'N', b'Not Run'), (b'R', b'Run'), (b'C', b'Cancelled'), (b'NA', b'N/A')])), - ('has_findings', models.BooleanField(default=False)), - ('findings', models.TextField(help_text=b'Document the specific finding related to this test or against the main test objectives if applicable', verbose_name=b'Findings', blank=True)), - ('findings_include_flag', models.BooleanField(default=True, verbose_name=b'Include Findings in report?')), - ('mitigation', models.TextField(help_text=b'Example: Configure xyz, implement xyz, etc.', verbose_name=b'Mitigation', blank=True)), - ('mitigation_include_flag', models.BooleanField(default=True, verbose_name=b'Include Mitigations in report?')), - ('point_of_contact', models.CharField(default=b'', help_text=b'Individual working or most familiar with this test case.', max_length=20, verbose_name=b'POC', blank=True)), - ('mission', models.ForeignKey(verbose_name=b'Mission', to='missions.Mission',on_delete=models.CASCADE)), - ('source_hosts', models.ManyToManyField(related_name='source_set', to='missions.Host')), - ('target_hosts', models.ManyToManyField(related_name='target_set', to='missions.Host')), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ( + "test_number", + models.IntegerField(default=0, verbose_name=b"Test Number"), + ), + ( + "test_case_include_flag", + models.BooleanField( + default=True, verbose_name=b"Include Test Case in report?" + ), + ), + ( + "test_case_status", + models.CharField( + default=b"NEW", + max_length=100, + verbose_name=b"Test Case Status", + choices=[ + (b"NEW", b"Not started"), + (b"IN_WORK", b"In work"), + (b"REVIEW", b"Ready for review"), + (b"FINAL", b"Approved / Final"), + ], + ), + ), + ( + "enclave", + models.CharField( + max_length=100, + verbose_name=b"Enclave Test Executed From", + blank=True, + ), + ), + ( + "test_objective", + models.CharField( + help_text=b"Brief objective (ex.Port Scan against xyz)", + max_length=255, + verbose_name=b"Test Objective / Title", + ), + ), + ( + "attack_phase", + models.CharField( + max_length=20, + verbose_name=b"Attack Phase", + choices=[ + (b"RECON", b"Reconnaissance"), + (b"WEP", b"Weaponization"), + (b"DEL", b"Delivery"), + (b"EXP", b"Exploitation"), + (b"INS", b"Installation"), + (b"C2", b"Command & Control"), + (b"AOO", b"Actions on Objectives"), + ], + ), + ), + ( + "attack_phase_include_flag", + models.BooleanField( + default=True, verbose_name=b"Include Attack Phase in report?" + ), + ), + ( + "attack_type", + models.CharField( + help_text=b"Example: SYN flood, UDP flood, malformed packets, web, fuzz, etc.", + max_length=255, + verbose_name=b"Attack Type", + blank=True, + ), + ), + ( + "attack_type_include_flag", + models.BooleanField( + default=True, verbose_name=b"Include Attack Type in report?" + ), + ), + ( + "assumptions", + models.TextField( + help_text=b"Example: Attacker has a presence in xyz segment, etc.", + verbose_name=b"Assumptions", + blank=True, + ), + ), + ( + "assumptions_include_flag", + models.BooleanField( + default=True, verbose_name=b"Include Assumptions in report?" + ), + ), + ( + "test_description", + models.TextField( + help_text=b"Describe test case to be performed, what is the objective of this test case (Is it to deny, disrupt, penetrate, modify etc.)", + verbose_name=b"Description", + blank=True, + ), + ), + ( + "test_description_include_flag", + models.BooleanField( + default=True, + verbose_name=b"Include Test Description in report?", + ), + ), + ( + "sources_include_flag", + models.BooleanField( + default=True, verbose_name=b"Include Attack Sources in report?" + ), + ), + ( + "targets_include_flag", + models.BooleanField( + default=True, verbose_name=b"Include Attack Targets in report?" + ), + ), + ( + "attack_time_date", + models.DateTimeField( + default=django.utils.timezone.now, + help_text=b"Date/time attack was launched", + verbose_name=b"Attack Date / Time", + blank=True, + ), + ), + ( + "attack_time_date_include_flag", + models.BooleanField( + default=True, verbose_name=b"Include Attack Time in report?" + ), + ), + ( + "tools_used", + models.TextField( + help_text=b"Example: Burp Suite, Wireshark, NMap, etc.", + verbose_name=b"Tools Used", + blank=True, + ), + ), + ( + "tools_used_include_flag", + models.BooleanField( + default=True, verbose_name=b"Include Tools Used in report?" + ), + ), + ( + "command_syntax", + models.TextField( + help_text=b"Include sample command/syntax used if possible. If a script was used/created, what commands will run it?", + verbose_name=b"Command/Syntax", + blank=True, + ), + ), + ( + "command_syntax_include_flag", + models.BooleanField( + default=True, verbose_name=b"Include Command Syntax in report?" + ), + ), + ( + "test_result_observation", + models.TextField( + help_text=b"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 \xe2\x80\x93 40s after which they recovered.", + verbose_name=b"Test Result Details", + blank=True, + ), + ), + ( + "test_result_observation_include_flag", + models.BooleanField( + default=True, verbose_name=b"Include Test Result in report?" + ), + ), + ( + "attack_side_effects", + models.TextField( + help_text=b"List any observed side effects, if any. Example: Firewall X froze and subsequently crashed.", + verbose_name=b"Attack Side Effects", + blank=True, + ), + ), + ( + "attack_side_effects_include_flag", + models.BooleanField( + default=True, + verbose_name=b"Include Attack Side Effects in report?", + ), + ), + ( + "execution_status", + models.CharField( + default=b"N", + help_text=b"Execution status of the test case.", + max_length=2, + verbose_name=b"Execution Status", + choices=[ + (b"N", b"Not Run"), + (b"R", b"Run"), + (b"C", b"Cancelled"), + (b"NA", b"N/A"), + ], + ), + ), + ("has_findings", models.BooleanField(default=False)), + ( + "findings", + models.TextField( + help_text=b"Document the specific finding related to this test or against the main test objectives if applicable", + verbose_name=b"Findings", + blank=True, + ), + ), + ( + "findings_include_flag", + models.BooleanField( + default=True, verbose_name=b"Include Findings in report?" + ), + ), + ( + "mitigation", + models.TextField( + help_text=b"Example: Configure xyz, implement xyz, etc.", + verbose_name=b"Mitigation", + blank=True, + ), + ), + ( + "mitigation_include_flag", + models.BooleanField( + default=True, verbose_name=b"Include Mitigations in report?" + ), + ), + ( + "point_of_contact", + models.CharField( + default=b"", + help_text=b"Individual working or most familiar with this test case.", + max_length=20, + verbose_name=b"POC", + blank=True, + ), + ), + ( + "mission", + models.ForeignKey( + verbose_name=b"Mission", + to="missions.Mission", + on_delete=models.CASCADE, + ), + ), + ( + "source_hosts", + models.ManyToManyField( + related_name="source_set", to="missions.Host" + ), + ), + ( + "target_hosts", + models.ManyToManyField( + related_name="target_set", to="missions.Host" + ), + ), ], ), migrations.AddField( - model_name='supportingdata', - name='test_detail', - field=models.ForeignKey(verbose_name=b'Test Details', to='missions.TestDetail',on_delete=models.CASCADE), + model_name="supportingdata", + name="test_detail", + field=models.ForeignKey( + verbose_name=b"Test Details", + to="missions.TestDetail", + on_delete=models.CASCADE, + ), ), migrations.AddField( - model_name='host', - name='mission', - field=models.ForeignKey(to='missions.Mission',on_delete=models.CASCADE), + model_name="host", + name="mission", + field=models.ForeignKey(to="missions.Mission", on_delete=models.CASCADE), ), migrations.AddField( - model_name='classificationlegend', - name='background_color', - field=models.ForeignKey(related_name='classificationlegend_background_set', to='missions.Color',on_delete=models.CASCADE), + model_name="classificationlegend", + name="background_color", + field=models.ForeignKey( + related_name="classificationlegend_background_set", + to="missions.Color", + on_delete=models.CASCADE, + ), ), migrations.AddField( - model_name='classificationlegend', - name='text_color', - field=models.ForeignKey(related_name='classificationlegend_text_set', to='missions.Color',on_delete=models.CASCADE), + model_name="classificationlegend", + name="text_color", + field=models.ForeignKey( + related_name="classificationlegend_text_set", + to="missions.Color", + on_delete=models.CASCADE, + ), ), ] diff --git a/missions/migrations/0002_dataload_minimal_classification.py b/missions/migrations/0002_dataload_minimal_classification.py index 0f7edd0..fe62abe 100644 --- a/missions/migrations/0002_dataload_minimal_classification.py +++ b/missions/migrations/0002_dataload_minimal_classification.py @@ -31,24 +31,23 @@ def load_data(apps, schema_editor): - # Colors - Color = apps.get_model('missions', 'Color') - black = Color.objects.create(display_text='Black', hex_color_code='000000') - white = Color.objects.create(display_text='White', hex_color_code='ffffff') + Color = apps.get_model("missions", "Color") + black = Color.objects.create(display_text="Black", hex_color_code="000000") + white = Color.objects.create(display_text="White", hex_color_code="ffffff") # Legends - ClassificationLegend = apps.get_model('missions', 'ClassificationLegend') + ClassificationLegend = apps.get_model("missions", "ClassificationLegend") unrestricted = ClassificationLegend.objects.create( - verbose_legend='UNRESTRICTED', - short_legend='', + verbose_legend="UNRESTRICTED", + short_legend="", text_color=white, background_color=black, - report_label_color_selection='B', + report_label_color_selection="B", ) # Create an initial (default) settings load - DARTDynamicSettings = apps.get_model('missions', 'DARTDynamicSettings') + DARTDynamicSettings = apps.get_model("missions", "DARTDynamicSettings") qryset = DARTDynamicSettings.objects.all() @@ -64,11 +63,8 @@ def unload_data(apps, schema_editor): class Migration(migrations.Migration): - dependencies = [ - ('missions', '0001_initial'), + ("missions", "0001_initial"), ] - operations = [ - migrations.RunPython(load_data, unload_data) - ] + operations = [migrations.RunPython(load_data, unload_data)] diff --git a/missions/migrations/0003_auto_event-id.py b/missions/migrations/0003_auto_event-id.py index 3ccf55e..77d3f8f 100644 --- a/missions/migrations/0003_auto_event-id.py +++ b/missions/migrations/0003_auto_event-id.py @@ -20,20 +20,30 @@ class Migration(migrations.Migration): - dependencies = [ - ('missions', '0002_dataload_minimal_classification'), + ("missions", "0002_dataload_minimal_classification"), ] operations = [ migrations.AddField( - model_name='mission', - name='test_case_identifier', - field=models.CharField(default=b'', max_length=20, verbose_name=b'Test Case Identifier', blank=True), + model_name="mission", + name="test_case_identifier", + field=models.CharField( + default=b"", + max_length=20, + verbose_name=b"Test Case Identifier", + blank=True, + ), ), migrations.AddField( - model_name='testdetail', - name='re_eval_test_case_number', - field=models.CharField(default=b'', help_text=b'Adds previous test case reference to description in report.', max_length=25, verbose_name=b'Re-Evaluate Test Case #', blank=True), + model_name="testdetail", + name="re_eval_test_case_number", + field=models.CharField( + default=b"", + help_text=b"Adds previous test case reference to description in report.", + max_length=25, + verbose_name=b"Re-Evaluate Test Case #", + blank=True, + ), ), ] diff --git a/missions/migrations/0004_testdetail_supporting_data_sort_order.py b/missions/migrations/0004_testdetail_supporting_data_sort_order.py index de5205a..7abc0fa 100644 --- a/missions/migrations/0004_testdetail_supporting_data_sort_order.py +++ b/missions/migrations/0004_testdetail_supporting_data_sort_order.py @@ -21,15 +21,14 @@ class Migration(migrations.Migration): - dependencies = [ - ('missions', '0003_auto_event-id'), + ("missions", "0003_auto_event-id"), ] operations = [ migrations.AddField( - model_name='testdetail', - name='supporting_data_sort_order', - field=models.TextField(blank=True, default=b'[]'), + model_name="testdetail", + name="supporting_data_sort_order", + field=models.TextField(blank=True, default=b"[]"), ), ] diff --git a/missions/migrations/0005_auto_20230708_1306.py b/missions/migrations/0005_auto_20230708_1306.py index a63b9e6..3b9cc14 100644 --- a/missions/migrations/0005_auto_20230708_1306.py +++ b/missions/migrations/0005_auto_20230708_1306.py @@ -23,365 +23,579 @@ class Migration(migrations.Migration): - dependencies = [ - ('missions', '0004_testdetail_supporting_data_sort_order'), + ("missions", "0004_testdetail_supporting_data_sort_order"), ] operations = [ migrations.AlterField( - model_name='classificationlegend', - name='report_label_color_selection', - field=models.CharField(choices=[('T', 'Text Color'), ('B', 'Back Color')], default='B', max_length=1), - ), - migrations.AlterField( - model_name='classificationlegend', - name='short_legend', - field=models.CharField(default='', max_length=100), - ), - migrations.AlterField( - model_name='classificationlegend', - name='verbose_legend', - field=models.CharField(default='', max_length=200), - ), - migrations.AlterField( - model_name='color', - name='display_text', - field=models.CharField(default='', max_length=30), - ), - migrations.AlterField( - model_name='color', - name='hex_color_code', - field=models.CharField(default='', max_length=6), - ), - migrations.AlterField( - model_name='dartdynamicsettings', - name='host_output_format', - field=models.CharField(default='{ip} ({name})', help_text='Use "{ip}" and "{name}" to specify how you want hosts to be displayed.', max_length=50, validators=[missions.extras.validators.validate_host_format_string]), - ), - migrations.AlterField( - model_name='host', - name='host_name', - field=models.CharField(blank=True, default='', max_length=100), - ), - migrations.AlterField( - model_name='mission', - name='assumptions_include_flag', - field=models.BooleanField(default=True, verbose_name='Mission Option: Include Assumptions in report?'), - ), - migrations.AlterField( - model_name='mission', - name='attack_phase_include_flag', - field=models.BooleanField(default=True, verbose_name='Mission Option: Include Attack Phases in report?'), - ), - migrations.AlterField( - model_name='mission', - name='attack_side_effects_include_flag', - field=models.BooleanField(default=True, verbose_name='Mission Option: Include Attack Side Effects in report?'), - ), - migrations.AlterField( - model_name='mission', - name='attack_time_date_include_flag', - field=models.BooleanField(default=True, verbose_name='Mission Option: Include Attack Times in report?'), - ), - migrations.AlterField( - model_name='mission', - name='attack_type_include_flag', - field=models.BooleanField(default=True, verbose_name='Mission Option: Include Attack Types in report?'), - ), - migrations.AlterField( - model_name='mission', - name='business_area', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='missions.businessarea', verbose_name='Business Area'), - ), - migrations.AlterField( - model_name='mission', - name='command_syntax_include_flag', - field=models.BooleanField(default=True, verbose_name='Mission Option: Include Command Syntaxes in report?'), - ), - migrations.AlterField( - model_name='mission', - name='conclusion', - field=models.TextField(blank=True, default=missions.models.conclusion_default, verbose_name='Conclusion'), - ), - migrations.AlterField( - model_name='mission', - name='customer_notes_include_flag', - field=models.BooleanField(default=True, verbose_name='Mission Option: Include customer notes section in report?'), - ), - migrations.AlterField( - model_name='mission', - name='executive_summary', - field=models.TextField(blank=True, default=missions.models.executive_summary_default, verbose_name='Executive Summary'), - ), - migrations.AlterField( - model_name='mission', - name='findings_include_flag', - field=models.BooleanField(default=True, verbose_name='Mission Option: Include Findings in report?'), - ), - migrations.AlterField( - model_name='mission', - name='introduction', - field=models.TextField(blank=True, default=missions.models.introduction_default, verbose_name='Introduction'), - ), - migrations.AlterField( - model_name='mission', - name='mission_name', - field=models.CharField(max_length=255, verbose_name='Mission Name'), - ), - migrations.AlterField( - model_name='mission', - name='mission_number', - field=models.CharField(max_length=5, verbose_name='Mission Number'), - ), - migrations.AlterField( - model_name='mission', - name='mitigation_include_flag', - field=models.BooleanField(default=True, verbose_name='Mission Option: Include Mitigations in report?'), - ), - migrations.AlterField( - model_name='mission', - name='objectives', - field=models.TextField(blank=True, default=missions.models.objectives_default, verbose_name='Objectives'), - ), - migrations.AlterField( - model_name='mission', - name='scope', - field=models.TextField(blank=True, default=missions.models.scope_default, verbose_name='Scope'), - ), - migrations.AlterField( - model_name='mission', - name='sources_include_flag', - field=models.BooleanField(default=True, verbose_name='Mission Option: Include Attack Sources in report?'), - ), - migrations.AlterField( - model_name='mission', - name='supporting_data_include_flag', - field=models.BooleanField(default=True, verbose_name='Mission Option: Include Supporting Data Information in report?'), - ), - migrations.AlterField( - model_name='mission', - name='targets_include_flag', - field=models.BooleanField(default=True, verbose_name='Mission Option: Include Attack Targets in report?'), - ), - migrations.AlterField( - model_name='mission', - name='technical_assessment_overview', - field=models.TextField(blank=True, default=missions.models.technical_assessment_overview_default, verbose_name='Technical Assessment / Attack Architecture Overview'), - ), - migrations.AlterField( - model_name='mission', - name='test_case_identifier', - field=models.CharField(blank=True, default='', max_length=20, verbose_name='Test Case Identifier'), - ), - migrations.AlterField( - model_name='mission', - name='test_description_include_flag', - field=models.BooleanField(default=True, verbose_name='Mission Option: Include Test Descriptions in report?'), - ), - migrations.AlterField( - model_name='mission', - name='test_result_observation_include_flag', - field=models.BooleanField(default=True, verbose_name='Mission Option: Include Test Details in report?'), + model_name="classificationlegend", + name="report_label_color_selection", + field=models.CharField( + choices=[("T", "Text Color"), ("B", "Back Color")], + default="B", + max_length=1, + ), ), migrations.AlterField( - model_name='mission', - name='testdetail_sort_order', - field=models.TextField(blank=True, default='[]'), + model_name="classificationlegend", + name="short_legend", + field=models.CharField(default="", max_length=100), ), migrations.AlterField( - model_name='mission', - name='tools_used_include_flag', - field=models.BooleanField(default=True, verbose_name='Mission Option: Include Tools Used in report?'), + model_name="classificationlegend", + name="verbose_legend", + field=models.CharField(default="", max_length=200), ), migrations.AlterField( - model_name='supportingdata', - name='caption', - field=models.TextField(blank=True, verbose_name='Caption'), + model_name="color", + name="display_text", + field=models.CharField(default="", max_length=30), ), migrations.AlterField( - model_name='supportingdata', - name='include_flag', - field=models.BooleanField(default=True, verbose_name='Include attachment in report'), + model_name="color", + name="hex_color_code", + field=models.CharField(default="", max_length=6), ), migrations.AlterField( - model_name='supportingdata', - name='test_detail', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='missions.testdetail', verbose_name='Test Details'), + model_name="dartdynamicsettings", + name="host_output_format", + field=models.CharField( + default="{ip} ({name})", + help_text='Use "{ip}" and "{name}" to specify how you want hosts to be displayed.', + max_length=50, + validators=[missions.extras.validators.validate_host_format_string], + ), ), migrations.AlterField( - model_name='supportingdata', - name='test_file', - field=models.FileField(upload_to='', verbose_name='Supporting Data'), + model_name="host", + name="host_name", + field=models.CharField(blank=True, default="", max_length=100), ), migrations.AlterField( - model_name='testdetail', - name='assumptions', - field=models.TextField(blank=True, help_text='Example: Attacker has a presence in xyz segment, etc.', verbose_name='Assumptions'), + model_name="mission", + name="assumptions_include_flag", + field=models.BooleanField( + default=True, + verbose_name="Mission Option: Include Assumptions in report?", + ), ), migrations.AlterField( - model_name='testdetail', - name='assumptions_include_flag', - field=models.BooleanField(default=True, verbose_name='Include Assumptions in report?'), + model_name="mission", + name="attack_phase_include_flag", + field=models.BooleanField( + default=True, + verbose_name="Mission Option: Include Attack Phases in report?", + ), ), migrations.AlterField( - model_name='testdetail', - name='attack_phase', - field=models.CharField(choices=[('RECON', 'Reconnaissance'), ('WEP', 'Weaponization'), ('DEL', 'Delivery'), ('EXP', 'Exploitation'), ('INS', 'Installation'), ('C2', 'Command & Control'), ('AOO', 'Actions on Objectives')], max_length=20, verbose_name='Attack Phase'), + model_name="mission", + name="attack_side_effects_include_flag", + field=models.BooleanField( + default=True, + verbose_name="Mission Option: Include Attack Side Effects in report?", + ), ), migrations.AlterField( - model_name='testdetail', - name='attack_phase_include_flag', - field=models.BooleanField(default=True, verbose_name='Include Attack Phase in report?'), + model_name="mission", + name="attack_time_date_include_flag", + field=models.BooleanField( + default=True, + verbose_name="Mission Option: Include Attack Times in report?", + ), ), migrations.AlterField( - model_name='testdetail', - name='attack_side_effects', - field=models.TextField(blank=True, help_text='List any observed side effects, if any. Example: Firewall X froze and subsequently crashed.', verbose_name='Attack Side Effects'), + model_name="mission", + name="attack_type_include_flag", + field=models.BooleanField( + default=True, + verbose_name="Mission Option: Include Attack Types in report?", + ), ), migrations.AlterField( - model_name='testdetail', - name='attack_side_effects_include_flag', - field=models.BooleanField(default=True, verbose_name='Include Attack Side Effects in report?'), + model_name="mission", + name="business_area", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="missions.businessarea", + verbose_name="Business Area", + ), ), migrations.AlterField( - model_name='testdetail', - name='attack_time_date', - field=models.DateTimeField(blank=True, default=django.utils.timezone.now, help_text='Date/time attack was launched', verbose_name='Attack Date / Time'), + model_name="mission", + name="command_syntax_include_flag", + field=models.BooleanField( + default=True, + verbose_name="Mission Option: Include Command Syntaxes in report?", + ), ), migrations.AlterField( - model_name='testdetail', - name='attack_time_date_include_flag', - field=models.BooleanField(default=True, verbose_name='Include Attack Time in report?'), + model_name="mission", + name="conclusion", + field=models.TextField( + blank=True, + default=missions.models.conclusion_default, + verbose_name="Conclusion", + ), ), migrations.AlterField( - model_name='testdetail', - name='attack_type', - field=models.CharField(blank=True, help_text='Example: SYN flood, UDP flood, malformed packets, web, fuzz, etc.', max_length=255, verbose_name='Attack Type'), + model_name="mission", + name="customer_notes_include_flag", + field=models.BooleanField( + default=True, + verbose_name="Mission Option: Include customer notes section in report?", + ), ), migrations.AlterField( - model_name='testdetail', - name='attack_type_include_flag', - field=models.BooleanField(default=True, verbose_name='Include Attack Type in report?'), + model_name="mission", + name="executive_summary", + field=models.TextField( + blank=True, + default=missions.models.executive_summary_default, + verbose_name="Executive Summary", + ), ), migrations.AlterField( - model_name='testdetail', - name='command_syntax', - field=models.TextField(blank=True, help_text='Include sample command/syntax used if possible. If a script was used/created, what commands will run it?', verbose_name='Command/Syntax'), + model_name="mission", + name="findings_include_flag", + field=models.BooleanField( + default=True, verbose_name="Mission Option: Include Findings in report?" + ), ), migrations.AlterField( - model_name='testdetail', - name='command_syntax_include_flag', - field=models.BooleanField(default=True, verbose_name='Include Command Syntax in report?'), + model_name="mission", + name="introduction", + field=models.TextField( + blank=True, + default=missions.models.introduction_default, + verbose_name="Introduction", + ), ), migrations.AlterField( - model_name='testdetail', - name='enclave', - field=models.CharField(blank=True, max_length=100, verbose_name='Enclave Test Executed From'), + model_name="mission", + name="mission_name", + field=models.CharField(max_length=255, verbose_name="Mission Name"), ), migrations.AlterField( - model_name='testdetail', - name='execution_status', - field=models.CharField(choices=[('N', 'Not Run'), ('R', 'Run'), ('C', 'Cancelled'), ('NA', 'N/A')], default='N', help_text='Execution status of the test case.', max_length=2, verbose_name='Execution Status'), + model_name="mission", + name="mission_number", + field=models.CharField(max_length=5, verbose_name="Mission Number"), ), migrations.AlterField( - model_name='testdetail', - name='findings', - field=models.TextField(blank=True, help_text='Document the specific finding related to this test or against the main test objectives if applicable', verbose_name='Findings'), + model_name="mission", + name="mitigation_include_flag", + field=models.BooleanField( + default=True, + verbose_name="Mission Option: Include Mitigations in report?", + ), ), migrations.AlterField( - model_name='testdetail', - name='findings_include_flag', - field=models.BooleanField(default=True, verbose_name='Include Findings in report?'), + model_name="mission", + name="objectives", + field=models.TextField( + blank=True, + default=missions.models.objectives_default, + verbose_name="Objectives", + ), ), migrations.AlterField( - model_name='testdetail', - name='mission', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='missions.mission', verbose_name='Mission'), + model_name="mission", + name="scope", + field=models.TextField( + blank=True, default=missions.models.scope_default, verbose_name="Scope" + ), ), migrations.AlterField( - model_name='testdetail', - name='mitigation', - field=models.TextField(blank=True, help_text='Example: Configure xyz, implement xyz, etc.', verbose_name='Mitigation'), + model_name="mission", + name="sources_include_flag", + field=models.BooleanField( + default=True, + verbose_name="Mission Option: Include Attack Sources in report?", + ), ), migrations.AlterField( - model_name='testdetail', - name='mitigation_include_flag', - field=models.BooleanField(default=True, verbose_name='Include Mitigations in report?'), + model_name="mission", + name="supporting_data_include_flag", + field=models.BooleanField( + default=True, + verbose_name="Mission Option: Include Supporting Data Information in report?", + ), ), migrations.AlterField( - model_name='testdetail', - name='point_of_contact', - field=models.CharField(blank=True, default='', help_text='Individual working or most familiar with this test case.', max_length=20, verbose_name='POC'), + model_name="mission", + name="targets_include_flag", + field=models.BooleanField( + default=True, + verbose_name="Mission Option: Include Attack Targets in report?", + ), ), migrations.AlterField( - model_name='testdetail', - name='re_eval_test_case_number', - field=models.CharField(blank=True, default='', help_text='Adds previous test case reference to description in report.', max_length=25, verbose_name='Re-Evaluate Test Case #'), + model_name="mission", + name="technical_assessment_overview", + field=models.TextField( + blank=True, + default=missions.models.technical_assessment_overview_default, + verbose_name="Technical Assessment / Attack Architecture Overview", + ), ), migrations.AlterField( - model_name='testdetail', - name='sources_include_flag', - field=models.BooleanField(default=True, verbose_name='Include Attack Sources in report?'), + model_name="mission", + name="test_case_identifier", + field=models.CharField( + blank=True, + default="", + max_length=20, + verbose_name="Test Case Identifier", + ), ), migrations.AlterField( - model_name='testdetail', - name='supporting_data_sort_order', - field=models.TextField(blank=True, default='[]'), + model_name="mission", + name="test_description_include_flag", + field=models.BooleanField( + default=True, + verbose_name="Mission Option: Include Test Descriptions in report?", + ), ), migrations.AlterField( - model_name='testdetail', - name='targets_include_flag', - field=models.BooleanField(default=True, verbose_name='Include Attack Targets in report?'), + model_name="mission", + name="test_result_observation_include_flag", + field=models.BooleanField( + default=True, + verbose_name="Mission Option: Include Test Details in report?", + ), ), migrations.AlterField( - model_name='testdetail', - name='test_case_include_flag', - field=models.BooleanField(default=True, verbose_name='Include Test Case in report?'), + model_name="mission", + name="testdetail_sort_order", + field=models.TextField(blank=True, default="[]"), ), migrations.AlterField( - model_name='testdetail', - name='test_case_status', - field=models.CharField(choices=[('NEW', 'Not started'), ('IN_WORK', 'In work'), ('REVIEW', 'Ready for review'), ('FINAL', 'Approved / Final')], default='NEW', max_length=100, verbose_name='Test Case Status'), + model_name="mission", + name="tools_used_include_flag", + field=models.BooleanField( + default=True, + verbose_name="Mission Option: Include Tools Used in report?", + ), ), migrations.AlterField( - model_name='testdetail', - name='test_description', - field=models.TextField(blank=True, help_text='Describe test case to be performed, what is the objective of this test case (Is it to deny, disrupt, penetrate, modify etc.)', verbose_name='Description'), + model_name="supportingdata", + name="caption", + field=models.TextField(blank=True, verbose_name="Caption"), + ), + migrations.AlterField( + model_name="supportingdata", + name="include_flag", + field=models.BooleanField( + default=True, verbose_name="Include attachment in report" + ), ), - migrations.AlterField( - model_name='testdetail', - name='test_description_include_flag', - field=models.BooleanField(default=True, verbose_name='Include Test Description in report?'), + migrations.AlterField( + model_name="supportingdata", + name="test_detail", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="missions.testdetail", + verbose_name="Test Details", + ), ), - migrations.AlterField( - model_name='testdetail', - name='test_number', - field=models.IntegerField(default=0, verbose_name='Test Number'), + migrations.AlterField( + model_name="supportingdata", + name="test_file", + field=models.FileField(upload_to="", verbose_name="Supporting Data"), ), migrations.AlterField( - model_name='testdetail', - name='test_objective', - field=models.CharField(help_text='Brief objective (ex.Port Scan against xyz)', max_length=255, verbose_name='Test Objective / Title'), + model_name="testdetail", + name="assumptions", + field=models.TextField( + blank=True, + help_text="Example: Attacker has a presence in xyz segment, etc.", + verbose_name="Assumptions", + ), ), migrations.AlterField( - model_name='testdetail', - name='test_result_observation', - field=models.TextField(blank=True, 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.', verbose_name='Test Result Details'), + model_name="testdetail", + name="assumptions_include_flag", + field=models.BooleanField( + default=True, verbose_name="Include Assumptions in report?" + ), ), migrations.AlterField( - model_name='testdetail', - name='test_result_observation_include_flag', - field=models.BooleanField(default=True, verbose_name='Include Test Result in report?'), - ), + model_name="testdetail", + name="attack_phase", + field=models.CharField( + choices=[ + ("RECON", "Reconnaissance"), + ("WEP", "Weaponization"), + ("DEL", "Delivery"), + ("EXP", "Exploitation"), + ("INS", "Installation"), + ("C2", "Command & Control"), + ("AOO", "Actions on Objectives"), + ], + max_length=20, + verbose_name="Attack Phase", + ), + ), + migrations.AlterField( + model_name="testdetail", + name="attack_phase_include_flag", + field=models.BooleanField( + default=True, verbose_name="Include Attack Phase in report?" + ), + ), + migrations.AlterField( + model_name="testdetail", + name="attack_side_effects", + field=models.TextField( + blank=True, + help_text="List any observed side effects, if any. Example: Firewall X froze and subsequently crashed.", + verbose_name="Attack Side Effects", + ), + ), + migrations.AlterField( + model_name="testdetail", + name="attack_side_effects_include_flag", + field=models.BooleanField( + default=True, verbose_name="Include Attack Side Effects in report?" + ), + ), + migrations.AlterField( + model_name="testdetail", + name="attack_time_date", + field=models.DateTimeField( + blank=True, + default=django.utils.timezone.now, + help_text="Date/time attack was launched", + verbose_name="Attack Date / Time", + ), + ), + migrations.AlterField( + model_name="testdetail", + name="attack_time_date_include_flag", + field=models.BooleanField( + default=True, verbose_name="Include Attack Time in report?" + ), + ), + migrations.AlterField( + model_name="testdetail", + name="attack_type", + field=models.CharField( + blank=True, + help_text="Example: SYN flood, UDP flood, malformed packets, web, fuzz, etc.", + max_length=255, + verbose_name="Attack Type", + ), + ), + migrations.AlterField( + model_name="testdetail", + name="attack_type_include_flag", + field=models.BooleanField( + default=True, verbose_name="Include Attack Type in report?" + ), + ), + migrations.AlterField( + model_name="testdetail", + name="command_syntax", + field=models.TextField( + blank=True, + help_text="Include sample command/syntax used if possible. If a script was used/created, what commands will run it?", + verbose_name="Command/Syntax", + ), + ), + migrations.AlterField( + model_name="testdetail", + name="command_syntax_include_flag", + field=models.BooleanField( + default=True, verbose_name="Include Command Syntax in report?" + ), + ), + migrations.AlterField( + model_name="testdetail", + name="enclave", + field=models.CharField( + blank=True, max_length=100, verbose_name="Enclave Test Executed From" + ), + ), + migrations.AlterField( + model_name="testdetail", + name="execution_status", + field=models.CharField( + choices=[ + ("N", "Not Run"), + ("R", "Run"), + ("C", "Cancelled"), + ("NA", "N/A"), + ], + default="N", + help_text="Execution status of the test case.", + max_length=2, + verbose_name="Execution Status", + ), + ), + migrations.AlterField( + model_name="testdetail", + name="findings", + field=models.TextField( + blank=True, + help_text="Document the specific finding related to this test or against the main test objectives if applicable", + verbose_name="Findings", + ), + ), + migrations.AlterField( + model_name="testdetail", + name="findings_include_flag", + field=models.BooleanField( + default=True, verbose_name="Include Findings in report?" + ), + ), + migrations.AlterField( + model_name="testdetail", + name="mission", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="missions.mission", + verbose_name="Mission", + ), + ), + migrations.AlterField( + model_name="testdetail", + name="mitigation", + field=models.TextField( + blank=True, + help_text="Example: Configure xyz, implement xyz, etc.", + verbose_name="Mitigation", + ), + ), + migrations.AlterField( + model_name="testdetail", + name="mitigation_include_flag", + field=models.BooleanField( + default=True, verbose_name="Include Mitigations in report?" + ), + ), + migrations.AlterField( + model_name="testdetail", + name="point_of_contact", + field=models.CharField( + blank=True, + default="", + help_text="Individual working or most familiar with this test case.", + max_length=20, + verbose_name="POC", + ), + ), + migrations.AlterField( + model_name="testdetail", + name="re_eval_test_case_number", + field=models.CharField( + blank=True, + default="", + help_text="Adds previous test case reference to description in report.", + max_length=25, + verbose_name="Re-Evaluate Test Case #", + ), + ), + migrations.AlterField( + model_name="testdetail", + name="sources_include_flag", + field=models.BooleanField( + default=True, verbose_name="Include Attack Sources in report?" + ), + ), + migrations.AlterField( + model_name="testdetail", + name="supporting_data_sort_order", + field=models.TextField(blank=True, default="[]"), + ), + migrations.AlterField( + model_name="testdetail", + name="targets_include_flag", + field=models.BooleanField( + default=True, verbose_name="Include Attack Targets in report?" + ), + ), + migrations.AlterField( + model_name="testdetail", + name="test_case_include_flag", + field=models.BooleanField( + default=True, verbose_name="Include Test Case in report?" + ), + ), + migrations.AlterField( + model_name="testdetail", + name="test_case_status", + field=models.CharField( + choices=[ + ("NEW", "Not started"), + ("IN_WORK", "In work"), + ("REVIEW", "Ready for review"), + ("FINAL", "Approved / Final"), + ], + default="NEW", + max_length=100, + verbose_name="Test Case Status", + ), + ), + migrations.AlterField( + model_name="testdetail", + name="test_description", + field=models.TextField( + blank=True, + help_text="Describe test case to be performed, what is the objective of this test case (Is it to deny, disrupt, penetrate, modify etc.)", + verbose_name="Description", + ), + ), + migrations.AlterField( + model_name="testdetail", + name="test_description_include_flag", + field=models.BooleanField( + default=True, verbose_name="Include Test Description in report?" + ), + ), + migrations.AlterField( + model_name="testdetail", + name="test_number", + field=models.IntegerField(default=0, verbose_name="Test Number"), + ), + migrations.AlterField( + model_name="testdetail", + name="test_objective", + field=models.CharField( + help_text="Brief objective (ex.Port Scan against xyz)", + max_length=255, + verbose_name="Test Objective / Title", + ), + ), + migrations.AlterField( + model_name="testdetail", + name="test_result_observation", + field=models.TextField( + blank=True, + 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.", + verbose_name="Test Result Details", + ), + ), + migrations.AlterField( + model_name="testdetail", + name="test_result_observation_include_flag", + field=models.BooleanField( + default=True, verbose_name="Include Test Result in report?" + ), + ), migrations.AlterField( - model_name='testdetail', - name='tools_used', - field=models.TextField(blank=True, help_text='Example: Burp Suite, Wireshark, NMap, etc.', verbose_name='Tools Used'), + model_name="testdetail", + name="tools_used", + field=models.TextField( + blank=True, + help_text="Example: Burp Suite, Wireshark, NMap, etc.", + verbose_name="Tools Used", + ), ), migrations.AlterField( - model_name='testdetail', - name='tools_used_include_flag', - field=models.BooleanField(default=True, verbose_name='Include Tools Used in report?'), + model_name="testdetail", + name="tools_used_include_flag", + field=models.BooleanField( + default=True, verbose_name="Include Tools Used in report?" + ), ), ] diff --git a/missions/models.py b/missions/models.py index 615ad88..cbf32d5 100644 --- a/missions/models.py +++ b/missions/models.py @@ -34,27 +34,27 @@ def introduction_default(): - return Mission.get_default_text('introduction.txt') + return Mission.get_default_text("introduction.txt") def executive_summary_default(): - return Mission.get_default_text('executive_summary.txt') + return Mission.get_default_text("executive_summary.txt") def scope_default(): - return Mission.get_default_text('scope.txt') + return Mission.get_default_text("scope.txt") def objectives_default(): - return Mission.get_default_text('objectives.txt') + return Mission.get_default_text("objectives.txt") def technical_assessment_overview_default(): - return Mission.get_default_text('technical_assessment.txt') + return Mission.get_default_text("technical_assessment.txt") def conclusion_default(): - return Mission.get_default_text('conclusion.txt') + return Mission.get_default_text("conclusion.txt") """ @@ -63,11 +63,10 @@ def conclusion_default(): 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: + 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 @@ -91,7 +90,7 @@ def get_default_text(file_name): ) business_area = models.ForeignKey( - 'BusinessArea', + "BusinessArea", verbose_name="Business Area", on_delete=models.CASCADE, ) @@ -103,33 +102,23 @@ def get_default_text(file_name): ) executive_summary = models.TextField( - blank=True, - verbose_name="Executive Summary", - default=executive_summary_default + blank=True, verbose_name="Executive Summary", default=executive_summary_default ) - scope = models.TextField( - blank=True, - verbose_name="Scope", - default=scope_default - ) + scope = models.TextField(blank=True, verbose_name="Scope", default=scope_default) objectives = models.TextField( - blank=True, - verbose_name="Objectives", - default=objectives_default + 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 + default=technical_assessment_overview_default, ) conclusion = models.TextField( - blank=True, - verbose_name="Conclusion", - default=conclusion_default + blank=True, verbose_name="Conclusion", default=conclusion_default ) # Mission-wide reporting include flags @@ -200,18 +189,15 @@ def get_default_text(file_name): supporting_data_include_flag = models.BooleanField( default=True, - verbose_name="Mission Option: Include Supporting Data Information in report?" + 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?" + verbose_name="Mission Option: Include customer notes section in report?", ) - testdetail_sort_order = models.TextField( - blank=True, - default="[]" - ) + testdetail_sort_order = models.TextField(blank=True, default="[]") def __str__(self): return "%s (%s)" % (self.mission_name, self.mission_number) @@ -240,14 +226,18 @@ class Host(models.Model): @staticmethod def get_host_output_format_string(): - format_string = str(DARTDynamicSettings.objects.get_as_object().host_output_format) + 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') + 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') + 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): @@ -255,37 +245,36 @@ def get_absolute_url(self): class TestDetail(models.Model): - mission = models.ForeignKey(Mission, verbose_name="Mission", on_delete=models.CASCADE,) + mission = models.ForeignKey( + Mission, + verbose_name="Mission", + on_delete=models.CASCADE, + ) test_number = models.IntegerField( - blank=False, - verbose_name="Test Number", - default=0 + blank=False, verbose_name="Test Number", default=0 ) test_case_include_flag = models.BooleanField( - default=True, - verbose_name="Include Test Case in report?" + 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'), + ("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', + default="NEW", ) enclave = models.CharField( - blank=True, - max_length=100, - verbose_name='Enclave Test Executed From' + blank=True, max_length=100, verbose_name="Enclave Test Executed From" ) test_objective = models.CharField( @@ -297,13 +286,13 @@ class TestDetail(models.Model): # 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'), + ("RECON", "Reconnaissance"), + ("WEP", "Weaponization"), + ("DEL", "Delivery"), + ("EXP", "Exploitation"), + ("INS", "Installation"), + ("C2", "Command & Control"), + ("AOO", "Actions on Objectives"), ) attack_phase = models.CharField( @@ -332,7 +321,7 @@ class TestDetail(models.Model): assumptions = models.TextField( blank=True, verbose_name="Assumptions", - help_text="Example: Attacker has a presence in xyz segment, etc." + help_text="Example: Attacker has a presence in xyz segment, etc.", ) assumptions_include_flag = models.BooleanField( @@ -343,7 +332,7 @@ class TestDetail(models.Model): 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.)" + 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( @@ -352,8 +341,8 @@ class TestDetail(models.Model): ) source_hosts = models.ManyToManyField( - 'Host', - related_name='source_set', + "Host", + related_name="source_set", ) sources_include_flag = models.BooleanField( @@ -362,8 +351,8 @@ class TestDetail(models.Model): ) target_hosts = models.ManyToManyField( - 'Host', - related_name='target_set', + "Host", + related_name="target_set", ) targets_include_flag = models.BooleanField( @@ -375,7 +364,7 @@ class TestDetail(models.Model): blank=True, default=timezone.now, verbose_name="Attack Date / Time", - help_text="Date/time attack was launched" + help_text="Date/time attack was launched", ) attack_time_date_include_flag = models.BooleanField( @@ -386,7 +375,7 @@ class TestDetail(models.Model): tools_used = models.TextField( blank=True, verbose_name="Tools Used", - help_text="Example: Burp Suite, Wireshark, NMap, etc." + help_text="Example: Burp Suite, Wireshark, NMap, etc.", ) tools_used_include_flag = models.BooleanField( @@ -397,7 +386,7 @@ class TestDetail(models.Model): 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?" + 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( @@ -409,8 +398,8 @@ class TestDetail(models.Model): 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." + "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( @@ -421,7 +410,7 @@ class TestDetail(models.Model): 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." + help_text="List any observed side effects, if any. Example: Firewall X froze and subsequently crashed.", ) attack_side_effects_include_flag = models.BooleanField( @@ -430,18 +419,18 @@ class TestDetail(models.Model): ) EXECUTION_STATUS_OPTIONS = ( - ('N', 'Not Run'), - ('R', 'Run'), - ('C', 'Cancelled'), - ('NA', 'N/A'), + ("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." + default="N", + help_text="Execution status of the test case.", ) has_findings = models.BooleanField( @@ -451,7 +440,7 @@ class TestDetail(models.Model): 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" + help_text="Document the specific finding related to this test or against the main test objectives if applicable", ) findings_include_flag = models.BooleanField( @@ -462,7 +451,7 @@ class TestDetail(models.Model): mitigation = models.TextField( blank=True, verbose_name="Mitigation", - help_text="Example: Configure xyz, implement xyz, etc." + help_text="Example: Configure xyz, implement xyz, etc.", ) mitigation_include_flag = models.BooleanField( @@ -475,7 +464,7 @@ class TestDetail(models.Model): blank=True, default="", verbose_name="POC", - help_text="Individual working or most familiar with this test case." + help_text="Individual working or most familiar with this test case.", ) re_eval_test_case_number = models.CharField( @@ -483,13 +472,10 @@ class TestDetail(models.Model): blank=True, default="", verbose_name="Re-Evaluate Test Case #", - help_text="Adds previous test case reference to description in report." + help_text="Adds previous test case reference to description in report.", ) - supporting_data_sort_order = models.TextField( - blank=True, - default="[]" - ) + supporting_data_sort_order = models.TextField(blank=True, default="[]") def count_of_supporting_data(self): return len(SupportingData.objects.filter(test_detail=self.pk)) @@ -503,7 +489,11 @@ def save(self, *args, **kwargs): class SupportingData(models.Model): - test_detail = models.ForeignKey(TestDetail, verbose_name="Test Details",on_delete=models.CASCADE,) + test_detail = models.ForeignKey( + TestDetail, + verbose_name="Test Details", + on_delete=models.CASCADE, + ) caption = models.TextField( blank=True, @@ -511,19 +501,16 @@ class SupportingData(models.Model): ) include_flag = models.BooleanField( - default=True, - verbose_name="Include attachment in report" + default=True, verbose_name="Include attachment in report" ) - test_file = models.FileField( - verbose_name="Supporting Data" - ) + 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}) + return reverse_lazy("data-view", {"supportingdata": self.pk}) class BusinessArea(models.Model): @@ -547,7 +534,7 @@ class Color(models.Model): ) def __str__(self): - return '{0.display_text}'.format(self) + return "{0.display_text}".format(self) class ClassificationLegend(models.Model): @@ -564,37 +551,37 @@ class ClassificationLegend(models.Model): ) text_color = models.ForeignKey( - 'Color', - related_name='classificationlegend_text_set', + "Color", + related_name="classificationlegend_text_set", on_delete=models.CASCADE, ) background_color = models.ForeignKey( - 'Color', - related_name='classificationlegend_background_set', + "Color", + related_name="classificationlegend_background_set", on_delete=models.CASCADE, ) REPORT_LABEL_COLOR_OPTIONS = ( - ('T', 'Text Color'), - ('B', 'Back Color'), + ("T", "Text Color"), + ("B", "Back Color"), ) report_label_color_selection = models.CharField( choices=REPORT_LABEL_COLOR_OPTIONS, max_length=1, - default='B', + default="B", blank=False, ) def get_report_label_color(self): - if self.report_label_color_selection == 'T': + 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) + return "{0.verbose_legend} ({0.short_legend})".format(self) class DARTDynamicSettingsManager(models.Manager): @@ -614,15 +601,15 @@ def get_as_object(self): class DARTDynamicSettings(models.Model): system_classification = models.ForeignKey( - 'ClassificationLegend', + "ClassificationLegend", on_delete=models.CASCADE, ) host_output_format = models.CharField( max_length=50, - default='{ip} ({name})', + default="{ip} ({name})", help_text='Use "{ip}" and "{name}" to specify how you want hosts to be displayed.', - validators=[validate_host_format_string] + validators=[validate_host_format_string], ) # Since we're treating Dynamic Settings as a singleton, diff --git a/missions/templates/about.html b/missions/templates/about.html index cb4c5fe..07cd4f3 100644 --- a/missions/templates/about.html +++ b/missions/templates/about.html @@ -18,29 +18,20 @@ --> {% endcomment %} {% load bootstrap3 %} - -{% block title %} - About DART -{% endblock %} - -{% block main_heading %} -

    - About DART -

    -{% endblock %} - +{% block title %}About DART{% endblock %} +{% block main_heading %}

    About DART

    {% endblock %} {% block content %} -

    What is it?

    -

    DART is a tool created by the Lockheed Martin Red Team to document and report results from penetration tests, particularly in isolated network environments.

    - -

    Who created it?

    -

    - Everyone on the Lockheed Martin Red Team has contributed to this tool - through development, ideas, and "hey wouldn't it be cool if..." feature requests. - It has been tailored to support the Red Team testing best practices created at Lockheed Martin. -

    - -

    Where can I get it?

    -

    - You can download DART on GitHub -

    +

    What is it?

    +

    + DART is a tool created by the Lockheed Martin Red Team to document and report results from penetration tests, particularly in isolated network environments. +

    +

    Who created it?

    +

    + Everyone on the Lockheed Martin Red Team has contributed to this tool - through development, ideas, and "hey wouldn't it be cool if..." feature requests. + It has been tailored to support the Red Team testing best practices created at Lockheed Martin. +

    +

    Where can I get it?

    +

    + You can download DART on GitHub +

    {% endblock %} diff --git a/missions/templates/business_area_list.html b/missions/templates/business_area_list.html index a64e286..e2094e0 100644 --- a/missions/templates/business_area_list.html +++ b/missions/templates/business_area_list.html @@ -19,39 +19,44 @@ {% endcomment %} {% load bootstrap3 %} {% block title %}Business Area Update{% endblock %} - {% block extra_nav_bar_content_right %} -
  • +
  • System Settings -
  • + {% endblock %} - {% block main_heading %} -

    Business Area Update

    +

    + Business Area Update + +

    {% endblock %} {% block content %} - -
     
    - - - - - - - {% for ba in object_list %} +
     
    +
    NameActions
    - - + + - {% endfor %} -
    {{ ba.name }} - {% bootstrap_icon "pencil" %}  - {% bootstrap_icon "trash" %}
    -
    NameActions
    - - - + {% endblock %} diff --git a/missions/templates/classification_list.html b/missions/templates/classification_list.html index cbdfa9a..b85694a 100644 --- a/missions/templates/classification_list.html +++ b/missions/templates/classification_list.html @@ -19,87 +19,106 @@ {% endcomment %} {% load bootstrap3 %} {% block title %}Classification Legend Update{% endblock %} - {% block extra_nav_bar_content_right %} -
  • +
  • System Settings -
  • + {% endblock %} - {% block main_heading %} -

    Classification Legend Update

    +

    + Classification Legend Update +
    + +
    +

    {% endblock %} {% block content %} - -
     
    - - - - - - - - - - - {% for class in object_list %} - - - - - + + + + + + + {% endfor %} +
    Verbose LegendShort LegendText ColorBackground ColorReport Label ColorActions
    - {{ class.verbose_legend }} - - - {{ class.short_legend }} - - - {{ class.text_color }} - {# Autocomplete attribute required for FF compatibility 8-( #} - - - {{ class.background_color }} - + + + + + + + + + {% for class in object_list %} + + + + + + - - - - {% endfor %} -
    Verbose LegendShort LegendText ColorBackground ColorReport Label ColorActions
    + {{ class.verbose_legend }} + + + {{ class.short_legend }} + + + {{ class.text_color }} + {# Autocomplete attribute required for FF compatibility 8-( #} + + + {{ class.background_color }} + + + {{ class.get_report_label_color_selection_display }} + - - {{ class.get_report_label_color_selection_display }} - - - {% bootstrap_icon "pencil" %}  - {% bootstrap_icon "trash" %}
    -
    - - + $(function() { + $("#classification-legend-list").on("keyup", ".form-control", function(event) { + /* On enter, update the classification */ + if (event.keyCode == 13) { + updateClassification($(this).closest("tr")); + } + }); + }); + {% endblock %} diff --git a/missions/templates/color_list.html b/missions/templates/color_list.html index 58dcf9c..56f51eb 100644 --- a/missions/templates/color_list.html +++ b/missions/templates/color_list.html @@ -19,47 +19,57 @@ {% endcomment %} {% load bootstrap3 %} {% block title %}Color Options Update{% endblock %} - {% block extra_nav_bar_content_right %} -
  • +
  • System Settings -
  • + {% endblock %} - {% block main_heading %} -

    Color Options Update

    +

    + Color Options Update +
    + +
    +

    {% endblock %} {% block content %} - -
     
    - - - - - - - - {% for color in object_list %} - - - - +
     
    +
    Display Text6-Character Hex Code (RGB)Actions
    - {{ color.display_text }} - - - {{ color.hex_color_code }} - - - {% bootstrap_icon "pencil" %}  - {% bootstrap_icon "trash" %}
    -
    + + + + - {% endfor %} -
    Display Text6-Character Hex Code (RGB)Actions
    - - + $(function() { + $("#color-options-list").on("keyup", ".form-control", function(event) { + /* On enter, update the color */ + if (event.keyCode == 13) { + updateColor($(this).closest("tr")); + } + }); + }); + {% endblock %} diff --git a/missions/templates/create_account.html b/missions/templates/create_account.html index a85b244..38a6f2e 100644 --- a/missions/templates/create_account.html +++ b/missions/templates/create_account.html @@ -19,54 +19,60 @@ {% endcomment %} {% load bootstrap3 %} {% load static %} - -{% block title %} - Account Creation -{% endblock %} - +{% block title %}Account Creation{% endblock %} {% block main_heading %} -

    - {% if any_accounts_configured %} - Account Help - {% else %} - Account Creation - {% endif %} -

    +

    + {% if any_accounts_configured %} + Account Help + {% else %} + Account Creation + {% endif %} +

    {% endblock %} - {% block content %} -{% if any_accounts_configured %} -
    -
    Forgotten Credentials Assistance
    -
    -

    If you have forgotten your credentials you will be unable to access the web page until you have - removed all user accounts (which will allow you to create a new user using this page). - Don't fret, your mission data will be preserved.

    -

    Removing All User Accounts

    -
      -
    1. Execute the following command from the DART directory:
      - python manage.py removeallusers -
    2. -
    + {% if any_accounts_configured %} +
    +
    Forgotten Credentials Assistance
    +
    +

    + If you have forgotten your credentials you will be unable to access the web page until you have + removed all user accounts (which will allow you to create a new user using this page). + Don't fret, your mission data will be preserved. +

    +

    Removing All User Accounts

    +
      +
    1. + Execute the following command from the DART directory: +
      + python manage.py removeallusers +
    2. +
    +
    - - -
    -{% else %} + {% else %}
    Create an Account

    It looks like you're the first user here, go ahead and enter some credentials to get started!

    {% csrf_token %} - - +
    -{% endif %} + {% endif %} {% endblock %} diff --git a/missions/templates/delete_mission.html b/missions/templates/delete_mission.html index 833dae2..45e439e 100644 --- a/missions/templates/delete_mission.html +++ b/missions/templates/delete_mission.html @@ -18,26 +18,15 @@ --> {% endcomment %} {% load bootstrap3 %} -{% block title %} - Confirm Mission Deletion -{% endblock %} - -{% block main_heading %} -

    - Confirm Mission Deletion -

    -{% endblock %} - +{% block title %}Confirm Mission Deletion{% endblock %} +{% block main_heading %}

    Confirm Mission Deletion

    {% endblock %} {% block content %} -

    Are you sure you want to permanently delete this mission and all the hard work you put into it?

    -
    -{% csrf_token %} - -{% buttons %} - I've made a horrible mistake! Take me back! - -{% endbuttons %} +

    Are you sure you want to permanently delete this mission and all the hard work you put into it?

    + + {% csrf_token %} + {% buttons %} + I've made a horrible mistake! Take me back! + + {% endbuttons %}
    {% endblock %} diff --git a/missions/templates/delete_test.html b/missions/templates/delete_test.html index 81fea10..69b9cd0 100644 --- a/missions/templates/delete_test.html +++ b/missions/templates/delete_test.html @@ -18,27 +18,18 @@ --> {% endcomment %} {% load bootstrap3 %} -{% block title %} - Confirm Test Deletion -{% endblock %} - -{% block main_heading %} -

    - Confirm Test Deletion -

    -{% endblock %} - +{% block title %}Confirm Test Deletion{% endblock %} +{% block main_heading %}

    Confirm Test Deletion

    {% endblock %} {% block content %} -

    Are you sure you want to permanently delete this test case from this mission and all the hard work you put into it? -

    -
    -{% csrf_token %} - -{% buttons %} - I've made a horrible mistake! Take me back! - -{% endbuttons %} +

    + Are you sure you want to permanently delete this test case from this mission and all the hard work you put into it? +

    + + {% csrf_token %} + {% buttons %} + I've made a horrible mistake! Take me back! + + {% endbuttons %}
    {% endblock %} diff --git a/missions/templates/delete_test_data.html b/missions/templates/delete_test_data.html index e584905..7786197 100644 --- a/missions/templates/delete_test_data.html +++ b/missions/templates/delete_test_data.html @@ -18,36 +18,23 @@ --> {% endcomment %} {% load bootstrap3 %} -{% block title %} - Confirm Supporting Data Deletion -{% endblock %} - -{% block main_heading %} -

    - Confirm Supporting Data Deletion -

    -{% endblock %} - +{% block title %}Confirm Supporting Data Deletion{% endblock %} +{% block main_heading %}

    Confirm Supporting Data Deletion

    {% endblock %} {% block content %} -

    Are you sure you want to permanently delete this supporting data and all the hard work you put into it? -

    - - - -
    -{% csrf_token %} - -{% buttons %} - I've made a horrible mistake! Take me back! - -{% endbuttons %} +

    Are you sure you want to permanently delete this supporting data and all the hard work you put into it?

    + + + {% csrf_token %} + {% buttons %} + I've made a horrible mistake! Take me back! + + {% endbuttons %}
    {% endblock %} diff --git a/missions/templates/edit_mission.html b/missions/templates/edit_mission.html index 9836754..2ceb3a1 100644 --- a/missions/templates/edit_mission.html +++ b/missions/templates/edit_mission.html @@ -26,26 +26,23 @@ {% endif %} {% endblock %} {% block main_heading %} -

    - {% if mission.id %} - Edit Mission - {% else %} - Add Mission - {% endif %} -

    +

    + {% if mission.id %} + Edit Mission + {% else %} + Add Mission + {% endif %} +

    {% endblock %} - {% block content %} -
    -{% csrf_token %} -{% bootstrap_form form %} -{% buttons %} - - {% if mission.id %} - {% bootstrap_icon "trash" %} Delete - {% endif %} -{% endbuttons %} + + {% csrf_token %} + {% bootstrap_form form %} + {% buttons %} + + {% if mission.id %} + {% bootstrap_icon "trash" %} Delete + {% endif %} + {% endbuttons %}
    {% endblock %} diff --git a/missions/templates/edit_mission_hosts.html b/missions/templates/edit_mission_hosts.html index 53097c7..6846a14 100644 --- a/missions/templates/edit_mission_hosts.html +++ b/missions/templates/edit_mission_hosts.html @@ -18,76 +18,68 @@ --> {% endcomment %} {% load bootstrap3 %} - -{% block title %} -Edit Mission Hosts -{% endblock %} - -{% block main_heading %} -

    -Edit Mission Hosts -

    -{% endblock %} - +{% block title %}Edit Mission Hosts{% endblock %} +{% block main_heading %}

    Edit Mission Hosts

    {% endblock %} {% block content %} -
     
    - -{% include 'modals/edit_missionhosts_modal.html' with mission_id=mission.pk%} - -{% csrf_token %} -
    -
    - - - - - - - - {% for host in mission.host_set.all %} - - - - - - - {% endfor %} -
    NameIP AddressNo-HitActions
    {{ host.host_name }}{{ host.ip_address }} - {% if host.is_no_hit %} - Yes - {% else %} - No - {% endif %} - - - {% bootstrap_icon "pencil" %} -     - {% bootstrap_icon "trash" %} -
    +
     
    + {% include 'modals/edit_missionhosts_modal.html' with mission_id=mission.pk %} + {% csrf_token %} +
    +
    + + + + + + + + {% for host in mission.host_set.all %} + + + + + + + {% endfor %} +
    NameIP AddressNo-HitActions
    {{ host.host_name }}{{ host.ip_address }} + {% if host.is_no_hit %} + Yes + {% else %} + No + {% endif %} + + {% bootstrap_icon "pencil" %} +     + {% bootstrap_icon "trash" %} +
    +
    -
    -
    -
    -
    +
    +
    +
    + +
    +
    -
    - - + // addRow + // hostAjaxErrorHandler + {% endblock %} diff --git a/missions/templates/edit_mission_test.html b/missions/templates/edit_mission_test.html index e3f21e9..15a3033 100644 --- a/missions/templates/edit_mission_test.html +++ b/missions/templates/edit_mission_test.html @@ -19,7 +19,6 @@ {% endcomment %} {% load bootstrap3 %} {% load quickparts %} - {% block title %} {% if this_mission and testdetail.pk > 0 %} {% if is_read_only %} @@ -31,33 +30,29 @@ Add Test {% endif %} {% endblock %} - {% if testdetail.pk > 0 %} {% block extra_nav_bar_content_right %} {% if is_read_only %}
  • -   - + +  
  • {% elif this_mission and testdetail.pk > 0 %}
  • -   - + +  
  • {% endif %} - {% if this_mission and testdetail.pk > 0 %}
  • {% manage_data_button this_mission.id testdetail.pk testdetail.count_of_supporting_data as_button=True %} @@ -65,104 +60,120 @@ {% endif %} {% endblock %} {% endif %} - -{% block main_heading %}

    - {% if this_mission and testdetail.pk > 0 %} - {% if is_read_only %} - Review Test +{% block main_heading %} +

    + {% if this_mission and testdetail.pk > 0 %} + {% if is_read_only %} + Review Test + {% else %} + Edit Test + {% endif %} + {{ testdetail.pk }} ({{ testdetail.test_objective }}) {{ object.pk }} {% else %} - Edit Test + Add Test for {{ this_mission }} {% endif %} - {{testdetail.pk}} ({{testdetail.test_objective}}) {{object.pk}} - {% else %} - Add Test for {{this_mission}} - {% endif %}

    -

    << Back to Tests

    + +

    + << Back to Tests +

    {% endblock %} - {% block content %} -{% if this_mission and testdetail.pk > 0 %} - {% include 'modals/edit_testhosts_modal.html' with mission_id=this_mission.pk test_id=testdetail.pk %} -{% endif %} - - - - - {% endblock %} diff --git a/missions/templates/edit_test_data.html b/missions/templates/edit_test_data.html index 8ab71c6..3b9989d 100644 --- a/missions/templates/edit_test_data.html +++ b/missions/templates/edit_test_data.html @@ -27,28 +27,27 @@ {% endblock %} {% block main_heading %}

    - {% if supportingdata.pk %} - Edit - {% else %} - Add - {% endif %} - Data for Test {{this_test.test_objective}}

    + {% if supportingdata.pk %} + Edit + {% else %} + Add + {% endif %} + Data for Test {{ this_test.test_objective }} +

    - << Back to Data List + << Back to Data List

    {% endblock %} - {% block content %} -
    -{% csrf_token %} -{% bootstrap_form form %} -{% buttons %} - - {% if supportingdata.pk %} - {% bootstrap_icon "trash" %} Delete - {% endif %} -{% endbuttons %} + + {% csrf_token %} + {% bootstrap_form form %} + {% buttons %} + + {% if supportingdata.pk %} + {% bootstrap_icon "trash" %} Delete + {% endif %} + {% endbuttons %}
    {% endblock %} diff --git a/missions/templates/edit_testhosts_partial.html b/missions/templates/edit_testhosts_partial.html index 23d6033..cdb9bc8 100644 --- a/missions/templates/edit_testhosts_partial.html +++ b/missions/templates/edit_testhosts_partial.html @@ -17,17 +17,16 @@ --> {% endcomment %} {# Requires testdetail to be set to a test case record #} -
    -
    +
    - {% for host in testdetail.source_hosts.all %} -
    {{ host }}
    - {% endfor %} + {% for host in testdetail.source_hosts.all %}
    {{ host }}
    {% endfor %}
    @@ -35,9 +34,7 @@
    - {% for host in testdetail.target_hosts.all %} -
    {{ host }}
    - {% endfor %} + {% for host in testdetail.target_hosts.all %}
    {{ host }}
    {% endfor %}
    diff --git a/missions/templates/login.html b/missions/templates/login.html index 0a9d32e..cc9c6bb 100644 --- a/missions/templates/login.html +++ b/missions/templates/login.html @@ -22,24 +22,15 @@ {% endif %} {% endblock %} - -{% block title %} -Login -{% endblock %} - -{% block main_heading %} -

    - Welcome to DART -

    -{% endblock %} - +{% block title %}Login{% endblock %} +{% block main_heading %}

    Welcome to DART

    {% endblock %} {% block content %} -

    Login above to continue.

    -

    If you are having trouble logging in or need to create your initial system account, please - click here. -

    - - +

    Login above to continue.

    +

    + If you are having trouble logging in or need to create your initial system account, please + click here. +

    + {% endblock %} diff --git a/missions/templates/login_interstitial.html b/missions/templates/login_interstitial.html index 33e2b2f..38f01bf 100644 --- a/missions/templates/login_interstitial.html +++ b/missions/templates/login_interstitial.html @@ -19,43 +19,30 @@ {% endcomment %} {% load bootstrap3 %} {% load static %} - -{% block title %} - Application Use Agreement -{% endblock %} - -{% block main_heading %} -

    - System Access Notice -

    -{% endblock %} - +{% block title %}Application Use Agreement{% endblock %} +{% block main_heading %}

    System Access Notice

    {% endblock %} {% block content %} -
    -
    System Access Notice
    -
    - {% comment %} +
    +
    System Access Notice
    +
    + {% comment %} {# Fun fact: If you remove the surrounding comment tags, this is a great place to put your company logo. #} -

    - {% endcomment %} -

    You must accept the following items to use this application.

    -
      -
    1. Use of this application is governed by the organization granting access to the system.
    2. -
    -

    If you do not agree to any of the above items you may not use this application.

    -
    - +
    - -
    - {% endblock %} diff --git a/missions/templates/mission_list.html b/missions/templates/mission_list.html index 850d77f..55b8dcc 100644 --- a/missions/templates/mission_list.html +++ b/missions/templates/mission_list.html @@ -19,42 +19,48 @@ {% endcomment %} {% load bootstrap3 %} {% block title %}Missions{% endblock %} - {% block extra_nav_bar_content_right %} -
  • +
  • System Settings -
  • + {% endblock %} - {% block main_heading %} -

    Mission List

    +

    + Mission List + +

    {% endblock %} {% block content %} - -
     
    - - - - - - - - - - {% for m in missions %} +
     
    +
    Mission NameMission #Business AreaActions
    - - - - - + + + + + - {% endfor %} -
    {% bootstrap_icon "edit" %} Edit{{ m.mission_name }}{{ m.mission_number }}{{ m.business_area }} - {% bootstrap_icon "stats" %} Test Cases
    - {% bootstrap_icon "hdd" %} Mission Hosts
    - {% bootstrap_icon "file" %} Generate Report
    - {% bootstrap_icon "gift" %} Generate Data Package -
    Mission NameMission #Business AreaActions
    + {% for m in missions %} +
    + {% bootstrap_icon "edit" %} Edit + {{ m.mission_name }}{{ m.mission_number }}{{ m.business_area }} + {% bootstrap_icon "stats" %} Test Cases +
    + {% bootstrap_icon "hdd" %} Mission Hosts +
    + {% bootstrap_icon "file" %} Generate Report +
    + {% bootstrap_icon "gift" %} Generate Data Package +
    {% bootstrap_pagination missions size="medium" %} {% endblock %} diff --git a/missions/templates/mission_list_tests.html b/missions/templates/mission_list_tests.html index 32e5f9e..a9f4319 100644 --- a/missions/templates/mission_list_tests.html +++ b/missions/templates/mission_list_tests.html @@ -20,173 +20,157 @@ {% load bootstrap3 %} {% load dart_bootstrap_formatting_helpers %} {% load quickparts %} - {% block title %}Mission Test Cases{% endblock %} - -{% block additional_head_content %} - - -{% endblock %} - +{% block additional_head_content %}{% endblock %} {% block extra_nav_bar_content_right %} -
  • -   - -
  • + {% endblock %} - {% block main_heading %} -

    Mission Test Cases for {{this_mission.mission_name}} - -

    +

    + Mission Test Cases for {{ this_mission.mission_name }} + +

    {% endblock %} - -{% block content%} - - -