Skip to content

Commit

Permalink
Merge pull request #98 from mitodl/jkachel/add-variegated-viewset-sys…
Browse files Browse the repository at this point in the history
…tem-slug

Add system slug field, add selectable view set
  • Loading branch information
jkachel authored Apr 3, 2024
2 parents 5c2a9b6 + 2448811 commit cdbf771
Show file tree
Hide file tree
Showing 12 changed files with 1,065 additions and 91 deletions.
56 changes: 16 additions & 40 deletions openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -172,12 +172,20 @@ paths:
description: Number of results to return per page.
schema:
type: integer
- in: query
name: name
schema:
type: string
- name: offset
required: false
in: query
description: The initial index from which to return the results.
schema:
type: integer
- in: query
name: system__slug
schema:
type: string
tags:
- product
security:
Expand Down Expand Up @@ -328,36 +336,18 @@ components:
id:
type: integer
readOnly: true
deleted_on:
type: string
format: date-time
readOnly: true
nullable: true
deleted_by_cascade:
type: boolean
readOnly: true
created_on:
type: string
format: date-time
readOnly: true
updated_on:
type: string
format: date-time
readOnly: true
name:
type: string
maxLength: 255
description:
slug:
type: string
api_key:
nullable: true
maxLength: 80
description:
type: string
required:
- created_on
- deleted_by_cascade
- deleted_on
- id
- name
- updated_on
PaginatedIntegratedSystemList:
type: object
properties:
Expand Down Expand Up @@ -405,28 +395,14 @@ components:
id:
type: integer
readOnly: true
deleted_on:
type: string
format: date-time
readOnly: true
nullable: true
deleted_by_cascade:
type: boolean
readOnly: true
created_on:
type: string
format: date-time
readOnly: true
updated_on:
type: string
format: date-time
readOnly: true
name:
type: string
maxLength: 255
description:
slug:
type: string
api_key:
nullable: true
maxLength: 80
description:
type: string
PatchedProduct:
type: object
Expand Down
234 changes: 234 additions & 0 deletions system_meta/management/commands/generate_test_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
"""
Adds some test data to the system. This includes three IntegratedSystems with three
products each.
Ignoring A003 because "help" is valid for argparse.
Ignoring S311 because it's complaining about the faker package.
"""
# ruff: noqa: A003, S311

import random
import uuid
from decimal import Decimal

import faker
from django.core.management import BaseCommand
from django.core.management.base import CommandParser
from django.db import transaction

from system_meta.models import IntegratedSystem, Product


def get_input(text):
"""Wrap the internal input function so we can test it later."""

return input(text)


def fake_courseware_id(courseware_type: str, **kwargs) -> str:
"""
Generate a fake courseware id.
Courseware IDs generally are in the format:
<type>-v1:<school ID>+<courseware ID>(+<run tag>)
Type is either "course" or "program", depending on what you specify. School ID is
one of "MITx", "MITxT", "edX", "xPRO", or "Sample". Courseware ID is a set of
numbers: a number < 100, a number < 1000 with a leading zero, and an optional
number < 10, separated by periods. Courseware ID is followed by an "x". This
should be pretty like the IDs that are on MITx Online now (but pretty unlike the
xPRO ones, which usually use a text courseware ID, but that's fine since these
are fake).
Arguments:
- courseware_type (str): "course" or "program"; the type of
courseware id to generate.
Keyword Arguments:
- include_run_tag (bool): include the run tag. Defaults to False.
Returns:
- str: The generated courseware id, in the normal format.
"""
fake = faker.Faker()

school_id = random.choice(["MITx", "MITxT", "edX", "xPRO", "Sample"])
courseware_id = f"{random.randint(0, 99)}.{random.randint(0, 999):03d}"
courseware_type = courseware_type.lower()
optional_third_digit = random.randint(0, 9) if fake.boolean() else ""
optional_run_tag = (
f"+{random.randint(1,3)}T{fake.date_this_decade().year}"
if kwargs.get("include_run_tag", False)
else ""
)

return (
f"{courseware_type}-v1:{school_id}+{courseware_id}"
f"{optional_third_digit}x{optional_run_tag}"
)


class Command(BaseCommand):
"""Adds some test data to the system."""

def add_arguments(self, parser: CommandParser) -> None:
"""Add arguments to the command parser."""
parser.add_argument(
"--remove",
action="store_true",
help="Remove the test data. This is potentially dangerous.",
)

parser.add_argument(
"--only-systems",
action="store_true",
help="Only add test systems. Will add two active and one inactive system.",
)

parser.add_argument(
"--only-products",
action="store_true",
help="Only add test products.",
)

parser.add_argument(
"--system-slug",
type=str,
help=(
"The slug of the system to add products to."
" Only used with --only-products."
),
nargs="?",
)

def add_test_systems(self) -> None:
"""Add the test systems."""
max_systems = 3
for i in range(1, max_systems + 1):
system = IntegratedSystem.objects.create(
name=f"Test System {i}",
description=f"Test System {i} description.",
api_key=uuid.uuid4(),
)
self.stdout.write(f"Created system {system.name} - {system.slug}")

def add_test_products(self, system_slug: str) -> None:
"""Add the test products to the specified system."""
self.stdout.write(f"Creating test products for {system_slug}")

if not IntegratedSystem.objects.filter(slug=system_slug).exists():
self.stdout.write(
self.style.ERROR(f"Integrated system {system_slug} does not exist.")
)
return

system = IntegratedSystem.objects.get(slug=system_slug)

max_products = 3
for i in range(1, max_products + 1):
product_sku = fake_courseware_id("course", include_run_tag=True)
product = Product.objects.create(
name=f"Test Product {i}",
description=f"Test Product {i} description.",
sku=product_sku,
system=system,
price=Decimal(random.random() * 10000).quantize(Decimal("0.01")),
system_data={
"courserun": product_sku,
"program": fake_courseware_id("program"),
},
)
self.stdout.write(f"Created product {product.id} - {product.sku}")

def remove_test_data(self) -> None:
"""Remove the test data."""

test_systems = (
IntegratedSystem.all_objects.prefetch_related("products")
.filter(name__startswith="Test System")
.all()
)

self.stdout.write(
self.style.WARNING("This command will remove these systems and products:")
)

for system in test_systems:
self.stdout.write(
self.style.WARNING(f"System: {system.name} ({system.id})")
)

for product in system.products.all():
self.stdout.write(
self.style.WARNING(f"\tProduct: {product.name} ({product.id})")
)

self.stdout.write(
self.style.WARNING(
"This will ACTUALLY DELETE these records."
" Are you sure you want to do this?"
)
)

if get_input("Type 'yes' to continue: ") != "yes":
self.stdout.write(self.style.ERROR("Aborting."))
return

for system in test_systems:
Product.all_objects.filter(
pk__in=[product.id for product in system.products.all()]
).delete()
IntegratedSystem.all_objects.filter(pk=system.id).delete()

self.stdout.write(self.style.SUCCESS("Test data removed."))

def handle(self, *args, **options) -> None: # noqa: ARG002
"""Handle the command."""
remove = options["remove"]
only_systems = options["only_systems"]
only_products = options["only_products"]
systems = []

with transaction.atomic():
if remove:
self.remove_test_data()
return

if not only_products:
self.add_test_systems()
systems = [
system.slug
for system in (
IntegratedSystem.all_objects.filter(
name__startswith="Test System"
).all()
)
]

if only_systems:
IntegratedSystem.objects.filter(
name__startswith="Test System"
).last().delete()
return

if only_products:
if not options["system_slug"] or len(options["system_slug"]) == 0:
self.stdout.write(
self.style.ERROR(
"You must specify a system when using --only-products."
)
)
return
else:
systems = [options["system_slug"]]

self.stdout.write(f"we are creating products now {systems}")

[self.add_test_products(system) for system in systems]

if not only_products:
IntegratedSystem.objects.filter(
name__startswith="Test System"
).last().delete()

return
Loading

0 comments on commit cdbf771

Please sign in to comment.