Skip to content

Commit 64b6497

Browse files
Maffoochclaude
andcommitted
Replace DD_CLOUD_BANNER with centralized additional_banners system
Migrate all promotional messaging to the additional_banners context processor pattern. Product announcements now store banners in the session for rendering via the unified template loop. Each banner carries a source field (os, product_announcement) so downstream repos can filter by origin. - Remove DD_CREATE_CLOUD_BANNER setting and env var entirely - Repurpose ProductAnnouncementManager to use session-based banners - Remove evaluate_pro_proposition celery task and beat schedule - Remove create_announcement_banner from initialization command - Simplify announcement signal to remove cloud-specific logic - Add SHOW_PLG_LINK context variable for PLG menu item control - Rename os-banner-* CSS classes to generic banner-* classes - Add data-source attribute to banner template markup - Switch OS message bucket URL from dev to prod - Add 52 tests covering context processor and product announcements Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 8056eae commit 64b6497

11 files changed

Lines changed: 315 additions & 119 deletions

File tree

dojo/announcement/os_message.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
logger = logging.getLogger(__name__)
99

10-
BUCKET_URL = "https://storage.googleapis.com/defectdojo-os-messages-dev/open_source_message.md"
10+
BUCKET_URL = "https://storage.googleapis.com/defectdojo-os-messages-prod/open_source_message.md"
1111
CACHE_SECONDS = 3600
1212
HTTP_TIMEOUT_SECONDS = 2
1313
CACHE_KEY = "os_message:v1"

dojo/announcement/signals.py

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
from django.conf import settings
21
from django.db.models.signals import post_save
32
from django.dispatch import receiver
43

@@ -7,22 +6,11 @@
76

87
@receiver(post_save, sender=Dojo_User)
98
def add_announcement_to_new_user(sender, instance, **kwargs):
10-
announcements = Announcement.objects.all()
11-
if announcements.count() > 0:
12-
dojo_user = Dojo_User.objects.get(id=instance.id)
13-
announcement = announcements.first()
14-
cloud_announcement = (
15-
"DefectDojo Pro Cloud and On-Premise Subscriptions Now Available!"
16-
in announcement.message
9+
announcement = Announcement.objects.first()
10+
if announcement is not None:
11+
UserAnnouncement.objects.get_or_create(
12+
user=instance, announcement=announcement,
1713
)
18-
if not cloud_announcement or settings.CREATE_CLOUD_BANNER:
19-
user_announcements = UserAnnouncement.objects.filter(
20-
user=dojo_user, announcement=announcement,
21-
)
22-
if user_announcements.count() == 0:
23-
UserAnnouncement.objects.get_or_create(
24-
user=dojo_user, announcement=announcement,
25-
)
2614

2715

2816
@receiver(post_save, sender=Announcement)

dojo/context_processors.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,19 +36,30 @@ def globalize_vars(request):
3636
"DOCUMENTATION_URL": settings.DOCUMENTATION_URL,
3737
"API_TOKENS_ENABLED": settings.API_TOKENS_ENABLED,
3838
"API_TOKEN_AUTH_ENDPOINT_ENABLED": settings.API_TOKEN_AUTH_ENDPOINT_ENABLED,
39-
"CREATE_CLOUD_BANNER": settings.CREATE_CLOUD_BANNER,
39+
"SHOW_PLG_LINK": True,
4040
# V3 Feature Flags
4141
"V3_FEATURE_LOCATIONS": settings.V3_FEATURE_LOCATIONS,
4242
}
4343

44+
additional_banners = []
45+
4446
if (os_banner := get_os_banner()) is not None:
45-
context["additional_banners"] = [{
47+
additional_banners.append({
48+
"source": "os",
4649
"message": os_banner["message"],
4750
"style": "info",
4851
"url": "",
4952
"link_text": "",
5053
"expanded_html": os_banner["expanded_html"],
51-
}]
54+
})
55+
56+
if hasattr(request, "session"):
57+
for banner in request.session.pop("_product_banners", []):
58+
additional_banners.append(banner)
59+
60+
if additional_banners:
61+
context["additional_banners"] = additional_banners
62+
5263
return context
5364

5465

dojo/management/commands/complete_initialization.py

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
from django.db.utils import ProgrammingError
1515

1616
from dojo.auditlog import configure_pghistory_triggers
17-
from dojo.models import Announcement, Dojo_User, UserAnnouncement
1817

1918

2019
class Command(BaseCommand):
@@ -38,13 +37,11 @@ def handle(self, *args: Any, **options: Any) -> None:
3837

3938
if self.admin_user_exists():
4039
self.stdout.write("Admin user already exists; skipping first-boot setup")
41-
self.create_announcement_banner()
4240
self.initialize_data()
4341
return
4442

4543
self.ensure_admin_secrets()
4644
self.first_boot_setup()
47-
self.create_announcement_banner()
4845
self.initialize_data()
4946

5047
# ------------------------------------------------------------------
@@ -58,29 +55,6 @@ def initialize_data(self) -> None:
5855
self.stdout.write("Initializing non-standard permissions")
5956
call_command("initialize_permissions")
6057

61-
def create_announcement_banner(self) -> None:
62-
if os.getenv("DD_CREATE_CLOUD_BANNER"):
63-
return
64-
65-
self.stdout.write("Creating announcement banner")
66-
67-
announcement, _ = Announcement.objects.get_or_create(id=1)
68-
announcement.message = (
69-
'<a href="https://cloud.defectdojo.com/accounts/onboarding/plg_step_1" '
70-
'target="_blank">'
71-
"DefectDojo Pro Cloud and On-Premise Subscriptions Now Available! "
72-
"Create an account to try Pro for free!"
73-
"</a>"
74-
)
75-
announcement.dismissable = True
76-
announcement.save()
77-
78-
for user in Dojo_User.objects.all():
79-
UserAnnouncement.objects.get_or_create(
80-
user=user,
81-
announcement=announcement,
82-
)
83-
8458
# ------------------------------------------------------------------
8559
# Auditlog consistency
8660
# ------------------------------------------------------------------

dojo/product_announcements.py

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11

22
import logging
33

4-
from django.conf import settings
5-
from django.contrib import messages
64
from django.http import HttpRequest, HttpResponse
75
from django.utils.safestring import mark_safe
86
from django.utils.translation import gettext_lazy as _
@@ -30,12 +28,8 @@ def __init__(
3028
response_data: dict | None = None,
3129
**kwargs: dict,
3230
):
33-
"""Skip all this if the CREATE_CLOUD_BANNER is not set"""
34-
if not settings.CREATE_CLOUD_BANNER:
35-
return
36-
# Fill in the vars if the were supplied correctly
3731
if request is not None and isinstance(request, HttpRequest):
38-
self._add_django_message(
32+
self._add_session_banner(
3933
request=request,
4034
message=mark_safe(f"{self.base_message} {self.ui_outreach}"),
4135
)
@@ -51,18 +45,21 @@ def __init__(
5145
msg = "At least one of request, response, or response_data must be supplied"
5246
raise ValueError(msg)
5347

54-
def _add_django_message(self, request: HttpRequest, message: str):
55-
"""Add a message to the UI"""
48+
def _add_session_banner(self, request: HttpRequest, message: str):
49+
"""Store a banner in the session for rendering via additional_banners."""
5650
try:
57-
messages.add_message(
58-
request=request,
59-
level=messages.INFO,
60-
message=_(message),
61-
extra_tags="alert-info",
62-
)
51+
banners = request.session.get("_product_banners", [])
52+
banners.append({
53+
"source": "product_announcement",
54+
"message": str(_(message)),
55+
"style": "info",
56+
"url": "",
57+
"link_text": "",
58+
"expanded_html": None,
59+
})
60+
request.session["_product_banners"] = banners
6361
except Exception:
64-
# make sure we catch any exceptions that might happen: https://github.com/DefectDojo/django-DefectDojo/issues/14041
65-
logger.exception(f"Error adding message to Django: {message}")
62+
logger.exception(f"Error storing product announcement banner: {message}")
6663

6764
def _add_api_response_key(self, message: str, data: dict) -> dict:
6865
"""Update the response data in place"""

dojo/settings/settings.dist.py

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -356,8 +356,6 @@
356356
DD_HASHCODE_FIELDS_PER_SCANNER=(str, ""),
357357
# Set deduplication algorithms per parser, via en env variable that contains a JSON string
358358
DD_DEDUPLICATION_ALGORITHM_PER_PARSER=(str, ""),
359-
# Dictates whether cloud banner is created or not
360-
DD_CREATE_CLOUD_BANNER=(bool, True),
361359
# With this setting turned on, Dojo maintains an audit log of changes made to entities (Findings, Tests, Engagements, Products, ...)
362360
# If you run big import you may want to disable this because there's a performance hit during (re-)imports.
363361
DD_ENABLE_AUDITLOG=(bool, True),
@@ -1339,13 +1337,6 @@ def saml2_attrib_map_format(din):
13391337
"expires": int(60 * 1 * 1.2), # If a task is not executed within 72 seconds, it should be dropped from the queue. Two more tasks should be scheduled in the meantime.
13401338
},
13411339
},
1342-
"trigger_evaluate_pro_proposition": {
1343-
"task": "dojo.tasks.evaluate_pro_proposition",
1344-
"schedule": timedelta(hours=8),
1345-
"options": {
1346-
"expires": int(60 * 60 * 8 * 1.2), # If a task is not executed within 9.6 hours, it should be dropped from the queue. Two more tasks should be scheduled in the meantime.
1347-
},
1348-
},
13491340
"clear_sessions": {
13501341
"task": "dojo.tasks.clear_sessions",
13511342
"schedule": crontab(hour=0, minute=0, day_of_week=0),
@@ -2082,9 +2073,6 @@ def saml2_attrib_map_format(din):
20822073
AUDITLOG_DISABLE_ON_RAW_SAVE = False
20832074
# You can set extra Jira headers by suppling a dictionary in header: value format (pass as env var like "headr_name=value,another_header=anohter_value")
20842075
ADDITIONAL_HEADERS = env("DD_ADDITIONAL_HEADERS")
2085-
# Dictates whether cloud banner is created or not
2086-
CREATE_CLOUD_BANNER = env("DD_CREATE_CLOUD_BANNER")
2087-
20882076
# ------------------------------------------------------------------------------
20892077
# Auditlog
20902078
# ------------------------------------------------------------------------------

dojo/static/dojo/css/dojo.css

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1124,9 +1124,20 @@ div.custom-search-form {
11241124
.announcement-banner {
11251125
margin: 0px -15px;
11261126
border-radius: 0px 0px 4px 4px;
1127+
color: #000;
1128+
}
1129+
1130+
.announcement-banner a {
1131+
color: #0645ad;
1132+
text-decoration: underline;
1133+
}
1134+
1135+
.announcement-banner strong,
1136+
.announcement-banner b {
1137+
color: #222;
11271138
}
11281139

1129-
.os-banner-toggle {
1140+
.banner-toggle {
11301141
background: transparent;
11311142
border: 0;
11321143
padding: 0;
@@ -1136,17 +1147,17 @@ div.custom-search-form {
11361147
line-height: 1;
11371148
}
11381149

1139-
.os-banner-toggle:focus,
1140-
.os-banner-toggle:active {
1150+
.banner-toggle:focus,
1151+
.banner-toggle:active {
11411152
outline: none;
11421153
box-shadow: none;
11431154
}
11441155

1145-
.os-banner-toggle:not(.collapsed) .fa-caret-down {
1156+
.banner-toggle:not(.collapsed) .fa-caret-down {
11461157
transform: rotate(180deg);
11471158
}
11481159

1149-
.os-banner-expanded {
1160+
.banner-expanded {
11501161
margin-top: 8px;
11511162
}
11521163

dojo/tasks.py

Lines changed: 1 addition & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,8 @@
1616
from dojo.celery import app
1717
from dojo.celery_dispatch import dojo_dispatch_task
1818
from dojo.finding.helper import fix_loop_duplicates
19-
from dojo.location.models import Location
2019
from dojo.management.commands.jira_status_reconciliation import jira_status_reconciliation
21-
from dojo.models import Alerts, Announcement, Endpoint, Engagement, Finding, Product, System_Settings, User
20+
from dojo.models import Alerts, Engagement, Finding, Product, System_Settings, User
2221
from dojo.notifications.helper import create_notification
2322
from dojo.utils import calculate_grade, sla_compute_and_notify
2423

@@ -218,37 +217,6 @@ def fix_loop_duplicates_task(*args, **kwargs):
218217
return fix_loop_duplicates()
219218

220219

221-
@app.task
222-
def evaluate_pro_proposition(*args, **kwargs):
223-
# Ensure we should be doing this
224-
if not settings.CREATE_CLOUD_BANNER:
225-
return
226-
# Get the announcement object
227-
announcement = Announcement.objects.get_or_create(id=1)[0]
228-
# Quick check for a user has modified the current banner - if not, exit early as we dont want to stomp
229-
if not any(
230-
entry in announcement.message
231-
for entry in [
232-
"",
233-
"DefectDojo Pro Cloud and On-Premise Subscriptions Now Available!",
234-
"Findings/Endpoints in their systems",
235-
]
236-
):
237-
return
238-
# Count the objects the determine if the banner should be updated
239-
if settings.V3_FEATURE_LOCATIONS:
240-
object_count = Finding.objects.count() + Location.objects.count()
241-
else:
242-
# TODO: Delete this after the move to Locations
243-
object_count = Finding.objects.count() + Endpoint.objects.count()
244-
# Unless the count is greater than 100k, exit early
245-
if object_count < 100000:
246-
return
247-
# Update the announcement
248-
announcement.message = f'Only professionals have {object_count:,} Findings and Endpoints in their systems... <a href="https://www.defectdojo.com/pricing" target="_blank">Get DefectDojo Pro</a> today!'
249-
announcement.save()
250-
251-
252220
@app.task
253221
def clear_sessions(*args, **kwargs):
254222
call_command("clearsessions")

dojo/templates/base.html

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@
199199
</a>
200200
</li>
201201
{% endif %}
202-
{% if CREATE_CLOUD_BANNER %}
202+
{% if SHOW_PLG_LINK %}
203203
<li>
204204
<a href="https://cloud.defectdojo.com/accounts/onboarding/plg_step_1">
205205
<i class="fa-solid fa-level-up fa-fw"></i>
@@ -671,17 +671,18 @@
671671
</div>
672672
{% endif %}
673673
{% for banner in additional_banners %}
674-
<div role="alert" class="announcement-banner alert alert-{{ banner.style }} show">
674+
<div role="alert" class="announcement-banner alert alert-{{ banner.style }} show"
675+
data-source="{{ banner.source }}">
675676
{{ banner.message|safe }}{% if banner.url %} <a href="{{ banner.url }}">{{ banner.link_text }}</a>{% endif %}
676677
{% if banner.expanded_html %}
677-
<button type="button" class="os-banner-toggle collapsed"
678+
<button type="button" class="banner-toggle collapsed"
678679
data-toggle="collapse"
679-
data-target="#os-banner-expanded-{{ forloop.counter }}"
680+
data-target="#banner-expanded-{{ forloop.counter }}"
680681
aria-expanded="false"
681-
aria-controls="os-banner-expanded-{{ forloop.counter }}">
682+
aria-controls="banner-expanded-{{ forloop.counter }}">
682683
<i class="fa-solid fa-caret-down"></i>
683684
</button>
684-
<div id="os-banner-expanded-{{ forloop.counter }}" class="collapse os-banner-expanded">
685+
<div id="banner-expanded-{{ forloop.counter }}" class="collapse banner-expanded">
685686
{{ banner.expanded_html|safe }}
686687
</div>
687688
{% endif %}

0 commit comments

Comments
 (0)