Skip to content

Commit

Permalink
Merge pull request #34042 from openedx/dkaplan1/APER-2851_replicate-s…
Browse files Browse the repository at this point in the history
…hare-certificate-in-facebook-improvements-for-edx.org

feat: adds a certificate template modifier
  • Loading branch information
deborahgu authored Jan 22, 2024
2 parents 2ae9155 + b4bf076 commit 611e3cc
Show file tree
Hide file tree
Showing 8 changed files with 446 additions and 47 deletions.
8 changes: 7 additions & 1 deletion lms/djangoapps/certificates/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
CertificateHtmlViewConfiguration,
CertificateTemplate,
CertificateTemplateAsset,
GeneratedCertificate
GeneratedCertificate,
ModifiedCertificateTemplateCommandConfiguration,
)


Expand Down Expand Up @@ -92,6 +93,11 @@ class CertificateGenerationCourseSettingAdmin(admin.ModelAdmin):
show_full_result_count = False


@admin.register(ModifiedCertificateTemplateCommandConfiguration)
class ModifiedCertificateTemplateCommandConfigurationAdmin(ConfigurationModelAdmin):
pass


@admin.register(CertificateGenerationCommandConfiguration)
class CertificateGenerationCommandConfigurationAdmin(ConfigurationModelAdmin):
pass
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
"""Management command to modify certificate templates."""
import logging
import shlex
from argparse import RawDescriptionHelpFormatter

from django.core.management.base import BaseCommand, CommandError

from lms.djangoapps.certificates.models import (
ModifiedCertificateTemplateCommandConfiguration,
)
from lms.djangoapps.certificates.tasks import handle_modify_cert_template

log = logging.getLogger(__name__)


class Command(BaseCommand):
"""Management command to modify certificate templates.
Example usage:
./manage.py lms modify_cert_template --old-text "</head>" --new text "<p>boo!</p></head>" --templates 867 3509
"""

help = """Modify one or more certificate templates.
This is DANGEROUS.
* This uses string replacement to modify HTML-like templates, because the presence of
Django Templating makes it impossible to do true parsing.
* This isn't parameterizing the replacement text, for the same reason. It has
no way of knowing what is template language and what is HTML.
Do not trust that this will get the conversion right without verification,
and absolutely do not accepted untrusted user input for the replacement text. This is
to be run by trusted users only.
Always run this with dry-run or in a reliable test environment.
"""

def add_arguments(self, parser):
parser.formatter_class = RawDescriptionHelpFormatter
parser.add_argument(
"--dry-run",
action="store_true",
help="Just show a preview of what would happen.",
)
parser.add_argument(
"--old-text",
help="Text to replace in the template.",
)
parser.add_argument(
"--new-text",
help="Replacement text for the template.",
)
parser.add_argument(
"--templates",
nargs="+",
help="Certificate templates to modify.",
)
parser.add_argument(
"--args-from-database",
action="store_true",
help="Use arguments from the ModifyCertificateTemplateConfiguration model instead of the command line.",
)

def get_args_from_database(self):
"""
Returns an options dictionary from the current ModifiedCertificateTemplateCommandConfiguration instance.
"""
config = ModifiedCertificateTemplateCommandConfiguration.current()
if not config.enabled:
raise CommandError(
"ModifyCertificateTemplateCommandConfiguration is disabled, but --args-from-database was requested"
)
args = shlex.split(config.arguments)
parser = self.create_parser("manage.py", "modify_cert_template")
return vars(parser.parse_args(args))

def handle(self, *args, **options):
# database args will override cmd line args
if options["args_from_database"]:
options = self.get_args_from_database()
# Check required arguments here. We can't rely on marking args "required" because they might come from django
if not (options["old_text"] and options["new_text"] and options["templates"]):
raise CommandError(
"The following arguments are required: --old-text, --new-text, --templates"
)
log.info(
"modify_cert_template starting, dry-run={dry_run}, templates={templates}, "
"old-text={old}, new-text={new}".format(
dry_run=options["dry_run"],
templates=options["templates"],
old=options["old_text"],
new=options["new_text"],
)
)
handle_modify_cert_template.delay(options)
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"""
Tests for the modify_cert_template command
"""

import pytest
from django.core.management import CommandError, call_command
from django.test import TestCase


class ModifyCertTemplateTests(TestCase):
"""Tests for the modify_cert_template management command"""

def test_command_with_missing_param_old_text(self):
"""Verify command with a missing param --old-text."""
with pytest.raises(
CommandError,
match="The following arguments are required: --old-text, --new-text, --templates",
):
call_command(
"modify_cert_template", "--new-text", "blah", "--templates", "1 2 3"
)

def test_command_with_missing_param_new_text(self):
"""Verify command with a missing param --new-text."""
with pytest.raises(
CommandError,
match="The following arguments are required: --old-text, --new-text, --templates",
):
call_command(
"modify_cert_template", "--old-text", "blah", "--templates", "1 2 3"
)

def test_command_with_missing_param_templates(self):
"""Verify command with a missing param --templates."""
with pytest.raises(
CommandError,
match="The following arguments are required: --old-text, --new-text, --templates",
):
call_command(
"modify_cert_template", "--new-text", "blah", "--old-text", "xyzzy"
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Generated by Django 3.2.23 on 2024-01-16 18:57

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('certificates', '0035_auto_20230808_0944'),
]

operations = [
migrations.CreateModel(
name='ModifiedCertificateTemplateCommandConfiguration',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('change_date', models.DateTimeField(auto_now_add=True, verbose_name='Change date')),
('enabled', models.BooleanField(default=False, verbose_name='Enabled')),
('arguments', models.TextField(blank=True, default='', help_text='Arguments for the \'modify_cert_template\' management command. Specify like \'--old-text "foo" --new-text "bar" --template_ids <id1> <id2>\'')),
('changed_by', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL, verbose_name='Changed by')),
],
options={
'verbose_name': 'modify_cert_template argument',
},
),
]
27 changes: 25 additions & 2 deletions lms/djangoapps/certificates/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
Course certificates are created for a student and an offering of a course (a course run).
"""

from datetime import timezone
import json
import logging
import os
import uuid
from datetime import timezone

from config_models.models import ConfigurationModel
from django.apps import apps
Expand All @@ -16,7 +16,6 @@
from django.db import models, transaction
from django.db.models import Count
from django.dispatch import receiver

from django.utils.translation import gettext_lazy as _
from model_utils import Choices
from model_utils.models import TimeStampedModel
Expand Down Expand Up @@ -1243,6 +1242,30 @@ class Meta:
app_label = "certificates"


class ModifiedCertificateTemplateCommandConfiguration(ConfigurationModel):
"""
Manages configuration for a run of the modify_cert_template management command.
.. no_pii:
"""

class Meta:
app_label = "certificates"
verbose_name = "modify_cert_template argument"

arguments = models.TextField(
blank=True,
help_text=(
"Arguments for the 'modify_cert_template' management command. Specify like '--old-text \"foo\" "
"--new-text \"bar\" --template_ids <id1> <id2>'"
),
default="",
)

def __str__(self):
return str(self.arguments)


class CertificateGenerationCommandConfiguration(ConfigurationModel):
"""
Manages configuration for a run of the cert_generation management command.
Expand Down
Loading

0 comments on commit 611e3cc

Please sign in to comment.