mirror of
https://github.com/boostorg/website-v2.git
synced 2026-01-19 04:42:17 +00:00
@@ -157,10 +157,14 @@ Update database values in settings to use the same host, user, password, and the
|
|||||||
run `django-admin migrate --pythonpath example_project --settings settings`
|
run `django-admin migrate --pythonpath example_project --settings settings`
|
||||||
|
|
||||||
Give your ssh key to Sam so he can add it to the boost.cpp.al server, and then download the mailman db archive and cp the sql to the docker container
|
Give your ssh key to Sam so he can add it to the boost.cpp.al server, and then download the mailman db archive and cp the sql to the docker container
|
||||||
|
|
||||||
|
Create a database in your postgres instance called `hyperkitty_db`, then:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
scp {user}@staging-db1.boost.cpp.al:/tmp/lists_stage_web.staging-db1-2.2025-02-06-08-00-01.sql.gz .
|
scp {user}@staging-db1.boost.cpp.al:/tmp/lists_stage_web.staging-db1-2.2025-02-06-08-00-01.sql.gz .
|
||||||
docker cp lists_stage_web.staging-db1-2.2025-02-06-08-00-01.sql website-v2-web-1:/lists_stage_web.staging-db1-2.2025-02-06-08-00-01.sql
|
docker cp lists_stage_web.staging-db1-2.2025-02-06-08-00-01.sql website-v2-web-1:/lists_stage_web.staging-db1-2.2025-02-06-08-00-01.sql
|
||||||
docker exec -it website-v2-web-1 /bin/bash
|
docker exec -it website-v2-web-1 /bin/bash
|
||||||
|
apt update && apt -y install postgresql
|
||||||
psql -U postgres -W hyperkitty_db < /lists_stage_web.staging-db1-2.2025-02-06-08-00-01.sql
|
psql -U postgres -W hyperkitty_db < /lists_stage_web.staging-db1-2.2025-02-06-08-00-01.sql
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -91,6 +91,8 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: docker/Dockerfile
|
dockerfile: docker/Dockerfile
|
||||||
|
args:
|
||||||
|
LOCAL_DEVELOPMENT: "true"
|
||||||
command:
|
command:
|
||||||
- /bin/bash
|
- /bin/bash
|
||||||
- -c
|
- -c
|
||||||
@@ -113,8 +115,11 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: docker/Dockerfile
|
dockerfile: docker/Dockerfile
|
||||||
|
args:
|
||||||
|
LOCAL_DEVELOPMENT: "true"
|
||||||
command: [ "celery", "-A", "config", "beat", "--loglevel=debug" ]
|
command: [ "celery", "-A", "config", "beat", "--loglevel=debug" ]
|
||||||
environment:
|
environment:
|
||||||
|
LOCAL_DEVELOPMENT: "true"
|
||||||
DEBUG_TOOLBAR: "false"
|
DEBUG_TOOLBAR: "false"
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
|
|||||||
@@ -44,7 +44,18 @@ RUN yarn build
|
|||||||
# Final image.
|
# Final image.
|
||||||
FROM python:3.13-slim AS release
|
FROM python:3.13-slim AS release
|
||||||
|
|
||||||
RUN apt update && apt install -y git libpq-dev ruby ruby-dev && rm -rf /var/lib/apt/lists/*
|
# Install system dependencies including Chromium
|
||||||
|
RUN apt update && apt install -y \
|
||||||
|
git \
|
||||||
|
libpq-dev \
|
||||||
|
ruby \
|
||||||
|
ruby-dev \
|
||||||
|
fonts-liberation \
|
||||||
|
fonts-noto \
|
||||||
|
fonts-noto-mono \
|
||||||
|
fonts-noto-color-emoji \
|
||||||
|
chromium \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Install Asciidoctor
|
# Install Asciidoctor
|
||||||
RUN gem install asciidoctor asciidoctor-boost
|
RUN gem install asciidoctor asciidoctor-boost
|
||||||
@@ -67,6 +78,9 @@ COPY --from=builder-js /code/static/css/styles.css /code/static/css/styles.css
|
|||||||
|
|
||||||
WORKDIR /code
|
WORKDIR /code
|
||||||
|
|
||||||
|
# Set environment variable for Playwright to use system Chromium
|
||||||
|
ENV PLAYWRIGHT_BROWSERS_PATH=/usr/bin
|
||||||
|
|
||||||
CMD ["gunicorn", "-c", "/code/gunicorn.conf.py", "config.wsgi"]
|
CMD ["gunicorn", "-c", "/code/gunicorn.conf.py", "config.wsgi"]
|
||||||
|
|
||||||
ARG TAG
|
ARG TAG
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
from django.core.files.storage import default_storage
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.models import F, Count, OuterRef, Window
|
from django.db.models import F, Count, OuterRef, Window
|
||||||
from django.db.models.functions import RowNumber
|
from django.db.models.functions import RowNumber
|
||||||
@@ -8,10 +9,13 @@ from django.urls import path, reverse
|
|||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
from django.views.generic import TemplateView
|
from django.views.generic import TemplateView
|
||||||
|
from django import forms
|
||||||
|
|
||||||
|
from core.admin_filters import StaffUserCreatedByFilter
|
||||||
from libraries.forms import CreateReportForm, CreateReportFullForm
|
from libraries.forms import CreateReportForm, CreateReportFullForm
|
||||||
from versions.models import Version
|
from versions.models import Version
|
||||||
from versions.tasks import import_all_library_versions
|
from versions.tasks import import_all_library_versions
|
||||||
|
from .filters import ReportConfigurationFilter
|
||||||
from .models import (
|
from .models import (
|
||||||
Category,
|
Category,
|
||||||
Commit,
|
Commit,
|
||||||
@@ -21,6 +25,7 @@ from .models import (
|
|||||||
Library,
|
Library,
|
||||||
LibraryVersion,
|
LibraryVersion,
|
||||||
PullRequest,
|
PullRequest,
|
||||||
|
ReleaseReport,
|
||||||
WordcloudMergeWord,
|
WordcloudMergeWord,
|
||||||
)
|
)
|
||||||
from .tasks import (
|
from .tasks import (
|
||||||
@@ -34,6 +39,7 @@ from .tasks import (
|
|||||||
generate_release_report,
|
generate_release_report,
|
||||||
synchronize_commit_author_user_data,
|
synchronize_commit_author_user_data,
|
||||||
)
|
)
|
||||||
|
from .utils import generate_release_report_filename
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Commit)
|
@admin.register(Commit)
|
||||||
@@ -177,7 +183,9 @@ class ReleaseReportView(TemplateView):
|
|||||||
return context
|
return context
|
||||||
|
|
||||||
def generate_report(self):
|
def generate_report(self):
|
||||||
generate_release_report.delay(self.request.GET)
|
generate_release_report.delay(
|
||||||
|
user_id=self.request.user.id, params=self.request.GET
|
||||||
|
)
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
form = self.get_form()
|
form = self.get_form()
|
||||||
@@ -440,3 +448,43 @@ class WordcloudMergeWordAdmin(admin.ModelAdmin):
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ReleaseReportAdminForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = ReleaseReport
|
||||||
|
fields = "__all__"
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
if self.instance.pk and not self.instance.published:
|
||||||
|
file_name = generate_release_report_filename(
|
||||||
|
self.instance.report_configuration.get_slug()
|
||||||
|
)
|
||||||
|
published_filename = f"{ReleaseReport.upload_dir}{file_name}"
|
||||||
|
if default_storage.exists(published_filename):
|
||||||
|
# we require users to intentionally manually delete existing reports
|
||||||
|
self.fields["published"].disabled = True
|
||||||
|
self.fields["published"].help_text = (
|
||||||
|
f"⚠️ A published '{file_name}' already exists. To prevent accidents "
|
||||||
|
"you must manually delete that file before publishing this report."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(ReleaseReport)
|
||||||
|
class ReleaseReportAdmin(admin.ModelAdmin):
|
||||||
|
form = ReleaseReportAdminForm
|
||||||
|
list_display = ["__str__", "created_at", "published", "published_at"]
|
||||||
|
list_filter = ["published", ReportConfigurationFilter, StaffUserCreatedByFilter]
|
||||||
|
search_fields = ["file"]
|
||||||
|
readonly_fields = ["created_at", "created_by"]
|
||||||
|
ordering = ["-created_at"]
|
||||||
|
|
||||||
|
def has_add_permission(self, request):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def save_model(self, request, obj, form, change):
|
||||||
|
if not change:
|
||||||
|
obj.created_by = request.user
|
||||||
|
super().save_model(request, obj, form, change)
|
||||||
|
|||||||
@@ -366,3 +366,4 @@ DEVELOP_RELEASE_URL_PATH_STR = "develop"
|
|||||||
MASTER_RELEASE_URL_PATH_STR = "master"
|
MASTER_RELEASE_URL_PATH_STR = "master"
|
||||||
VERSION_SLUG_PREFIX = "boost-"
|
VERSION_SLUG_PREFIX = "boost-"
|
||||||
RELEASE_REPORT_SEARCH_TOP_COUNTRIES_LIMIT = 5
|
RELEASE_REPORT_SEARCH_TOP_COUNTRIES_LIMIT = 5
|
||||||
|
DOCKER_CONTAINER_URL_WEB = "http://web:8000"
|
||||||
|
|||||||
22
libraries/filters.py
Normal file
22
libraries/filters.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from versions.models import ReportConfiguration
|
||||||
|
|
||||||
|
|
||||||
|
class ReportConfigurationFilter(admin.SimpleListFilter):
|
||||||
|
title = "report configuration"
|
||||||
|
parameter_name = "report_configuration"
|
||||||
|
|
||||||
|
def lookups(self, request, model_admin):
|
||||||
|
# get only ReportConfigurations that have associated ReleaseReports
|
||||||
|
configs = (
|
||||||
|
ReportConfiguration.objects.filter(releasereport__isnull=False)
|
||||||
|
.distinct()
|
||||||
|
.order_by("version")
|
||||||
|
)
|
||||||
|
return [(config.id, str(config)) for config in configs]
|
||||||
|
|
||||||
|
def queryset(self, request, queryset):
|
||||||
|
if self.value():
|
||||||
|
return queryset.filter(report_configuration_id=self.value())
|
||||||
|
return queryset
|
||||||
@@ -90,6 +90,11 @@ class CreateReportFullForm(Form):
|
|||||||
initial=False,
|
initial=False,
|
||||||
help_text="Force the page to be regenerated, do not use cache.",
|
help_text="Force the page to be regenerated, do not use cache.",
|
||||||
)
|
)
|
||||||
|
publish = BooleanField(
|
||||||
|
required=False,
|
||||||
|
initial=False,
|
||||||
|
help_text="Warning: overwrites existing published report, not reversible.",
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def cache_key(self):
|
def cache_key(self):
|
||||||
@@ -205,13 +210,16 @@ class CreateReportFullForm(Form):
|
|||||||
"library_count": self.library_queryset.count(),
|
"library_count": self.library_queryset.count(),
|
||||||
}
|
}
|
||||||
|
|
||||||
def cache_html(self):
|
def cache_html(self, base_uri=None):
|
||||||
"""Render and cache the html for this report."""
|
"""Render and cache the html for this report."""
|
||||||
# ensure we have "cleaned_data"
|
# ensure we have "cleaned_data"
|
||||||
if not self.is_valid():
|
if not self.is_valid():
|
||||||
return ""
|
return ""
|
||||||
try:
|
try:
|
||||||
html = render_to_string(self.html_template_name, self.get_stats())
|
context = self.get_stats()
|
||||||
|
if base_uri:
|
||||||
|
context["base_uri"] = base_uri
|
||||||
|
html = render_to_string(self.html_template_name, context)
|
||||||
except FileNotFoundError as e:
|
except FileNotFoundError as e:
|
||||||
html = (
|
html = (
|
||||||
f"An error occurred generating the report: {e}. To see the image "
|
f"An error occurred generating the report: {e}. To see the image "
|
||||||
|
|||||||
@@ -17,11 +17,10 @@ from core.management.actions import (
|
|||||||
ActionsManager,
|
ActionsManager,
|
||||||
send_notification,
|
send_notification,
|
||||||
)
|
)
|
||||||
from libraries.forms import CreateReportForm
|
from libraries.tasks import update_commits, generate_release_report
|
||||||
from libraries.tasks import update_commits
|
|
||||||
from reports.models import WebsiteStatReport
|
from reports.models import WebsiteStatReport
|
||||||
from slack.management.commands.fetch_slack_activity import get_my_channels, locked
|
from slack.management.commands.fetch_slack_activity import get_my_channels, locked
|
||||||
from versions.models import Version
|
from versions.models import Version, ReportConfiguration
|
||||||
|
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
|
|
||||||
@@ -30,8 +29,12 @@ class ReleaseTasksManager(ActionsManager):
|
|||||||
latest_version: Version | None = None
|
latest_version: Version | None = None
|
||||||
handled_commits: dict[str, int] = {}
|
handled_commits: dict[str, int] = {}
|
||||||
|
|
||||||
def __init__(self, should_generate_report: bool = False):
|
def __init__(
|
||||||
|
self, base_uri: str, user_id: int, should_generate_report: bool = False
|
||||||
|
):
|
||||||
|
self.base_uri = base_uri
|
||||||
self.should_generate_report = should_generate_report
|
self.should_generate_report = should_generate_report
|
||||||
|
self.user_id = user_id
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
def set_tasks(self):
|
def set_tasks(self):
|
||||||
@@ -80,20 +83,32 @@ class ReleaseTasksManager(ActionsManager):
|
|||||||
"""
|
"""
|
||||||
start_date = timezone.now() - timedelta(days=120)
|
start_date = timezone.now() - timedelta(days=120)
|
||||||
date_string = start_date.strftime("%Y-%m-%d")
|
date_string = start_date.strftime("%Y-%m-%d")
|
||||||
print(f"{date_string = }")
|
|
||||||
call_command("import_ml_counts", start_date=date_string)
|
call_command("import_ml_counts", start_date=date_string)
|
||||||
|
|
||||||
def generate_report(self):
|
def generate_report(self):
|
||||||
if not self.should_generate_report:
|
if not self.should_generate_report:
|
||||||
self.add_progress_message("Skipped - report generation not requested")
|
self.add_progress_message("Skipped - report generation not requested")
|
||||||
return
|
return
|
||||||
form = CreateReportForm({"version": self.latest_version.id})
|
|
||||||
form.cache_html()
|
report_configuration = ReportConfiguration.objects.get(
|
||||||
|
version=self.latest_version.name
|
||||||
|
)
|
||||||
|
generate_release_report.delay(
|
||||||
|
user_id=self.user_id,
|
||||||
|
params={"report_configuration": report_configuration.id, "publish": True},
|
||||||
|
base_uri=self.base_uri,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@locked(1138692)
|
@locked(1138692)
|
||||||
def run_commands(progress: list[str], generate_report: bool = False):
|
def run_commands(
|
||||||
manager = ReleaseTasksManager(should_generate_report=generate_report)
|
progress: list[str], base_uri: str, user_id: int, generate_report: bool = False
|
||||||
|
):
|
||||||
|
manager = ReleaseTasksManager(
|
||||||
|
base_uri=base_uri,
|
||||||
|
should_generate_report=generate_report,
|
||||||
|
user_id=user_id,
|
||||||
|
)
|
||||||
manager.run_tasks()
|
manager.run_tasks()
|
||||||
progress.extend(manager.progress_messages)
|
progress.extend(manager.progress_messages)
|
||||||
return manager.handled_commits
|
return manager.handled_commits
|
||||||
@@ -125,11 +140,16 @@ def bad_credentials() -> list[str]:
|
|||||||
|
|
||||||
|
|
||||||
@click.command()
|
@click.command()
|
||||||
|
@click.option(
|
||||||
|
"--base_uri",
|
||||||
|
is_flag=False,
|
||||||
|
help="The URI to which paths should be relative",
|
||||||
|
default=None,
|
||||||
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--user_id",
|
"--user_id",
|
||||||
is_flag=False,
|
is_flag=False,
|
||||||
help="The ID of the user that started this task (For notification purposes)",
|
help="The ID of the user that started this task (For notification purposes)",
|
||||||
default=None,
|
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--generate_report",
|
"--generate_report",
|
||||||
@@ -137,11 +157,11 @@ def bad_credentials() -> list[str]:
|
|||||||
help="Generate a report at the end of the command",
|
help="Generate a report at the end of the command",
|
||||||
default=False,
|
default=False,
|
||||||
)
|
)
|
||||||
def command(user_id=None, generate_report=False):
|
def command(user_id, base_uri=None, generate_report=False):
|
||||||
"""A long running chain of tasks to import and update library data."""
|
"""A long running chain of tasks to import and update library data."""
|
||||||
start = timezone.now()
|
start = timezone.now()
|
||||||
|
|
||||||
user = User.objects.filter(id=user_id).first() if user_id else None
|
user = User.objects.filter(id=user_id).first()
|
||||||
|
|
||||||
progress = ["___Progress Messages___"]
|
progress = ["___Progress Messages___"]
|
||||||
if missing_creds := bad_credentials():
|
if missing_creds := bad_credentials():
|
||||||
@@ -162,7 +182,7 @@ def command(user_id=None, generate_report=False):
|
|||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
handled_commits = run_commands(progress, generate_report)
|
handled_commits = run_commands(progress, base_uri, generate_report, user_id)
|
||||||
end = timezone.now()
|
end = timezone.now()
|
||||||
except Exception:
|
except Exception:
|
||||||
error = traceback.format_exc()
|
error = traceback.format_exc()
|
||||||
|
|||||||
55
libraries/migrations/0035_releasereport.py
Normal file
55
libraries/migrations/0035_releasereport.py
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2025-10-27 22:52
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("libraries", "0034_strip_boost_from_documentation_urls"),
|
||||||
|
("versions", "0024_alter_versionfile_checksum_and_more"),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="ReleaseReport",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"file",
|
||||||
|
models.FileField(
|
||||||
|
blank=True, null=True, upload_to="release-reports/"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("published", models.BooleanField(default=False)),
|
||||||
|
("published_at", models.DateTimeField(blank=True, null=True)),
|
||||||
|
(
|
||||||
|
"created_by",
|
||||||
|
models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"report_configuration",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="versions.reportconfiguration",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -7,6 +7,8 @@ from urllib.parse import urlparse
|
|||||||
from django.core.cache import caches
|
from django.core.cache import caches
|
||||||
from django.db import models, transaction
|
from django.db import models, transaction
|
||||||
from django.db.models import Sum
|
from django.db.models import Sum
|
||||||
|
from django.db.models.signals import pre_delete
|
||||||
|
from django.dispatch import receiver
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
@@ -21,9 +23,14 @@ from core.asciidoc import convert_adoc_to_html
|
|||||||
from core.validators import image_validator, max_file_size_validator
|
from core.validators import image_validator, max_file_size_validator
|
||||||
from libraries.managers import IssueManager
|
from libraries.managers import IssueManager
|
||||||
from mailing_list.models import EmailData
|
from mailing_list.models import EmailData
|
||||||
|
from versions.models import ReportConfiguration
|
||||||
from .constants import LIBRARY_GITHUB_URL_OVERRIDES
|
from .constants import LIBRARY_GITHUB_URL_OVERRIDES
|
||||||
|
|
||||||
from .utils import generate_random_string, write_content_to_tempfile
|
from .utils import (
|
||||||
|
generate_random_string,
|
||||||
|
write_content_to_tempfile,
|
||||||
|
generate_release_report_filename,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Category(models.Model):
|
class Category(models.Model):
|
||||||
@@ -542,3 +549,62 @@ class WordcloudMergeWord(models.Model):
|
|||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.from_word}->{self.to_word}"
|
return f"{self.from_word}->{self.to_word}"
|
||||||
|
|
||||||
|
|
||||||
|
class ReleaseReport(models.Model):
|
||||||
|
upload_dir = "release-reports/"
|
||||||
|
file = models.FileField(upload_to=upload_dir, blank=True, null=True)
|
||||||
|
report_configuration = models.ForeignKey(
|
||||||
|
ReportConfiguration, on_delete=models.CASCADE
|
||||||
|
)
|
||||||
|
|
||||||
|
created_by = models.ForeignKey(
|
||||||
|
settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True
|
||||||
|
)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
published = models.BooleanField(default=False)
|
||||||
|
published_at = models.DateTimeField(blank=True, null=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.file.name.replace(self.upload_dir, "")}"
|
||||||
|
|
||||||
|
def rename_file_to(self, filename: str, allow_overwrite: bool = False):
|
||||||
|
"""Rename the file to use the version slug from report_configuration."""
|
||||||
|
from django.core.files.storage import default_storage
|
||||||
|
|
||||||
|
current_name = self.file.name
|
||||||
|
final_filename = f"{self._meta.get_field("file").upload_to}{filename}"
|
||||||
|
if current_name == final_filename:
|
||||||
|
return
|
||||||
|
|
||||||
|
if default_storage.exists(final_filename):
|
||||||
|
if not allow_overwrite:
|
||||||
|
raise ValueError(f"{final_filename} already exists")
|
||||||
|
default_storage.delete(final_filename)
|
||||||
|
|
||||||
|
with default_storage.open(current_name, "rb") as source:
|
||||||
|
default_storage.save(final_filename, source)
|
||||||
|
# delete the old file and update the reference
|
||||||
|
default_storage.delete(current_name)
|
||||||
|
self.file.name = final_filename
|
||||||
|
|
||||||
|
def save(self, allow_overwrite=False, *args, **kwargs):
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
is_being_published = self.published and not self.published_at
|
||||||
|
if is_being_published and self.file:
|
||||||
|
new_filename = generate_release_report_filename(
|
||||||
|
self.report_configuration.get_slug(), self.published
|
||||||
|
)
|
||||||
|
self.rename_file_to(new_filename, allow_overwrite)
|
||||||
|
self.published_at = timezone.now()
|
||||||
|
super().save(update_fields=["published_at", "file"])
|
||||||
|
|
||||||
|
|
||||||
|
# Signal handler to delete files when ReleaseReport is deleted
|
||||||
|
@receiver(pre_delete, sender=ReleaseReport)
|
||||||
|
def delete_release_report_files(sender, instance, **kwargs):
|
||||||
|
"""Delete file from storage when ReleaseReport is deleted."""
|
||||||
|
if instance.file:
|
||||||
|
instance.file.delete(save=False)
|
||||||
|
|||||||
@@ -10,15 +10,26 @@ from core.boostrenderer import get_content_from_s3
|
|||||||
from core.htmlhelper import get_library_documentation_urls
|
from core.htmlhelper import get_library_documentation_urls
|
||||||
from libraries.forms import CreateReportForm, CreateReportFullForm
|
from libraries.forms import CreateReportForm, CreateReportFullForm
|
||||||
from libraries.github import LibraryUpdater
|
from libraries.github import LibraryUpdater
|
||||||
from libraries.models import Library, LibraryVersion, CommitAuthorEmail, CommitAuthor
|
from libraries.models import (
|
||||||
|
Library,
|
||||||
|
LibraryVersion,
|
||||||
|
CommitAuthorEmail,
|
||||||
|
CommitAuthor,
|
||||||
|
ReleaseReport,
|
||||||
|
)
|
||||||
from users.tasks import User
|
from users.tasks import User
|
||||||
from versions.models import Version
|
from versions.models import Version
|
||||||
from .constants import (
|
from .constants import (
|
||||||
LIBRARY_DOCS_EXCEPTIONS,
|
LIBRARY_DOCS_EXCEPTIONS,
|
||||||
LIBRARY_DOCS_MISSING,
|
LIBRARY_DOCS_MISSING,
|
||||||
VERSION_DOCS_MISSING,
|
VERSION_DOCS_MISSING,
|
||||||
|
DOCKER_CONTAINER_URL_WEB,
|
||||||
|
)
|
||||||
|
from .utils import (
|
||||||
|
version_within_range,
|
||||||
|
update_base_tag,
|
||||||
|
generate_release_report_filename,
|
||||||
)
|
)
|
||||||
from .utils import version_within_range
|
|
||||||
|
|
||||||
logger = structlog.getLogger(__name__)
|
logger = structlog.getLogger(__name__)
|
||||||
|
|
||||||
@@ -230,10 +241,75 @@ def update_issues(clean=False):
|
|||||||
|
|
||||||
|
|
||||||
@app.task
|
@app.task
|
||||||
def generate_release_report(params):
|
def generate_release_report(user_id: int, params: dict, base_uri: str = None):
|
||||||
"""Generate a release report asynchronously and save it in RenderedContent."""
|
"""Generate a release report asynchronously and save it in RenderedContent."""
|
||||||
form = CreateReportForm(params)
|
form = CreateReportForm(params)
|
||||||
form.cache_html()
|
html = form.cache_html(base_uri=base_uri)
|
||||||
|
# override the base uri to reference the internal container for local dev
|
||||||
|
if settings.LOCAL_DEVELOPMENT:
|
||||||
|
html = update_base_tag(html, DOCKER_CONTAINER_URL_WEB)
|
||||||
|
|
||||||
|
release_report = ReleaseReport(
|
||||||
|
created_by_id=user_id,
|
||||||
|
report_configuration_id=params.get("report_configuration"),
|
||||||
|
)
|
||||||
|
release_report.save()
|
||||||
|
generate_release_report_pdf.delay(
|
||||||
|
release_report.pk, html=html, publish=params.get("publish")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.task(bind=True, time_limit=300, soft_time_limit=240)
|
||||||
|
def generate_release_report_pdf(
|
||||||
|
self, release_report_id: int, html: str, publish: bool = False
|
||||||
|
):
|
||||||
|
"""Generate a release report asynchronously and save it in PDF using Playwright."""
|
||||||
|
from playwright.sync_api import sync_playwright
|
||||||
|
from django.core.files.base import ContentFile
|
||||||
|
|
||||||
|
release_report = ReleaseReport.objects.get(pk=release_report_id)
|
||||||
|
|
||||||
|
logger.info(f"{release_report_id=}, task id: {self.request.id}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
with sync_playwright() as p:
|
||||||
|
browser = p.chromium.launch(
|
||||||
|
headless=True, executable_path="/usr/bin/chromium"
|
||||||
|
)
|
||||||
|
page = browser.new_page()
|
||||||
|
page.set_content(html, wait_until="networkidle")
|
||||||
|
# wait for fonts to be ready
|
||||||
|
page.evaluate("document.fonts.ready")
|
||||||
|
logger.info("Generating PDF")
|
||||||
|
page.emulate_media(media="print")
|
||||||
|
pdf_bytes = page.pdf(
|
||||||
|
format="Letter",
|
||||||
|
print_background=True,
|
||||||
|
prefer_css_page_size=True,
|
||||||
|
margin={
|
||||||
|
"top": "0.5in",
|
||||||
|
"right": "0.5in",
|
||||||
|
"bottom": "0.5in",
|
||||||
|
"left": "0.5in",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
browser.close()
|
||||||
|
|
||||||
|
logger.info(f"PDF generated successfully, size: {len(pdf_bytes)} bytes")
|
||||||
|
# to start, we have the draft file, so it can be moved later into the
|
||||||
|
# final location by the ReleaseReport.save() process
|
||||||
|
filename = generate_release_report_filename(
|
||||||
|
release_report.report_configuration.get_slug(), published_format=False
|
||||||
|
)
|
||||||
|
release_report.file.save(filename, ContentFile(pdf_bytes), save=True)
|
||||||
|
if publish:
|
||||||
|
release_report.published = True
|
||||||
|
release_report.save(allow_overwrite=True)
|
||||||
|
logger.info(f"{release_report_id=} updated with PDF {filename=}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to generate PDF: {e}", exc_info=True)
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
@app.task
|
@app.task
|
||||||
@@ -252,14 +328,15 @@ def update_library_version_dependencies(token=None):
|
|||||||
|
|
||||||
|
|
||||||
@app.task
|
@app.task
|
||||||
def release_tasks(user_id=None, generate_report=False):
|
def release_tasks(base_uri, user_id=None, generate_report=False):
|
||||||
"""Call the release_tasks management command.
|
"""Call the release_tasks management command.
|
||||||
|
|
||||||
|
@param base_uri should be in the format https://domain.tld
|
||||||
If a user_id is given, that user will receive an email at the beginning
|
If a user_id is given, that user will receive an email at the beginning
|
||||||
and at the end of the task.
|
and at the end of the task.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
command = ["release_tasks"]
|
command = ["release_tasks", "--base_uri", base_uri]
|
||||||
if user_id:
|
if user_id:
|
||||||
command.extend(["--user_id", user_id])
|
command.extend(["--user_id", user_id])
|
||||||
if generate_report:
|
if generate_report:
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ from libraries.utils import (
|
|||||||
conditional_batched,
|
conditional_batched,
|
||||||
decode_content,
|
decode_content,
|
||||||
generate_fake_email,
|
generate_fake_email,
|
||||||
|
generate_release_report_filename,
|
||||||
get_first_last_day_last_month,
|
get_first_last_day_last_month,
|
||||||
parse_date,
|
parse_date,
|
||||||
|
update_base_tag,
|
||||||
version_within_range,
|
version_within_range,
|
||||||
write_content_to_tempfile,
|
write_content_to_tempfile,
|
||||||
)
|
)
|
||||||
@@ -282,3 +284,117 @@ def test_conditional_batched_invalid_n():
|
|||||||
|
|
||||||
with pytest.raises(ValueError, match="n must be at least one"):
|
with pytest.raises(ValueError, match="n must be at least one"):
|
||||||
list(conditional_batched(items, 0, lambda x: True))
|
list(conditional_batched(items, 0, lambda x: True))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"html, base_uri, expected",
|
||||||
|
[
|
||||||
|
# Test basic base tag replacement
|
||||||
|
(
|
||||||
|
'<html><head><base href="/old/path/"></head><body>content</body></html>',
|
||||||
|
"/new/path/",
|
||||||
|
'<html><head><base href="/new/path/"></head><body>content</body></html>',
|
||||||
|
),
|
||||||
|
# Test with different base tag format (no trailing slash)
|
||||||
|
(
|
||||||
|
'<base href="https://example.com/docs">',
|
||||||
|
"https://newsite.com/documentation",
|
||||||
|
'<base href="https://newsite.com/documentation">',
|
||||||
|
),
|
||||||
|
# Test multiple base tags (should replace all occurrences)
|
||||||
|
(
|
||||||
|
'<base href="/old1/"><base href="/old2/">',
|
||||||
|
"/new/",
|
||||||
|
'<base href="/new/"><base href="/new/">',
|
||||||
|
),
|
||||||
|
# Test with empty base URI
|
||||||
|
(
|
||||||
|
'<base href="/docs/">',
|
||||||
|
"",
|
||||||
|
'<base href="">',
|
||||||
|
),
|
||||||
|
# Test with complex HTML structure
|
||||||
|
(
|
||||||
|
"""<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<base href="/doc/libs/1_84_0/">
|
||||||
|
<title>Test</title>
|
||||||
|
</head>
|
||||||
|
<body>content</body>
|
||||||
|
</html>""",
|
||||||
|
"/doc/libs/latest/",
|
||||||
|
"""<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<base href="/doc/libs/latest/">
|
||||||
|
<title>Test</title>
|
||||||
|
</head>
|
||||||
|
<body>content</body>
|
||||||
|
</html>""",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_update_base_tag(html, base_uri, expected):
|
||||||
|
"""Test update_base_tag replaces base tag href correctly."""
|
||||||
|
result = update_base_tag(html, base_uri)
|
||||||
|
assert result == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_base_tag_no_base_tag():
|
||||||
|
"""Test update_base_tag when there is no base tag in the HTML."""
|
||||||
|
html = "<html><head><title>Test</title></head><body>content</body></html>"
|
||||||
|
base_uri = "/new/path/"
|
||||||
|
result = update_base_tag(html, base_uri)
|
||||||
|
# Should return the original HTML unchanged since there's no base tag to replace
|
||||||
|
assert result == html
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"version_slug, published_format, expected_prefix, should_have_timestamp",
|
||||||
|
[
|
||||||
|
# Published format (no timestamp)
|
||||||
|
("boost-1-84-0", True, "release-report-boost-1-84-0.pdf", False),
|
||||||
|
("boost-1-85-0", True, "release-report-boost-1-85-0.pdf", False),
|
||||||
|
# Draft format (with timestamp)
|
||||||
|
("boost-1-84-0", False, "release-report-boost-1-84-0-", True),
|
||||||
|
("boost-1-85-0", False, "release-report-boost-1-85-0-", True),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_generate_release_report_filename(
|
||||||
|
version_slug, published_format, expected_prefix, should_have_timestamp
|
||||||
|
):
|
||||||
|
"""Test generate_release_report_filename generates correct filenames."""
|
||||||
|
result = generate_release_report_filename(version_slug, published_format)
|
||||||
|
assert result.startswith(expected_prefix)
|
||||||
|
assert result.endswith(".pdf")
|
||||||
|
|
||||||
|
if should_have_timestamp:
|
||||||
|
# timestamp should be in ISO format (contains 'T' and timezone info)
|
||||||
|
assert "T" in result
|
||||||
|
# should have the pattern: release-report-{slug}-{timestamp}.pdf
|
||||||
|
assert len(result.split("-")) >= 4 # release, report, slug, timestamp
|
||||||
|
else:
|
||||||
|
# published format should not have a timestamp
|
||||||
|
assert "T" not in result
|
||||||
|
# should be release-report-{slug}.pdf
|
||||||
|
assert result == expected_prefix
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_release_report_filename_timestamp_format():
|
||||||
|
"""Test that the timestamp in the filename is a valid ISO format."""
|
||||||
|
version_slug = "boost-1-84-0"
|
||||||
|
result = generate_release_report_filename(version_slug, published_format=False)
|
||||||
|
|
||||||
|
# extract the timestamp portion (between last dash and .pdf)
|
||||||
|
# format: release-report-boost-1-84-0-{timestamp}.pdf
|
||||||
|
timestamp_part = result.replace("release-report-boost-1-84-0-", "").replace(
|
||||||
|
".pdf", ""
|
||||||
|
)
|
||||||
|
# parse it as an ISO format datetime to ensure it's valid
|
||||||
|
try:
|
||||||
|
datetime.fromisoformat(timestamp_part)
|
||||||
|
except ValueError:
|
||||||
|
pytest.fail(f"Timestamp '{timestamp_part}' is not a valid ISO format")
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from itertools import islice
|
|||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
import tempfile
|
import tempfile
|
||||||
from datetime import datetime
|
from datetime import datetime, timezone
|
||||||
from dateutil.relativedelta import relativedelta
|
from dateutil.relativedelta import relativedelta
|
||||||
|
|
||||||
from dateutil.parser import ParserError, parse
|
from dateutil.parser import ParserError, parse
|
||||||
@@ -353,3 +353,21 @@ def parse_boostdep_artifact(content: str):
|
|||||||
"Some library versions were skipped during artifact parsing.",
|
"Some library versions were skipped during artifact parsing.",
|
||||||
skipped_library_versions=skipped_library_versions,
|
skipped_library_versions=skipped_library_versions,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def update_base_tag(html: str, base_uri: str):
|
||||||
|
"""
|
||||||
|
Replace the base tag href with the new base_uri
|
||||||
|
"""
|
||||||
|
pattern = r'<base\s+href="[^"]*">'
|
||||||
|
replacement = f'<base href="{base_uri}">'
|
||||||
|
return re.sub(pattern, replacement, html)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_release_report_filename(version_slug: str, published_format: bool = False):
|
||||||
|
filename_data = ["release-report", version_slug]
|
||||||
|
if not published_format:
|
||||||
|
filename_data.append(datetime.now(timezone.utc).isoformat())
|
||||||
|
|
||||||
|
filename = f"{"-".join(filename_data)}.pdf"
|
||||||
|
return filename
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
-c requirements.txt
|
-c requirements.txt
|
||||||
django-debug-toolbar
|
django-debug-toolbar
|
||||||
pydevd-pycharm==252.26830.99 # pinned to appropriate version for current pycharm
|
pydevd-pycharm==252.27397.106 # pinned to appropriate version for current pycharm
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ django==5.2.7
|
|||||||
# django-debug-toolbar
|
# django-debug-toolbar
|
||||||
django-debug-toolbar==6.0.0
|
django-debug-toolbar==6.0.0
|
||||||
# via -r ./requirements-dev.in
|
# via -r ./requirements-dev.in
|
||||||
pydevd-pycharm==252.26830.99
|
pydevd-pycharm==252.27397.106
|
||||||
# via -r ./requirements-dev.in
|
# via -r ./requirements-dev.in
|
||||||
sqlparse==0.5.3
|
sqlparse==0.5.3
|
||||||
# via
|
# via
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ wordcloud
|
|||||||
lxml
|
lxml
|
||||||
algoliasearch
|
algoliasearch
|
||||||
openai
|
openai
|
||||||
|
playwright
|
||||||
|
|
||||||
# Logging
|
# Logging
|
||||||
django-tracer
|
django-tracer
|
||||||
|
|||||||
@@ -205,6 +205,7 @@ greenlet==3.2.4
|
|||||||
# via
|
# via
|
||||||
# -r ./requirements.in
|
# -r ./requirements.in
|
||||||
# gevent
|
# gevent
|
||||||
|
# playwright
|
||||||
gunicorn==23.0.0
|
gunicorn==23.0.0
|
||||||
# via -r ./requirements.in
|
# via -r ./requirements.in
|
||||||
h11==0.16.0
|
h11==0.16.0
|
||||||
@@ -310,6 +311,8 @@ platformdirs==4.5.0
|
|||||||
# via
|
# via
|
||||||
# black
|
# black
|
||||||
# virtualenv
|
# virtualenv
|
||||||
|
playwright==1.55.0
|
||||||
|
# via -r ./requirements.in
|
||||||
pluggy==1.6.0
|
pluggy==1.6.0
|
||||||
# via
|
# via
|
||||||
# pytest
|
# pytest
|
||||||
@@ -344,6 +347,8 @@ pydantic==2.12.3
|
|||||||
# openai
|
# openai
|
||||||
pydantic-core==2.41.4
|
pydantic-core==2.41.4
|
||||||
# via pydantic
|
# via pydantic
|
||||||
|
pyee==13.0.0
|
||||||
|
# via playwright
|
||||||
pygments==2.19.2
|
pygments==2.19.2
|
||||||
# via
|
# via
|
||||||
# ipython
|
# ipython
|
||||||
@@ -435,16 +440,14 @@ traitlets==5.14.3
|
|||||||
# matplotlib-inline
|
# matplotlib-inline
|
||||||
typing-extensions==4.15.0
|
typing-extensions==4.15.0
|
||||||
# via
|
# via
|
||||||
# aiosignal
|
|
||||||
# anyio
|
|
||||||
# beautifulsoup4
|
# beautifulsoup4
|
||||||
# django-countries
|
# django-countries
|
||||||
# ipython
|
|
||||||
# jwcrypto
|
# jwcrypto
|
||||||
# minio
|
# minio
|
||||||
# openai
|
# openai
|
||||||
# pydantic
|
# pydantic
|
||||||
# pydantic-core
|
# pydantic-core
|
||||||
|
# pyee
|
||||||
# typing-inspection
|
# typing-inspection
|
||||||
typing-inspection==0.4.2
|
typing-inspection==0.4.2
|
||||||
# via pydantic
|
# via pydantic
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
{% if base_uri %}
|
||||||
|
<base href="{{ base_uri }}">
|
||||||
|
{% endif %}
|
||||||
<title>
|
<title>
|
||||||
{% block title %}Boost{% endblock %}
|
{% block title %}Boost{% endblock %}
|
||||||
</title>
|
</title>
|
||||||
|
|||||||
@@ -474,9 +474,9 @@ ul.slack-channels li div a:hover,
|
|||||||
{% with bg_list=bg_list_str|split:"," %}
|
{% with bg_list=bg_list_str|split:"," %}
|
||||||
{% for batch in batched_library_data %}
|
{% for batch in batched_library_data %}
|
||||||
{% with current_bg=bg_list|get_modulo_item:forloop.counter0 %}
|
{% with current_bg=bg_list|get_modulo_item:forloop.counter0 %}
|
||||||
<div class="pdf-page flex flex-col {{ bg_color }}" id="library-{{item.library.display_name}}" style="background-image: url('{% static current_bg %}')">
|
<div class="pdf-page flex flex-col {{ bg_color }}" style="background-image: url('{% static current_bg %}')">
|
||||||
{% for item in batch %}
|
{% for item in batch %}
|
||||||
<div class="grid grid-cols-3 gap-x-8 w-full p-4 h-1/2">
|
<div class="grid grid-cols-3 gap-x-8 w-full p-4 h-1/2" id="library-{{ item.library.display_name }}">
|
||||||
<div class="col-span-2 flex flex-col gap-y-4">
|
<div class="col-span-2 flex flex-col gap-y-4">
|
||||||
<div class="flex flex-col gap-y-4">
|
<div class="flex flex-col gap-y-4">
|
||||||
<h2 class="text-orange mb-1 mt-0">{{ item.library.display_name }}</h2>
|
<h2 class="text-orange mb-1 mt-0">{{ item.library.display_name }}</h2>
|
||||||
|
|||||||
@@ -44,7 +44,11 @@ class VersionAdmin(admin.ModelAdmin):
|
|||||||
return my_urls + urls
|
return my_urls + urls
|
||||||
|
|
||||||
def release_tasks(self, request):
|
def release_tasks(self, request):
|
||||||
release_tasks.delay(user_id=request.user.id, generate_report=True)
|
release_tasks.delay(
|
||||||
|
base_uri=f"https://{request.get_host()}",
|
||||||
|
user_id=request.user.id,
|
||||||
|
generate_report=True,
|
||||||
|
)
|
||||||
self.message_user(
|
self.message_user(
|
||||||
request,
|
request,
|
||||||
"release_tasks has started, you will receive an email when the task finishes.", # noqa: E501
|
"release_tasks has started, you will receive an email when the task finishes.", # noqa: E501
|
||||||
|
|||||||
@@ -322,6 +322,11 @@ class ReportConfiguration(models.Model):
|
|||||||
def display_name(self):
|
def display_name(self):
|
||||||
return self.version.replace("boost-", "")
|
return self.version.replace("boost-", "")
|
||||||
|
|
||||||
|
def get_slug(self):
|
||||||
|
# this output should always match the Version slug format
|
||||||
|
name = self.version.replace(".", " ").replace("boost_", "")
|
||||||
|
return slugify(name)[:50]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.version
|
return self.version
|
||||||
|
|
||||||
|
|||||||
@@ -129,3 +129,39 @@ def test_review_results():
|
|||||||
|
|
||||||
pending_result.refresh_from_db()
|
pending_result.refresh_from_db()
|
||||||
assert not pending_result.is_most_recent
|
assert not pending_result.is_most_recent
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"version_name",
|
||||||
|
[
|
||||||
|
"boost-1.75.0",
|
||||||
|
"boost-1.81.0",
|
||||||
|
"boost-1.82.0.beta1",
|
||||||
|
"boost_1.75.0",
|
||||||
|
"boost_1_75_0",
|
||||||
|
"develop",
|
||||||
|
"master",
|
||||||
|
"1.75.0",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_report_configuration_slug_matches_version_slug_format(version_name):
|
||||||
|
"""
|
||||||
|
Test that ReportConfiguration.get_slug() produces the same format as
|
||||||
|
Version.get_slug() for the same version name.
|
||||||
|
|
||||||
|
This ensures consistency between the two models' slug generation.
|
||||||
|
"""
|
||||||
|
# Create a Version with the version name
|
||||||
|
version = baker.prepare("versions.Version", name=version_name, slug=None)
|
||||||
|
version_slug = version.get_slug()
|
||||||
|
|
||||||
|
# Create a ReportConfiguration with the same version name
|
||||||
|
report_config = baker.prepare("versions.ReportConfiguration", version=version_name)
|
||||||
|
report_config_slug = report_config.get_slug()
|
||||||
|
|
||||||
|
# Assert that both slugs match
|
||||||
|
assert version_slug == report_config_slug, (
|
||||||
|
f"Slug mismatch for version name '{version_name}': "
|
||||||
|
f"Version.get_slug() = '{version_slug}', "
|
||||||
|
f"ReportConfiguration.get_slug() = '{report_config_slug}'"
|
||||||
|
)
|
||||||
|
|||||||
@@ -221,6 +221,10 @@ class ReportPreviewGenerateView(BoostVersionMixin, View):
|
|||||||
version_name = version.name
|
version_name = version.name
|
||||||
cache_key = f"release-report-,,,,,,,-{version_name}"
|
cache_key = f"release-report-,,,,,,,-{version_name}"
|
||||||
RenderedContent.objects.filter(cache_key=cache_key).delete()
|
RenderedContent.objects.filter(cache_key=cache_key).delete()
|
||||||
generate_release_report.delay({"version": version.id})
|
generate_release_report.delay(
|
||||||
|
user_id=request.user.id,
|
||||||
|
params={"version": version.id},
|
||||||
|
base_uri=f"https://{request.get_host()}",
|
||||||
|
)
|
||||||
messages.success(request, "Report generation queued.")
|
messages.success(request, "Report generation queued.")
|
||||||
return redirect("release-report-preview", version_slug=version_name)
|
return redirect("release-report-preview", version_slug=version_name)
|
||||||
|
|||||||
Reference in New Issue
Block a user