diff --git a/dojo/benchmark/__init__.py b/dojo/benchmark/__init__.py
index e69de29bb2d..08cfc4447d9 100644
--- a/dojo/benchmark/__init__.py
+++ b/dojo/benchmark/__init__.py
@@ -0,0 +1 @@
+import dojo.benchmark.admin # noqa: F401
diff --git a/dojo/benchmark/admin.py b/dojo/benchmark/admin.py
new file mode 100644
index 00000000000..288569dc768
--- /dev/null
+++ b/dojo/benchmark/admin.py
@@ -0,0 +1,15 @@
+from django.contrib import admin
+
+from dojo.benchmark.models import (
+ Benchmark_Category,
+ Benchmark_Product,
+ Benchmark_Product_Summary,
+ Benchmark_Requirement,
+ Benchmark_Type,
+)
+
+admin.site.register(Benchmark_Type)
+admin.site.register(Benchmark_Requirement)
+admin.site.register(Benchmark_Category)
+admin.site.register(Benchmark_Product)
+admin.site.register(Benchmark_Product_Summary)
diff --git a/dojo/benchmark/models.py b/dojo/benchmark/models.py
new file mode 100644
index 00000000000..184e9dc9b2d
--- /dev/null
+++ b/dojo/benchmark/models.py
@@ -0,0 +1,100 @@
+from django.db import models
+from django.utils.translation import gettext as _
+
+
+class Benchmark_Type(models.Model):
+ name = models.CharField(max_length=300)
+ version = models.CharField(max_length=15)
+ source = (("PCI", "PCI"),
+ ("OWASP ASVS", "OWASP ASVS"),
+ ("OWASP Mobile ASVS", "OWASP Mobile ASVS"))
+ benchmark_source = models.CharField(max_length=20, blank=False,
+ null=True, choices=source,
+ default="OWASP ASVS")
+ created = models.DateTimeField(auto_now_add=True, null=False)
+ updated = models.DateTimeField(auto_now=True)
+ enabled = models.BooleanField(default=True)
+
+ def __str__(self):
+ return self.name + " " + self.version
+
+
+class Benchmark_Category(models.Model):
+ type = models.ForeignKey("dojo.Benchmark_Type", verbose_name=_("Benchmark Type"), on_delete=models.CASCADE)
+ name = models.CharField(max_length=300)
+ objective = models.TextField()
+ references = models.TextField(blank=True, null=True)
+ enabled = models.BooleanField(default=True)
+ created = models.DateTimeField(auto_now_add=True, null=False)
+ updated = models.DateTimeField(auto_now=True)
+
+ class Meta:
+ ordering = ("name",)
+
+ def __str__(self):
+ return self.name + ": " + self.type.name
+
+
+class Benchmark_Requirement(models.Model):
+ category = models.ForeignKey("dojo.Benchmark_Category", on_delete=models.CASCADE)
+ objective_number = models.CharField(max_length=15, null=True, blank=True)
+ objective = models.TextField()
+ references = models.TextField(blank=True, null=True)
+ level_1 = models.BooleanField(default=False)
+ level_2 = models.BooleanField(default=False)
+ level_3 = models.BooleanField(default=False)
+ enabled = models.BooleanField(default=True)
+ cwe_mapping = models.ManyToManyField("dojo.CWE", blank=True)
+ testing_guide = models.ManyToManyField("dojo.Testing_Guide", blank=True)
+ created = models.DateTimeField(auto_now_add=True, null=False)
+ updated = models.DateTimeField(auto_now=True)
+
+ def __str__(self):
+ return str(self.objective_number) + ": " + self.category.name
+
+
+class Benchmark_Product(models.Model):
+ product = models.ForeignKey("dojo.Product", on_delete=models.CASCADE)
+ control = models.ForeignKey("dojo.Benchmark_Requirement", on_delete=models.CASCADE)
+ pass_fail = models.BooleanField(default=False, verbose_name=_("Pass"),
+ help_text=_("Does the product meet the requirement?"))
+ enabled = models.BooleanField(default=True,
+ help_text=_("Applicable for this specific product."))
+ notes = models.ManyToManyField("dojo.Notes", blank=True, editable=False)
+ created = models.DateTimeField(auto_now_add=True, null=False)
+ updated = models.DateTimeField(auto_now=True)
+
+ class Meta:
+ unique_together = [("product", "control")]
+
+ def __str__(self):
+ return self.product.name + ": " + self.control.objective_number + ": " + self.control.category.name
+
+
+class Benchmark_Product_Summary(models.Model):
+ product = models.ForeignKey("dojo.Product", on_delete=models.CASCADE)
+ benchmark_type = models.ForeignKey("dojo.Benchmark_Type", on_delete=models.CASCADE)
+ asvs_level = (("Level 1", "Level 1"),
+ ("Level 2", "Level 2"),
+ ("Level 3", "Level 3"))
+ desired_level = models.CharField(max_length=15,
+ null=False, choices=asvs_level,
+ default="Level 1")
+ current_level = models.CharField(max_length=15, blank=True,
+ null=True, choices=asvs_level,
+ default="None")
+ asvs_level_1_benchmark = models.IntegerField(null=False, default=0, help_text=_("Total number of active benchmarks for this application."))
+ asvs_level_1_score = models.IntegerField(null=False, default=0, help_text=_("ASVS Level 1 Score"))
+ asvs_level_2_benchmark = models.IntegerField(null=False, default=0, help_text=_("Total number of active benchmarks for this application."))
+ asvs_level_2_score = models.IntegerField(null=False, default=0, help_text=_("ASVS Level 2 Score"))
+ asvs_level_3_benchmark = models.IntegerField(null=False, default=0, help_text=_("Total number of active benchmarks for this application."))
+ asvs_level_3_score = models.IntegerField(null=False, default=0, help_text=_("ASVS Level 3 Score"))
+ publish = models.BooleanField(default=False, help_text=_("Publish score to Product."))
+ created = models.DateTimeField(auto_now_add=True, null=False)
+ updated = models.DateTimeField(auto_now=True)
+
+ class Meta:
+ unique_together = [("product", "benchmark_type")]
+
+ def __str__(self):
+ return self.product.name + ": " + self.benchmark_type.name
diff --git a/dojo/benchmark/signals.py b/dojo/benchmark/signals.py
index 6f87fa320cd..f6d997698a7 100644
--- a/dojo/benchmark/signals.py
+++ b/dojo/benchmark/signals.py
@@ -3,7 +3,7 @@
from django.db.models.signals import pre_delete
from django.dispatch import receiver
-from dojo.models import Benchmark_Product
+from dojo.benchmark.models import Benchmark_Product
from dojo.notes.helper import delete_related_notes
logger = logging.getLogger(__name__)
diff --git a/dojo/benchmark/ui/__init__.py b/dojo/benchmark/ui/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/dojo/benchmark/ui/forms.py b/dojo/benchmark/ui/forms.py
new file mode 100644
index 00000000000..c4416af53f9
--- /dev/null
+++ b/dojo/benchmark/ui/forms.py
@@ -0,0 +1,37 @@
+from django import forms
+
+from dojo.benchmark.models import (
+ Benchmark_Product,
+ Benchmark_Product_Summary,
+ Benchmark_Requirement,
+)
+
+
+class Benchmark_Product_SummaryForm(forms.ModelForm):
+
+ class Meta:
+ model = Benchmark_Product_Summary
+ exclude = ["product", "current_level", "benchmark_type", "asvs_level_1_benchmark", "asvs_level_1_score", "asvs_level_2_benchmark", "asvs_level_2_score", "asvs_level_3_benchmark", "asvs_level_3_score"]
+
+
+class DeleteBenchmarkForm(forms.ModelForm):
+ id = forms.IntegerField(required=True,
+ widget=forms.widgets.HiddenInput())
+
+ class Meta:
+ model = Benchmark_Product_Summary
+ fields = ["id"]
+
+
+class BenchmarkForm(forms.ModelForm):
+
+ class Meta:
+ model = Benchmark_Product
+ exclude = ["product", "control"]
+
+
+class Benchmark_RequirementForm(forms.ModelForm):
+
+ class Meta:
+ model = Benchmark_Requirement
+ exclude = [""]
diff --git a/dojo/benchmark/urls.py b/dojo/benchmark/ui/urls.py
similarity index 96%
rename from dojo/benchmark/urls.py
rename to dojo/benchmark/ui/urls.py
index 849e83c603c..3581ce165ab 100644
--- a/dojo/benchmark/urls.py
+++ b/dojo/benchmark/ui/urls.py
@@ -1,6 +1,6 @@
from django.urls import re_path
-from . import views
+from dojo.benchmark.ui import views
urlpatterns = [
re_path(
diff --git a/dojo/benchmark/views.py b/dojo/benchmark/ui/views.py
similarity index 98%
rename from dojo/benchmark/views.py
rename to dojo/benchmark/ui/views.py
index b1dc065692e..40bfde471d7 100644
--- a/dojo/benchmark/views.py
+++ b/dojo/benchmark/ui/views.py
@@ -8,15 +8,15 @@
from django.urls import reverse
from django.utils.translation import gettext as _
-from dojo.forms import Benchmark_Product_SummaryForm, DeleteBenchmarkForm
-from dojo.models import (
+from dojo.benchmark.models import (
Benchmark_Category,
Benchmark_Product,
Benchmark_Product_Summary,
Benchmark_Requirement,
Benchmark_Type,
- Product,
)
+from dojo.benchmark.ui.forms import Benchmark_Product_SummaryForm, DeleteBenchmarkForm
+from dojo.models import Product
from dojo.templatetags.display_tags import asvs_level
from dojo.utils import (
Product_Tab,
diff --git a/dojo/filters.py b/dojo/filters.py
index 8162da4e0c5..b6ff6815f6f 100644
--- a/dojo/filters.py
+++ b/dojo/filters.py
@@ -1,19 +1,16 @@
import collections
import decimal
import logging
-import warnings
from datetime import datetime, timedelta
import six
import tagulous
from django.apps import apps
from django.conf import settings
-from django.contrib.contenttypes.models import ContentType
from django.db.models import Count, Q
from django.utils.timezone import now, tzinfo
from django.utils.translation import gettext_lazy as _
from django_filters import (
- BooleanFilter,
CharFilter,
DateFilter,
FilterSet,
@@ -25,7 +22,6 @@
)
from django_filters import rest_framework as filters
from django_filters.filters import ChoiceFilter
-from polymorphic.base import ManagerInheritanceWarning
# from tagulous.forms import TagWidget
# import tagulous
@@ -46,21 +42,17 @@
from dojo.models import (
SEVERITY_CHOICES,
App_Analysis,
- ChoiceQuestion,
Development_Environment,
DojoMeta,
Endpoint,
Endpoint_Status,
Engagement,
- Engagement_Survey,
Finding,
Note_Type,
Product,
Product_Type,
- Question,
Risk_Acceptance,
Test,
- TextQuestion,
Vulnerability_Id,
)
from dojo.product_type.queries import get_authorized_product_types
@@ -1413,64 +1405,8 @@ class Meta:
exclude = []
include = ("name", "is_single", "description")
-# ==============================
-# Defect Dojo Engaegment Surveys
-# ==============================
-
-
-class QuestionnaireFilter(FilterSet):
- name = CharFilter(lookup_expr="icontains")
- description = CharFilter(lookup_expr="icontains")
- active = BooleanFilter()
-
- class Meta:
- model = Engagement_Survey
- exclude = ["questions"]
-
- survey_set = FilterSet
-
-
-class QuestionTypeFilter(ChoiceFilter):
- def any(self, qs, name):
- return qs.all()
-
- def text_question(self, qs, name):
- return qs.filter(polymorphic_ctype=ContentType.objects.get_for_model(TextQuestion))
-
- def choice_question(self, qs, name):
- return qs.filter(polymorphic_ctype=ContentType.objects.get_for_model(ChoiceQuestion))
-
- options = {
- None: (_("Any"), any),
- 1: (_("Text Question"), text_question),
- 2: (_("Choice Question"), choice_question),
- }
-
- def __init__(self, *args, **kwargs):
- kwargs["choices"] = [
- (key, value[0]) for key, value in six.iteritems(self.options)]
- super().__init__(*args, **kwargs)
-
- def filter(self, qs, value):
- try:
- value = int(value)
- except (ValueError, TypeError):
- value = None
- return self.options[value][1](self, qs, self.options[value][0])
-
-
# ApiUserFilter lives in dojo/user/api/filters.py — import from there directly.
-
-with warnings.catch_warnings(action="ignore", category=ManagerInheritanceWarning):
- class QuestionFilter(FilterSet):
- text = CharFilter(lookup_expr="icontains")
- type = QuestionTypeFilter()
-
- class Meta:
- model = Question
- exclude = ["polymorphic_ctype", "created", "modified", "order"]
-
- question_set = FilterSet
+# QuestionnaireFilter, QuestionTypeFilter, QuestionFilter live in dojo/survey/ui/filters.py
from dojo.auditlog.filters import LogEntryFilter, PgHistoryFilter # noqa: E402, F401 -- backward compat
diff --git a/dojo/forms.py b/dojo/forms.py
index e07dfb9517c..999c9bf16d8 100644
--- a/dojo/forms.py
+++ b/dojo/forms.py
@@ -1,13 +1,8 @@
-import json
import logging
import re
-import warnings
from datetime import date, datetime
from pathlib import Path
-from crispy_forms.bootstrap import InlineCheckboxes, InlineRadios
-from crispy_forms.helper import FormHelper
-from crispy_forms.layout import Layout
from crum import get_current_user
from dateutil.relativedelta import relativedelta
from django import forms
@@ -15,14 +10,12 @@
from django.contrib.auth.models import Permission
from django.contrib.auth.password_validation import validate_password
from django.core.exceptions import ValidationError
-from django.db.models import Count
from django.forms import modelformset_factory
from django.forms.widgets import Select, Widget
from django.utils import timezone
from django.utils.dates import MONTHS
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
-from polymorphic.base import ManagerInheritanceWarning
from tagulous.forms import TagField
from dojo.endpoint.utils import validate_endpoints_to_add
@@ -54,41 +47,28 @@
from dojo.models import (
SEVERITY_CHOICES,
Announcement,
- Answered_Survey,
App_Analysis,
- Benchmark_Product,
- Benchmark_Product_Summary,
- Benchmark_Requirement,
Check_List,
- Choice,
- ChoiceAnswer,
- ChoiceQuestion,
Development_Environment,
Dojo_User,
DojoMeta,
Endpoint,
- Engagement_Survey,
FileUpload,
Finding,
Finding_Group,
- General_Survey,
Note_Type,
Notes,
Objects_Product,
Product_API_Scan_Configuration,
Product_Type,
- Question,
Regulation,
Risk_Acceptance,
SLA_Configuration,
Test_Type,
- TextAnswer,
- TextQuestion,
User,
)
from dojo.product_type.queries import get_authorized_product_types
from dojo.tools.factory import get_choices_sorted, requires_file, requires_tool_type
-from dojo.user.queries import get_authorized_users
from dojo.user.utils import get_configuration_permissions_fields
from dojo.utils import (
get_password_requirements_string,
@@ -110,14 +90,6 @@
("out_of_scope", "Out of Scope"))
-class MultipleSelectWithPop(forms.SelectMultiple):
- def render(self, name, *args, **kwargs):
- html = super().render(name, *args, **kwargs)
- popup_plus = '
'
-
- return mark_safe(popup_plus)
-
-
class MonthYearWidget(Widget):
"""
@@ -953,20 +925,12 @@ class CustomReportOptionsForm(forms.Form):
report_type = forms.ChoiceField(choices=(("HTML", "HTML"),))
-class Benchmark_Product_SummaryForm(forms.ModelForm):
-
- class Meta:
- model = Benchmark_Product_Summary
- exclude = ["product", "current_level", "benchmark_type", "asvs_level_1_benchmark", "asvs_level_1_score", "asvs_level_2_benchmark", "asvs_level_2_score", "asvs_level_3_benchmark", "asvs_level_3_score"]
-
-
-class DeleteBenchmarkForm(forms.ModelForm):
- id = forms.IntegerField(required=True,
- widget=forms.widgets.HiddenInput())
-
- class Meta:
- model = Benchmark_Product_Summary
- fields = ["id"]
+from dojo.benchmark.ui.forms import ( # noqa: E402, F401 -- backward compat
+ Benchmark_Product_SummaryForm,
+ Benchmark_RequirementForm,
+ BenchmarkForm,
+ DeleteBenchmarkForm,
+)
class RegulationForm(forms.ModelForm):
@@ -1066,20 +1030,6 @@ def clean(self):
return self.cleaned_data
-class BenchmarkForm(forms.ModelForm):
-
- class Meta:
- model = Benchmark_Product
- exclude = ["product", "control"]
-
-
-class Benchmark_RequirementForm(forms.ModelForm):
-
- class Meta:
- model = Benchmark_Requirement
- exclude = [""]
-
-
from dojo.notifications.ui.forms import ( # noqa: E402, F401 -- backward compat
DeleteNotificationsWebhookForm,
NotificationsForm,
@@ -1124,391 +1074,6 @@ def __init__(self, *args, **kwargs):
self.fields["style"].disabled = True
-# ==============================
-# Defect Dojo Engaegment Surveys
-# ==============================
-
-# List of validator_name:func_name
-# Show in admin a multichoice list of validator names
-# pass this to form using field_name='validator_name' ?
-class QuestionForm(forms.Form):
-
- """Base class for a Question"""
-
- def __init__(self, *args, **kwargs):
- self.helper = FormHelper()
- self.helper.form_method = "post"
-
- # If true crispy-forms will render a tags
- self.helper.form_tag = kwargs.pop("form_tag", True)
-
- self.engagement_survey = kwargs.get("engagement_survey")
-
- self.answered_survey = kwargs.get("answered_survey")
- if not self.answered_survey:
- del kwargs["engagement_survey"]
- else:
- del kwargs["answered_survey"]
-
- self.helper.form_class = kwargs.get("form_class", "")
-
- self.question = kwargs.pop("question", None)
-
- if not self.question:
- msg = "Need a question to render"
- raise ValueError(msg)
-
- super().__init__(*args, **kwargs)
-
-
-class TextQuestionForm(QuestionForm):
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
-
- # work out initial data
-
- initial_answer = TextAnswer.objects.filter(
- answered_survey=self.answered_survey,
- question=self.question,
- )
-
- initial_answer = initial_answer[0].answer if initial_answer.exists() else ""
-
- self.fields["answer"] = forms.CharField(
- label=self.question.text,
- widget=forms.Textarea(attrs={"rows": 3, "cols": 10}),
- required=not self.question.optional,
- initial=initial_answer,
- )
-
- def save(self):
- if not self.is_valid():
- msg = "form is not valid"
- raise forms.ValidationError(msg)
-
- answer = self.cleaned_data.get("answer")
-
- if not answer:
- if self.fields["answer"].required:
- msg = "Required"
- raise forms.ValidationError(msg)
- return
-
- text_answer, created = TextAnswer.objects.get_or_create(
- answered_survey=self.answered_survey,
- question=self.question,
- )
-
- if created:
- text_answer.answered_survey = self.answered_survey
- text_answer.answer = answer
- text_answer.save()
-
-
-class ChoiceQuestionForm(QuestionForm):
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
-
- choices = [(c.id, c.label) for c in self.question.choices.all()]
-
- # initial values
-
- initial_choices = []
- choice_answer = ChoiceAnswer.objects.filter(
- answered_survey=self.answered_survey,
- question=self.question,
- ).annotate(a=Count("answer")).filter(a__gt=0)
-
- # we have ChoiceAnswer instance
- if choice_answer:
- choice_answer = choice_answer[0]
- initial_choices = list(choice_answer.answer.all().values_list("id", flat=True))
- if self.question.multichoice is False:
- initial_choices = initial_choices[0]
-
- # default classes
- widget = forms.RadioSelect
- field_type = forms.ChoiceField
- inline_type = InlineRadios
-
- if self.question.multichoice:
- field_type = forms.MultipleChoiceField
- widget = forms.CheckboxSelectMultiple
- inline_type = InlineCheckboxes
-
- field = field_type(
- label=self.question.text,
- required=not self.question.optional,
- choices=choices,
- initial=initial_choices,
- widget=widget,
- )
-
- self.fields["answer"] = field
-
- # Render choice buttons inline
- self.helper.layout = Layout(
- inline_type("answer"),
- )
-
- def clean_answer(self):
- real_answer = self.cleaned_data.get("answer")
-
- # for single choice questions, the selected answer is a single string
- if not isinstance(real_answer, list):
- real_answer = [real_answer]
- return real_answer
-
- def save(self):
- if not self.is_valid():
- msg = "Form is not valid"
- raise forms.ValidationError(msg)
-
- real_answer = self.cleaned_data.get("answer")
-
- if not real_answer:
- if self.fields["answer"].required:
- msg = "Required"
- raise forms.ValidationError(msg)
- return
-
- choices = Choice.objects.filter(id__in=real_answer)
-
- # find ChoiceAnswer and filter in answer !
- choice_answer = ChoiceAnswer.objects.filter(
- answered_survey=self.answered_survey,
- question=self.question,
- )
-
- # we have ChoiceAnswer instance
- if choice_answer:
- choice_answer = choice_answer[0]
-
- if not choice_answer:
- # create a ChoiceAnswer
- choice_answer = ChoiceAnswer.objects.create(
- answered_survey=self.answered_survey,
- question=self.question,
- )
-
- # re save out the choices
- choice_answer.answered_survey = self.answered_survey
- choice_answer.answer.set(choices)
- choice_answer.save()
-
-
-class Add_Questionnaire_Form(forms.ModelForm):
- survey = forms.ModelChoiceField(
- queryset=Engagement_Survey.objects.all(),
- required=True,
- widget=forms.widgets.Select(),
- help_text="Select the Questionnaire to add.")
-
- class Meta:
- model = Answered_Survey
- exclude = ("responder",
- "completed",
- "engagement",
- "answered_on",
- "assignee")
-
-
-class AddGeneralQuestionnaireForm(forms.ModelForm):
- survey = forms.ModelChoiceField(
- queryset=Engagement_Survey.objects.all(),
- required=True,
- widget=forms.widgets.Select(),
- help_text="Select the Questionnaire to add.")
- expiration = forms.DateField(widget=forms.TextInput(
- attrs={"class": "datepicker", "autocomplete": "off"}))
-
- class Meta:
- model = General_Survey
- exclude = ("num_responses", "generated")
-
- # date can only be today or in the past, not the future
- def clean_expiration(self):
- expiration = self.cleaned_data.get("expiration", None)
- if expiration:
- today = datetime.today().date()
- if expiration < today:
- msg = "The expiration cannot be in the past"
- raise forms.ValidationError(msg)
- if expiration == today:
- msg = "The expiration cannot be today"
- raise forms.ValidationError(msg)
- return timezone.make_aware(
- datetime.combine(expiration, datetime.min.time()),
- )
- msg = "An expiration for the survey must be supplied"
- raise forms.ValidationError(msg)
-
-
-class Delete_Questionnaire_Form(forms.ModelForm):
- id = forms.IntegerField(required=True,
- widget=forms.widgets.HiddenInput())
-
- class Meta:
- model = Answered_Survey
- fields = ["id"]
-
-
-class DeleteGeneralQuestionnaireForm(forms.ModelForm):
- id = forms.IntegerField(required=True,
- widget=forms.widgets.HiddenInput())
-
- class Meta:
- model = General_Survey
- fields = ["id"]
-
-
-class Delete_Eng_Survey_Form(forms.ModelForm):
- id = forms.IntegerField(required=True,
- widget=forms.widgets.HiddenInput())
-
- class Meta:
- model = Engagement_Survey
- fields = ["id"]
-
-
-class CreateQuestionnaireForm(forms.ModelForm):
- class Meta:
- model = Engagement_Survey
- exclude = ["questions"]
-
-
-with warnings.catch_warnings(action="ignore", category=ManagerInheritanceWarning):
- class EditQuestionnaireQuestionsForm(forms.ModelForm):
- questions = forms.ModelMultipleChoiceField(
- Question.polymorphic.all(),
- required=True,
- help_text="Select questions to include on this questionnaire. Field can be used to search available questions.",
- widget=MultipleSelectWithPop(attrs={"size": "11"}))
-
- class Meta:
- model = Engagement_Survey
- exclude = ["name", "description", "active"]
-
-
-class CreateQuestionForm(forms.Form):
- type = forms.ChoiceField(
- choices=(("---", "-----"), ("text", "Text"), ("choice", "Choice")))
- order = forms.IntegerField(
- min_value=1,
- widget=forms.TextInput(attrs={"data-type": "both"}),
- help_text="The order the question will appear on the questionnaire")
- optional = forms.BooleanField(help_text="If selected, user doesn't have to answer this question",
- initial=False,
- required=False,
- widget=forms.CheckboxInput(attrs={"data-type": "both"}))
- text = forms.CharField(widget=forms.Textarea(attrs={"data-type": "text"}),
- label="Question Text",
- help_text="The actual question.")
-
-
-class CreateTextQuestionForm(forms.Form):
- class Meta:
- model = TextQuestion
- exclude = ["order", "optional"]
-
-
-class MultiWidgetBasic(forms.widgets.MultiWidget):
- def __init__(self, attrs=None):
- widgets = [forms.TextInput(attrs={"data-type": "choice"}),
- forms.TextInput(attrs={"data-type": "choice"}),
- forms.TextInput(attrs={"data-type": "choice"}),
- forms.TextInput(attrs={"data-type": "choice"}),
- forms.TextInput(attrs={"data-type": "choice"}),
- forms.TextInput(attrs={"data-type": "choice"})]
- super().__init__(widgets, attrs)
-
- def decompress(self, value):
- if value:
- return json.loads(value)
- return [None, None, None, None, None, None]
-
- def format_output(self, rendered_widgets):
- return "
".join(rendered_widgets)
-
-
-class MultiExampleField(forms.fields.MultiValueField):
- widget = MultiWidgetBasic
-
- def __init__(self, *args, **kwargs):
- list_fields = [forms.fields.CharField(required=True),
- forms.fields.CharField(required=True),
- forms.fields.CharField(required=False),
- forms.fields.CharField(required=False),
- forms.fields.CharField(required=False),
- forms.fields.CharField(required=False)]
- super().__init__(list_fields, *args, **kwargs)
-
- def compress(self, values):
- return json.dumps(values)
-
-
-class CreateChoiceQuestionForm(forms.Form):
- multichoice = forms.BooleanField(required=False,
- initial=False,
- widget=forms.CheckboxInput(attrs={"data-type": "choice"}),
- help_text="Can more than one choice can be selected?")
-
- answer_choices = MultiExampleField(required=False, widget=MultiWidgetBasic(attrs={"data-type": "choice"}))
-
- class Meta:
- model = ChoiceQuestion
- exclude = ["order", "optional", "choices"]
-
-
-class EditQuestionForm(forms.ModelForm):
- class Meta:
- model = Question
- exclude = []
-
-
-class EditTextQuestionForm(EditQuestionForm):
- class Meta:
- model = TextQuestion
- exclude = []
-
-
-class EditChoiceQuestionForm(EditQuestionForm):
- choices = forms.ModelMultipleChoiceField(
- Choice.objects.all(),
- required=True,
- help_text="Select choices to include on this question. Field can be used to search available choices.",
- widget=MultipleSelectWithPop(attrs={"size": "11"}))
-
- class Meta:
- model = ChoiceQuestion
- exclude = []
-
-
-class AddChoicesForm(forms.ModelForm):
- class Meta:
- model = Choice
- exclude = []
-
-
-class AssignUserForm(forms.ModelForm):
- assignee = forms.CharField(required=False,
- widget=forms.widgets.HiddenInput())
-
- def __init__(self, *args, **kwargs):
- assignee = None
- if "assignee" in kwargs:
- assignee = kwargs.pop("asignees")
- super().__init__(*args, **kwargs)
- if assignee is None:
- self.fields["assignee"] = forms.ModelChoiceField(queryset=get_authorized_users("view"), empty_label="Not Assigned", required=False)
- else:
- self.fields["assignee"].initial = assignee
-
- class Meta:
- model = Answered_Survey
- exclude = ["engagement", "survey", "responder", "completed", "answered_on"]
-
-
class ConfigurationPermissionsForm(forms.Form):
def __init__(self, *args, **kwargs):
diff --git a/dojo/models.py b/dojo/models.py
index 7e6052ffe0c..651bebad557 100644
--- a/dojo/models.py
+++ b/dojo/models.py
@@ -1,6 +1,5 @@
import copy
import logging
-import warnings
from datetime import timedelta
from pathlib import Path
from uuid import uuid4
@@ -20,10 +19,6 @@
from django.utils.deconstruct import deconstructible
from django.utils.timezone import now
from django.utils.translation import gettext as _
-from django_extensions.db.models import TimeStampedModel
-from polymorphic.base import ManagerInheritanceWarning
-from polymorphic.managers import PolymorphicManager
-from polymorphic.models import PolymorphicModel
from tagulous.models import TagField
from tagulous.models.managers import FakeTagRelatedManager # noqa: F401 -- backward compat re-export
@@ -552,7 +547,7 @@ class Meta:
from dojo.finding.models import ( # noqa: E402 -- re-export; class-body FKs below reference these
- CWE,
+ CWE, # noqa: F401 -- re-export
BurpRawRequestResponse, # noqa: F401 -- re-export
Finding,
Finding_Group, # noqa: F401 -- re-export
@@ -905,278 +900,26 @@ def __str__(self):
return self.testing_guide_category.name + ": " + self.name
-class Benchmark_Type(models.Model):
- name = models.CharField(max_length=300)
- version = models.CharField(max_length=15)
- source = (("PCI", "PCI"),
- ("OWASP ASVS", "OWASP ASVS"),
- ("OWASP Mobile ASVS", "OWASP Mobile ASVS"))
- benchmark_source = models.CharField(max_length=20, blank=False,
- null=True, choices=source,
- default="OWASP ASVS")
- created = models.DateTimeField(auto_now_add=True, null=False)
- updated = models.DateTimeField(auto_now=True)
- enabled = models.BooleanField(default=True)
-
- def __str__(self):
- return self.name + " " + self.version
-
-
-class Benchmark_Category(models.Model):
- type = models.ForeignKey(Benchmark_Type, verbose_name=_("Benchmark Type"), on_delete=models.CASCADE)
- name = models.CharField(max_length=300)
- objective = models.TextField()
- references = models.TextField(blank=True, null=True)
- enabled = models.BooleanField(default=True)
- created = models.DateTimeField(auto_now_add=True, null=False)
- updated = models.DateTimeField(auto_now=True)
-
- class Meta:
- ordering = ("name",)
-
- def __str__(self):
- return self.name + ": " + self.type.name
-
-
-class Benchmark_Requirement(models.Model):
- category = models.ForeignKey(Benchmark_Category, on_delete=models.CASCADE)
- objective_number = models.CharField(max_length=15, null=True, blank=True)
- objective = models.TextField()
- references = models.TextField(blank=True, null=True)
- level_1 = models.BooleanField(default=False)
- level_2 = models.BooleanField(default=False)
- level_3 = models.BooleanField(default=False)
- enabled = models.BooleanField(default=True)
- cwe_mapping = models.ManyToManyField(CWE, blank=True)
- testing_guide = models.ManyToManyField(Testing_Guide, blank=True)
- created = models.DateTimeField(auto_now_add=True, null=False)
- updated = models.DateTimeField(auto_now=True)
-
- def __str__(self):
- return str(self.objective_number) + ": " + self.category.name
-
-
-class Benchmark_Product(models.Model):
- product = models.ForeignKey(Product, on_delete=models.CASCADE)
- control = models.ForeignKey(Benchmark_Requirement, on_delete=models.CASCADE)
- pass_fail = models.BooleanField(default=False, verbose_name=_("Pass"),
- help_text=_("Does the product meet the requirement?"))
- enabled = models.BooleanField(default=True,
- help_text=_("Applicable for this specific product."))
- notes = models.ManyToManyField(Notes, blank=True, editable=False)
- created = models.DateTimeField(auto_now_add=True, null=False)
- updated = models.DateTimeField(auto_now=True)
-
- class Meta:
- unique_together = [("product", "control")]
-
- def __str__(self):
- return self.product.name + ": " + self.control.objective_number + ": " + self.control.category.name
-
-
-class Benchmark_Product_Summary(models.Model):
- product = models.ForeignKey(Product, on_delete=models.CASCADE)
- benchmark_type = models.ForeignKey(Benchmark_Type, on_delete=models.CASCADE)
- asvs_level = (("Level 1", "Level 1"),
- ("Level 2", "Level 2"),
- ("Level 3", "Level 3"))
- desired_level = models.CharField(max_length=15,
- null=False, choices=asvs_level,
- default="Level 1")
- current_level = models.CharField(max_length=15, blank=True,
- null=True, choices=asvs_level,
- default="None")
- asvs_level_1_benchmark = models.IntegerField(null=False, default=0, help_text=_("Total number of active benchmarks for this application."))
- asvs_level_1_score = models.IntegerField(null=False, default=0, help_text=_("ASVS Level 1 Score"))
- asvs_level_2_benchmark = models.IntegerField(null=False, default=0, help_text=_("Total number of active benchmarks for this application."))
- asvs_level_2_score = models.IntegerField(null=False, default=0, help_text=_("ASVS Level 2 Score"))
- asvs_level_3_benchmark = models.IntegerField(null=False, default=0, help_text=_("Total number of active benchmarks for this application."))
- asvs_level_3_score = models.IntegerField(null=False, default=0, help_text=_("ASVS Level 3 Score"))
- publish = models.BooleanField(default=False, help_text=_("Publish score to Product."))
- created = models.DateTimeField(auto_now_add=True, null=False)
- updated = models.DateTimeField(auto_now=True)
-
- class Meta:
- unique_together = [("product", "benchmark_type")]
-
- def __str__(self):
- return self.product.name + ": " + self.benchmark_type.name
-
-
-# ==========================
-# Defect Dojo Engaegment Surveys
-# ==============================
-with warnings.catch_warnings(action="ignore", category=ManagerInheritanceWarning):
- class Question(PolymorphicModel, TimeStampedModel):
-
- """Represents a question."""
-
- class Meta:
- ordering = ["order"]
-
- order = models.PositiveIntegerField(default=1,
- help_text=_("The render order"))
-
- optional = models.BooleanField(
- default=False,
- help_text=_("If selected, user doesn't have to answer this question"))
-
- text = models.TextField(blank=False, help_text=_("The question text"), default="")
- objects = models.Manager()
- polymorphic = PolymorphicManager()
-
- def __str__(self):
- return self.text
-
-
-class TextQuestion(Question):
-
- """Question with a text answer"""
-
- objects = PolymorphicManager()
-
- def get_form(self):
- """Returns the form for this model"""
- from .forms import TextQuestionForm # noqa: PLC0415
- return TextQuestionForm
-
-
-class Choice(TimeStampedModel):
-
- """Model to store the choices for multi choice questions"""
-
- order = models.PositiveIntegerField(default=1)
-
- label = models.TextField(default="")
-
- class Meta:
- ordering = ["order"]
-
- def __str__(self):
- return self.label
-
-
-class ChoiceQuestion(Question):
-
- """
- Question with answers that are chosen from a list of choices defined
- by the user.
- """
-
- multichoice = models.BooleanField(default=False,
- help_text=_("Select one or more"))
- choices = models.ManyToManyField(Choice)
- objects = PolymorphicManager()
-
- def get_form(self):
- """Returns the form for this model"""
- from .forms import ChoiceQuestionForm # noqa: PLC0415
- return ChoiceQuestionForm
-
-
-# meant to be a abstract survey, identified by name for purpose
-class Engagement_Survey(models.Model):
- name = models.CharField(max_length=200, null=False, blank=False,
- editable=True, default="")
- description = models.TextField(editable=True, default="")
- questions = models.ManyToManyField(Question)
- active = models.BooleanField(default=True)
-
- class Meta:
- verbose_name = _("Engagement Survey")
- verbose_name_plural = "Engagement Surveys"
- ordering = ("-active", "name")
-
- def __str__(self):
- return self.name
-
-
-# meant to be an answered survey tied to an engagement
-
-class Answered_Survey(models.Model):
- # tie this to a specific engagement
- engagement = models.ForeignKey(Engagement, related_name="engagement+",
- null=True, blank=False, editable=True,
- on_delete=models.CASCADE)
- # what surveys have been answered
- survey = models.ForeignKey(Engagement_Survey, on_delete=models.CASCADE)
- assignee = models.ForeignKey(Dojo_User, related_name="assignee",
- null=True, blank=True, editable=True,
- default=None, on_delete=models.RESTRICT)
- # who answered it
- responder = models.ForeignKey(Dojo_User, related_name="responder",
- null=True, blank=True, editable=True,
- default=None, on_delete=models.RESTRICT)
- completed = models.BooleanField(default=False)
- answered_on = models.DateField(null=True)
-
- class Meta:
- verbose_name = _("Answered Engagement Survey")
- verbose_name_plural = _("Answered Engagement Surveys")
-
- def __str__(self):
- return self.survey.name
-
-
-def default_expiration():
- return timezone.now() + timedelta(days=7)
-
-
-class General_Survey(models.Model):
- survey = models.ForeignKey(Engagement_Survey, on_delete=models.CASCADE)
- num_responses = models.IntegerField(default=0)
- generated = models.DateTimeField(auto_now_add=True, null=True)
- expiration = models.DateTimeField(default=default_expiration)
-
- class Meta:
- verbose_name = _("General Engagement Survey")
- verbose_name_plural = _("General Engagement Surveys")
-
- def __str__(self):
- return self.survey.name
-
- def clean(self):
- if self.expiration and timezone.is_naive(self.expiration):
- self.expiration = timezone.make_aware(self.expiration)
-
-
-with warnings.catch_warnings(action="ignore", category=ManagerInheritanceWarning):
- class Answer(PolymorphicModel, TimeStampedModel):
-
- """Base Answer model"""
-
- question = models.ForeignKey(Question, on_delete=models.CASCADE)
-
- answered_survey = models.ForeignKey(Answered_Survey,
- null=False,
- blank=False,
- on_delete=models.CASCADE)
- objects = models.Manager()
- polymorphic = PolymorphicManager()
-
-
-class TextAnswer(Answer):
- answer = models.TextField(
- blank=False,
- help_text=_("The answer text"),
- default="")
- objects = PolymorphicManager()
-
- def __str__(self):
- return self.answer
-
-
-class ChoiceAnswer(Answer):
- answer = models.ManyToManyField(
- Choice,
- help_text=_("The selected choices as the answer"))
- objects = PolymorphicManager()
-
- def __str__(self):
- if len(self.answer.all()):
- return str(self.answer.all()[0])
- return "No Response"
-
+from dojo.benchmark.models import ( # noqa: E402, I001 -- re-export; backward compat
+ Benchmark_Category, # noqa: F401
+ Benchmark_Product, # noqa: F401
+ Benchmark_Product_Summary, # noqa: F401
+ Benchmark_Requirement, # noqa: F401
+ Benchmark_Type, # noqa: F401
+)
+from dojo.survey.models import ( # noqa: E402 -- re-export; backward compat
+ Answer, # noqa: F401
+ Answered_Survey, # noqa: F401
+ Choice, # noqa: F401
+ ChoiceAnswer, # noqa: F401
+ ChoiceQuestion, # noqa: F401
+ Engagement_Survey, # noqa: F401
+ General_Survey, # noqa: F401
+ Question, # noqa: F401
+ TextAnswer, # noqa: F401
+ TextQuestion, # noqa: F401
+ default_expiration, # noqa: F401
+)
# Audit logging registration is now handled in auditlog.py and configured in apps.py
# This allows for conditional registration of either django-auditlog or django-pghistory
@@ -1198,13 +941,6 @@ def __str__(self):
tagulous.admin.register(App_Analysis.tags)
tagulous.admin.register(Objects_Product.tags)
-# Benchmarks
-admin.site.register(Benchmark_Type)
-admin.site.register(Benchmark_Requirement)
-admin.site.register(Benchmark_Category)
-admin.site.register(Benchmark_Product)
-admin.site.register(Benchmark_Product_Summary)
-
# Testing
admin.site.register(Testing_Guide_Category)
admin.site.register(Testing_Guide)
@@ -1255,4 +991,3 @@ def __str__(self):
admin.site.register(Announcement)
admin.site.register(UserAnnouncement)
admin.site.register(BannerConf)
-admin.site.register(General_Survey)
diff --git a/dojo/survey/__init__.py b/dojo/survey/__init__.py
index e69de29bb2d..dcf96374631 100644
--- a/dojo/survey/__init__.py
+++ b/dojo/survey/__init__.py
@@ -0,0 +1 @@
+import dojo.survey.admin # noqa: F401
diff --git a/dojo/survey/admin.py b/dojo/survey/admin.py
new file mode 100644
index 00000000000..15b76ad2c8a
--- /dev/null
+++ b/dojo/survey/admin.py
@@ -0,0 +1,5 @@
+from django.contrib import admin
+
+from dojo.survey.models import General_Survey
+
+admin.site.register(General_Survey)
diff --git a/dojo/survey/models.py b/dojo/survey/models.py
new file mode 100644
index 00000000000..6e8b98a9cf7
--- /dev/null
+++ b/dojo/survey/models.py
@@ -0,0 +1,181 @@
+import warnings
+from datetime import timedelta
+
+from django.db import models
+from django.utils import timezone
+from django.utils.translation import gettext as _
+from django_extensions.db.models import TimeStampedModel
+from polymorphic.base import ManagerInheritanceWarning
+from polymorphic.managers import PolymorphicManager
+from polymorphic.models import PolymorphicModel
+
+with warnings.catch_warnings(action="ignore", category=ManagerInheritanceWarning):
+ class Question(PolymorphicModel, TimeStampedModel):
+
+ """Represents a question."""
+
+ class Meta:
+ ordering = ["order"]
+
+ order = models.PositiveIntegerField(default=1,
+ help_text=_("The render order"))
+
+ optional = models.BooleanField(
+ default=False,
+ help_text=_("If selected, user doesn't have to answer this question"))
+
+ text = models.TextField(blank=False, help_text=_("The question text"), default="")
+ objects = models.Manager()
+ polymorphic = PolymorphicManager()
+
+ def __str__(self):
+ return self.text
+
+
+class TextQuestion(Question):
+
+ """Question with a text answer"""
+
+ objects = PolymorphicManager()
+
+ def get_form(self):
+ """Returns the form for this model"""
+ from dojo.survey.ui.forms import TextQuestionForm # noqa: PLC0415 -- lazy import, avoids circular dependency
+ return TextQuestionForm
+
+
+class Choice(TimeStampedModel):
+
+ """Model to store the choices for multi choice questions"""
+
+ order = models.PositiveIntegerField(default=1)
+
+ label = models.TextField(default="")
+
+ class Meta:
+ ordering = ["order"]
+
+ def __str__(self):
+ return self.label
+
+
+class ChoiceQuestion(Question):
+
+ """
+ Question with answers that are chosen from a list of choices defined
+ by the user.
+ """
+
+ multichoice = models.BooleanField(default=False,
+ help_text=_("Select one or more"))
+ choices = models.ManyToManyField("dojo.Choice")
+ objects = PolymorphicManager()
+
+ def get_form(self):
+ """Returns the form for this model"""
+ from dojo.survey.ui.forms import ChoiceQuestionForm # noqa: PLC0415 -- lazy import, avoids circular dependency
+ return ChoiceQuestionForm
+
+
+# meant to be a abstract survey, identified by name for purpose
+class Engagement_Survey(models.Model):
+ name = models.CharField(max_length=200, null=False, blank=False,
+ editable=True, default="")
+ description = models.TextField(editable=True, default="")
+ questions = models.ManyToManyField("dojo.Question")
+ active = models.BooleanField(default=True)
+
+ class Meta:
+ verbose_name = _("Engagement Survey")
+ verbose_name_plural = "Engagement Surveys"
+ ordering = ("-active", "name")
+
+ def __str__(self):
+ return self.name
+
+
+# meant to be an answered survey tied to an engagement
+
+class Answered_Survey(models.Model):
+ # tie this to a specific engagement
+ engagement = models.ForeignKey("dojo.Engagement", related_name="engagement+",
+ null=True, blank=False, editable=True,
+ on_delete=models.CASCADE)
+ # what surveys have been answered
+ survey = models.ForeignKey("dojo.Engagement_Survey", on_delete=models.CASCADE)
+ assignee = models.ForeignKey("dojo.Dojo_User", related_name="assignee",
+ null=True, blank=True, editable=True,
+ default=None, on_delete=models.RESTRICT)
+ # who answered it
+ responder = models.ForeignKey("dojo.Dojo_User", related_name="responder",
+ null=True, blank=True, editable=True,
+ default=None, on_delete=models.RESTRICT)
+ completed = models.BooleanField(default=False)
+ answered_on = models.DateField(null=True)
+
+ class Meta:
+ verbose_name = _("Answered Engagement Survey")
+ verbose_name_plural = _("Answered Engagement Surveys")
+
+ def __str__(self):
+ return self.survey.name
+
+
+def default_expiration():
+ return timezone.now() + timedelta(days=7)
+
+
+class General_Survey(models.Model):
+ survey = models.ForeignKey("dojo.Engagement_Survey", on_delete=models.CASCADE)
+ num_responses = models.IntegerField(default=0)
+ generated = models.DateTimeField(auto_now_add=True, null=True)
+ expiration = models.DateTimeField(default=default_expiration)
+
+ class Meta:
+ verbose_name = _("General Engagement Survey")
+ verbose_name_plural = _("General Engagement Surveys")
+
+ def __str__(self):
+ return self.survey.name
+
+ def clean(self):
+ if self.expiration and timezone.is_naive(self.expiration):
+ self.expiration = timezone.make_aware(self.expiration)
+
+
+with warnings.catch_warnings(action="ignore", category=ManagerInheritanceWarning):
+ class Answer(PolymorphicModel, TimeStampedModel):
+
+ """Base Answer model"""
+
+ question = models.ForeignKey("dojo.Question", on_delete=models.CASCADE)
+
+ answered_survey = models.ForeignKey("dojo.Answered_Survey",
+ null=False,
+ blank=False,
+ on_delete=models.CASCADE)
+ objects = models.Manager()
+ polymorphic = PolymorphicManager()
+
+
+class TextAnswer(Answer):
+ answer = models.TextField(
+ blank=False,
+ help_text=_("The answer text"),
+ default="")
+ objects = PolymorphicManager()
+
+ def __str__(self):
+ return self.answer
+
+
+class ChoiceAnswer(Answer):
+ answer = models.ManyToManyField(
+ "dojo.Choice",
+ help_text=_("The selected choices as the answer"))
+ objects = PolymorphicManager()
+
+ def __str__(self):
+ if len(self.answer.all()):
+ return str(self.answer.all()[0])
+ return "No Response"
diff --git a/dojo/survey/ui/__init__.py b/dojo/survey/ui/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/dojo/survey/ui/filters.py b/dojo/survey/ui/filters.py
new file mode 100644
index 00000000000..c1cac668f04
--- /dev/null
+++ b/dojo/survey/ui/filters.py
@@ -0,0 +1,63 @@
+import warnings
+
+import six
+from django.contrib.contenttypes.models import ContentType
+from django.utils.translation import gettext_lazy as _
+from django_filters import BooleanFilter, CharFilter, FilterSet
+from django_filters.filters import ChoiceFilter
+from polymorphic.base import ManagerInheritanceWarning
+
+from dojo.survey.models import ChoiceQuestion, Engagement_Survey, Question, TextQuestion
+
+
+class QuestionnaireFilter(FilterSet):
+ name = CharFilter(lookup_expr="icontains")
+ description = CharFilter(lookup_expr="icontains")
+ active = BooleanFilter()
+
+ class Meta:
+ model = Engagement_Survey
+ exclude = ["questions"]
+
+ survey_set = FilterSet
+
+
+class QuestionTypeFilter(ChoiceFilter):
+ def any(self, qs, name):
+ return qs.all()
+
+ def text_question(self, qs, name):
+ return qs.filter(polymorphic_ctype=ContentType.objects.get_for_model(TextQuestion))
+
+ def choice_question(self, qs, name):
+ return qs.filter(polymorphic_ctype=ContentType.objects.get_for_model(ChoiceQuestion))
+
+ options = {
+ None: (_("Any"), any), # noqa: A003 -- shadows builtin; matches original dojo/filters.py pattern
+ 1: (_("Text Question"), text_question),
+ 2: (_("Choice Question"), choice_question),
+ }
+
+ def __init__(self, *args, **kwargs):
+ kwargs["choices"] = [
+ (key, value[0]) for key, value in six.iteritems(self.options)]
+ super().__init__(*args, **kwargs)
+
+ def filter(self, qs, value):
+ try:
+ value = int(value)
+ except (ValueError, TypeError):
+ value = None
+ return self.options[value][1](self, qs, self.options[value][0])
+
+
+with warnings.catch_warnings(action="ignore", category=ManagerInheritanceWarning):
+ class QuestionFilter(FilterSet):
+ text = CharFilter(lookup_expr="icontains")
+ type = QuestionTypeFilter()
+
+ class Meta:
+ model = Question
+ exclude = ["polymorphic_ctype", "created", "modified", "order"]
+
+ question_set = FilterSet
diff --git a/dojo/survey/ui/forms.py b/dojo/survey/ui/forms.py
new file mode 100644
index 00000000000..72d1898f4b0
--- /dev/null
+++ b/dojo/survey/ui/forms.py
@@ -0,0 +1,417 @@
+import json
+import warnings
+from datetime import datetime
+
+from crispy_forms.bootstrap import InlineCheckboxes, InlineRadios
+from crispy_forms.helper import FormHelper
+from crispy_forms.layout import Layout
+from django import forms
+from django.db.models import Count
+from django.utils import timezone
+from polymorphic.base import ManagerInheritanceWarning
+
+from dojo.survey.models import (
+ Answered_Survey,
+ Choice,
+ ChoiceAnswer,
+ ChoiceQuestion,
+ Engagement_Survey,
+ General_Survey,
+ Question,
+ TextAnswer,
+ TextQuestion,
+)
+from dojo.user.queries import get_authorized_users
+
+
+class MultipleSelectWithPop(forms.SelectMultiple):
+ def render(self, name, *args, **kwargs):
+ from django.utils.safestring import mark_safe # noqa: PLC0415 -- lazy import, avoids circular dependency
+ html = super().render(name, *args, **kwargs)
+ popup_plus = ''
+ return mark_safe(popup_plus)
+
+
+# ==============================
+# Defect Dojo Engaegment Surveys
+# ==============================
+
+# List of validator_name:func_name
+# Show in admin a multichoice list of validator names
+# pass this to form using field_name='validator_name' ?
+class QuestionForm(forms.Form):
+
+ """Base class for a Question"""
+
+ def __init__(self, *args, **kwargs):
+ self.helper = FormHelper()
+ self.helper.form_method = "post"
+
+ # If true crispy-forms will render a tags
+ self.helper.form_tag = kwargs.pop("form_tag", True)
+
+ self.engagement_survey = kwargs.get("engagement_survey")
+
+ self.answered_survey = kwargs.get("answered_survey")
+ if not self.answered_survey:
+ del kwargs["engagement_survey"]
+ else:
+ del kwargs["answered_survey"]
+
+ self.helper.form_class = kwargs.get("form_class", "")
+
+ self.question = kwargs.pop("question", None)
+
+ if not self.question:
+ msg = "Need a question to render"
+ raise ValueError(msg)
+
+ super().__init__(*args, **kwargs)
+
+
+class TextQuestionForm(QuestionForm):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ # work out initial data
+
+ initial_answer = TextAnswer.objects.filter(
+ answered_survey=self.answered_survey,
+ question=self.question,
+ )
+
+ initial_answer = initial_answer[0].answer if initial_answer.exists() else ""
+
+ self.fields["answer"] = forms.CharField(
+ label=self.question.text,
+ widget=forms.Textarea(attrs={"rows": 3, "cols": 10}),
+ required=not self.question.optional,
+ initial=initial_answer,
+ )
+
+ def save(self):
+ if not self.is_valid():
+ msg = "form is not valid"
+ raise forms.ValidationError(msg)
+
+ answer = self.cleaned_data.get("answer")
+
+ if not answer:
+ if self.fields["answer"].required:
+ msg = "Required"
+ raise forms.ValidationError(msg)
+ return
+
+ text_answer, created = TextAnswer.objects.get_or_create(
+ answered_survey=self.answered_survey,
+ question=self.question,
+ )
+
+ if created:
+ text_answer.answered_survey = self.answered_survey
+ text_answer.answer = answer
+ text_answer.save()
+
+
+class ChoiceQuestionForm(QuestionForm):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ choices = [(c.id, c.label) for c in self.question.choices.all()]
+
+ # initial values
+
+ initial_choices = []
+ choice_answer = ChoiceAnswer.objects.filter(
+ answered_survey=self.answered_survey,
+ question=self.question,
+ ).annotate(a=Count("answer")).filter(a__gt=0)
+
+ # we have ChoiceAnswer instance
+ if choice_answer:
+ choice_answer = choice_answer[0]
+ initial_choices = list(choice_answer.answer.all().values_list("id", flat=True))
+ if self.question.multichoice is False:
+ initial_choices = initial_choices[0]
+
+ # default classes
+ widget = forms.RadioSelect
+ field_type = forms.ChoiceField
+ inline_type = InlineRadios
+
+ if self.question.multichoice:
+ field_type = forms.MultipleChoiceField
+ widget = forms.CheckboxSelectMultiple
+ inline_type = InlineCheckboxes
+
+ field = field_type(
+ label=self.question.text,
+ required=not self.question.optional,
+ choices=choices,
+ initial=initial_choices,
+ widget=widget,
+ )
+
+ self.fields["answer"] = field
+
+ # Render choice buttons inline
+ self.helper.layout = Layout(
+ inline_type("answer"),
+ )
+
+ def clean_answer(self):
+ real_answer = self.cleaned_data.get("answer")
+
+ # for single choice questions, the selected answer is a single string
+ if not isinstance(real_answer, list):
+ real_answer = [real_answer]
+ return real_answer
+
+ def save(self):
+ if not self.is_valid():
+ msg = "Form is not valid"
+ raise forms.ValidationError(msg)
+
+ real_answer = self.cleaned_data.get("answer")
+
+ if not real_answer:
+ if self.fields["answer"].required:
+ msg = "Required"
+ raise forms.ValidationError(msg)
+ return
+
+ choices = Choice.objects.filter(id__in=real_answer)
+
+ # find ChoiceAnswer and filter in answer !
+ choice_answer = ChoiceAnswer.objects.filter(
+ answered_survey=self.answered_survey,
+ question=self.question,
+ )
+
+ # we have ChoiceAnswer instance
+ if choice_answer:
+ choice_answer = choice_answer[0]
+
+ if not choice_answer:
+ # create a ChoiceAnswer
+ choice_answer = ChoiceAnswer.objects.create(
+ answered_survey=self.answered_survey,
+ question=self.question,
+ )
+
+ # re save out the choices
+ choice_answer.answered_survey = self.answered_survey
+ choice_answer.answer.set(choices)
+ choice_answer.save()
+
+
+class Add_Questionnaire_Form(forms.ModelForm):
+ survey = forms.ModelChoiceField(
+ queryset=Engagement_Survey.objects.all(),
+ required=True,
+ widget=forms.widgets.Select(),
+ help_text="Select the Questionnaire to add.")
+
+ class Meta:
+ model = Answered_Survey
+ exclude = ("responder",
+ "completed",
+ "engagement",
+ "answered_on",
+ "assignee")
+
+
+class AddGeneralQuestionnaireForm(forms.ModelForm):
+ survey = forms.ModelChoiceField(
+ queryset=Engagement_Survey.objects.all(),
+ required=True,
+ widget=forms.widgets.Select(),
+ help_text="Select the Questionnaire to add.")
+ expiration = forms.DateField(widget=forms.TextInput(
+ attrs={"class": "datepicker", "autocomplete": "off"}))
+
+ class Meta:
+ model = General_Survey
+ exclude = ("num_responses", "generated")
+
+ # date can only be today or in the past, not the future
+ def clean_expiration(self):
+ expiration = self.cleaned_data.get("expiration", None)
+ if expiration:
+ today = datetime.today().date()
+ if expiration < today:
+ msg = "The expiration cannot be in the past"
+ raise forms.ValidationError(msg)
+ if expiration == today:
+ msg = "The expiration cannot be today"
+ raise forms.ValidationError(msg)
+ return timezone.make_aware(
+ datetime.combine(expiration, datetime.min.time()),
+ )
+ msg = "An expiration for the survey must be supplied"
+ raise forms.ValidationError(msg)
+
+
+class Delete_Questionnaire_Form(forms.ModelForm):
+ id = forms.IntegerField(required=True,
+ widget=forms.widgets.HiddenInput())
+
+ class Meta:
+ model = Answered_Survey
+ fields = ["id"]
+
+
+class DeleteGeneralQuestionnaireForm(forms.ModelForm):
+ id = forms.IntegerField(required=True,
+ widget=forms.widgets.HiddenInput())
+
+ class Meta:
+ model = General_Survey
+ fields = ["id"]
+
+
+class Delete_Eng_Survey_Form(forms.ModelForm):
+ id = forms.IntegerField(required=True,
+ widget=forms.widgets.HiddenInput())
+
+ class Meta:
+ model = Engagement_Survey
+ fields = ["id"]
+
+
+class CreateQuestionnaireForm(forms.ModelForm):
+ class Meta:
+ model = Engagement_Survey
+ exclude = ["questions"]
+
+
+with warnings.catch_warnings(action="ignore", category=ManagerInheritanceWarning):
+ class EditQuestionnaireQuestionsForm(forms.ModelForm):
+ questions = forms.ModelMultipleChoiceField(
+ Question.polymorphic.all(),
+ required=True,
+ help_text="Select questions to include on this questionnaire. Field can be used to search available questions.",
+ widget=MultipleSelectWithPop(attrs={"size": "11"}))
+
+ class Meta:
+ model = Engagement_Survey
+ exclude = ["name", "description", "active"]
+
+
+class CreateQuestionForm(forms.Form):
+ type = forms.ChoiceField(
+ choices=(("---", "-----"), ("text", "Text"), ("choice", "Choice")))
+ order = forms.IntegerField(
+ min_value=1,
+ widget=forms.TextInput(attrs={"data-type": "both"}),
+ help_text="The order the question will appear on the questionnaire")
+ optional = forms.BooleanField(help_text="If selected, user doesn't have to answer this question",
+ initial=False,
+ required=False,
+ widget=forms.CheckboxInput(attrs={"data-type": "both"}))
+ text = forms.CharField(widget=forms.Textarea(attrs={"data-type": "text"}),
+ label="Question Text",
+ help_text="The actual question.")
+
+
+class CreateTextQuestionForm(forms.Form):
+ class Meta:
+ model = TextQuestion
+ exclude = ["order", "optional"]
+
+
+class MultiWidgetBasic(forms.widgets.MultiWidget):
+ def __init__(self, attrs=None):
+ widgets = [forms.TextInput(attrs={"data-type": "choice"}),
+ forms.TextInput(attrs={"data-type": "choice"}),
+ forms.TextInput(attrs={"data-type": "choice"}),
+ forms.TextInput(attrs={"data-type": "choice"}),
+ forms.TextInput(attrs={"data-type": "choice"}),
+ forms.TextInput(attrs={"data-type": "choice"})]
+ super().__init__(widgets, attrs)
+
+ def decompress(self, value):
+ if value:
+ return json.loads(value)
+ return [None, None, None, None, None, None]
+
+ def format_output(self, rendered_widgets):
+ return "
".join(rendered_widgets)
+
+
+class MultiExampleField(forms.fields.MultiValueField):
+ widget = MultiWidgetBasic
+
+ def __init__(self, *args, **kwargs):
+ list_fields = [forms.fields.CharField(required=True),
+ forms.fields.CharField(required=True),
+ forms.fields.CharField(required=False),
+ forms.fields.CharField(required=False),
+ forms.fields.CharField(required=False),
+ forms.fields.CharField(required=False)]
+ super().__init__(list_fields, *args, **kwargs)
+
+ def compress(self, values):
+ return json.dumps(values)
+
+
+class CreateChoiceQuestionForm(forms.Form):
+ multichoice = forms.BooleanField(required=False,
+ initial=False,
+ widget=forms.CheckboxInput(attrs={"data-type": "choice"}),
+ help_text="Can more than one choice can be selected?")
+
+ answer_choices = MultiExampleField(required=False, widget=MultiWidgetBasic(attrs={"data-type": "choice"}))
+
+ class Meta:
+ model = ChoiceQuestion
+ exclude = ["order", "optional", "choices"]
+
+
+class EditQuestionForm(forms.ModelForm):
+ class Meta:
+ model = Question
+ exclude = []
+
+
+class EditTextQuestionForm(EditQuestionForm):
+ class Meta:
+ model = TextQuestion
+ exclude = []
+
+
+class EditChoiceQuestionForm(EditQuestionForm):
+ choices = forms.ModelMultipleChoiceField(
+ Choice.objects.all(),
+ required=True,
+ help_text="Select choices to include on this question. Field can be used to search available choices.",
+ widget=MultipleSelectWithPop(attrs={"size": "11"}))
+
+ class Meta:
+ model = ChoiceQuestion
+ exclude = []
+
+
+class AddChoicesForm(forms.ModelForm):
+ class Meta:
+ model = Choice
+ exclude = []
+
+
+class AssignUserForm(forms.ModelForm):
+ assignee = forms.CharField(required=False,
+ widget=forms.widgets.HiddenInput())
+
+ def __init__(self, *args, **kwargs):
+ assignee = None
+ if "assignee" in kwargs:
+ assignee = kwargs.pop("asignees")
+ super().__init__(*args, **kwargs)
+ if assignee is None:
+ self.fields["assignee"] = forms.ModelChoiceField(queryset=get_authorized_users("view"), empty_label="Not Assigned", required=False)
+ else:
+ self.fields["assignee"].initial = assignee
+
+ class Meta:
+ model = Answered_Survey
+ exclude = ["engagement", "survey", "responder", "completed", "answered_on"]
diff --git a/dojo/survey/urls.py b/dojo/survey/ui/urls.py
similarity index 94%
rename from dojo/survey/urls.py
rename to dojo/survey/ui/urls.py
index a592719aaf2..43865e055a1 100644
--- a/dojo/survey/urls.py
+++ b/dojo/survey/ui/urls.py
@@ -3,16 +3,9 @@
@author: jay7958
"""
-from django.apps import apps
-from django.contrib import admin
from django.urls import re_path
-from dojo.survey import views
-
-if not apps.ready:
- apps.get_models()
-
-admin.autodiscover()
+from dojo.survey.ui import views
urlpatterns = [
re_path(r"^questionnaire$",
diff --git a/dojo/survey/views.py b/dojo/survey/ui/views.py
similarity index 99%
rename from dojo/survey/views.py
rename to dojo/survey/ui/views.py
index b47e6bc3502..866184829c5 100644
--- a/dojo/survey/views.py
+++ b/dojo/survey/ui/views.py
@@ -18,11 +18,28 @@
user_has_permission,
user_has_permission_or_403,
)
-from dojo.filters import QuestionFilter, QuestionnaireFilter
from dojo.forms import (
+ AddEngagementForm,
+ ExistingEngagementForm,
+)
+from dojo.models import (
+ Engagement,
+ System_Settings,
+)
+from dojo.survey.models import (
+ Answer,
+ Answered_Survey,
+ Choice,
+ ChoiceQuestion,
+ Engagement_Survey,
+ General_Survey,
+ Question,
+ TextQuestion,
+)
+from dojo.survey.ui.filters import QuestionFilter, QuestionnaireFilter
+from dojo.survey.ui.forms import (
Add_Questionnaire_Form,
AddChoicesForm,
- AddEngagementForm,
AddGeneralQuestionnaireForm,
AssignUserForm,
CreateChoiceQuestionForm,
@@ -35,19 +52,6 @@
EditChoiceQuestionForm,
EditQuestionnaireQuestionsForm,
EditTextQuestionForm,
- ExistingEngagementForm,
-)
-from dojo.models import (
- Answer,
- Answered_Survey,
- Choice,
- ChoiceQuestion,
- Engagement,
- Engagement_Survey,
- General_Survey,
- Question,
- System_Settings,
- TextQuestion,
)
from dojo.utils import add_breadcrumb, get_page_items, get_setting
diff --git a/dojo/urls.py b/dojo/urls.py
index 706fe9225f3..28d2427cebd 100644
--- a/dojo/urls.py
+++ b/dojo/urls.py
@@ -39,7 +39,7 @@
from dojo.asset.api.urls import add_asset_urls
from dojo.asset.urls import urlpatterns as asset_urls
from dojo.banner.urls import urlpatterns as banner_urls
-from dojo.benchmark.urls import urlpatterns as benchmark_urls
+from dojo.benchmark.ui.urls import urlpatterns as benchmark_urls
from dojo.components.urls import urlpatterns as component_urls
from dojo.development_environment.urls import urlpatterns as dev_env_urls
from dojo.endpoint.api.urls import add_endpoint_urls, register_endpoint_meta_import
@@ -68,7 +68,7 @@
from dojo.reports.urls import urlpatterns as reports_urls
from dojo.search.urls import urlpatterns as search_urls
from dojo.sla_config.urls import urlpatterns as sla_urls
-from dojo.survey.urls import urlpatterns as survey_urls
+from dojo.survey.ui.urls import urlpatterns as survey_urls
from dojo.system_settings.api.urls import add_system_settings_urls
from dojo.system_settings.ui.urls import urlpatterns as system_settings_urls
from dojo.test.api.urls import add_test_urls
diff --git a/unittests/test_survey_forms.py b/unittests/test_survey_forms.py
index 9526c44424a..1945f2d2245 100644
--- a/unittests/test_survey_forms.py
+++ b/unittests/test_survey_forms.py
@@ -1,6 +1,6 @@
import json
-from dojo.forms import MultiExampleField, MultiWidgetBasic
+from dojo.survey.ui.forms import MultiExampleField, MultiWidgetBasic
from unittests.dojo_test_case import DojoTestCase