|
1 | 1 | """Utility functions/classes for course management commands"""
|
2 | 2 |
|
| 3 | +from dataclasses import dataclass |
| 4 | +from typing import Optional |
| 5 | + |
3 | 6 | from django.core.management.base import BaseCommand, CommandError
|
4 | 7 |
|
5 | 8 | from courses.models import CourseRun, CourseRunEnrollment, Program, ProgramEnrollment
|
@@ -243,3 +246,144 @@ def enroll_in_edx(self, user, course_runs):
|
243 | 246 | ) as exc:
|
244 | 247 | self.stdout.write(self.style.WARNING(str(exc)))
|
245 | 248 | return False
|
| 249 | + |
| 250 | + |
| 251 | +@dataclass |
| 252 | +class CourseInfo: |
| 253 | + """ |
| 254 | + Data class for course information with named fields |
| 255 | + """ |
| 256 | + |
| 257 | + code: str |
| 258 | + title: Optional[str] = None |
| 259 | + msg: Optional[str] = None |
| 260 | + |
| 261 | + |
| 262 | +class StatCategory: |
| 263 | + """Represents a category of statistics with its display information""" |
| 264 | + |
| 265 | + def __init__(self, key, display_name=None, label=None): |
| 266 | + """ |
| 267 | + Initialize a stat category |
| 268 | +
|
| 269 | + Args: |
| 270 | + key: The dictionary key for this stat |
| 271 | + display_name: Human-readable category name |
| 272 | + label: Description of the items |
| 273 | + """ |
| 274 | + self.key = key |
| 275 | + self.display_name = display_name or self._generate_display_name(key) |
| 276 | + self.label = label or self._generate_label(key) |
| 277 | + self.items = [] |
| 278 | + |
| 279 | + def _generate_display_name(self, key): |
| 280 | + """Generate a display name from the key""" |
| 281 | + return " ".join(word.capitalize() for word in key.split("_")) |
| 282 | + |
| 283 | + def _generate_label(self, key): |
| 284 | + """Generate a label based on the key""" |
| 285 | + if "course" in key and "run" not in key: |
| 286 | + return "External Course Codes" |
| 287 | + elif "run" in key: |
| 288 | + return "External Course Run Codes" |
| 289 | + elif "product" in key: |
| 290 | + return "Course Run courseware_ids" |
| 291 | + elif "certificate" in key: |
| 292 | + return "Course Readable IDs" |
| 293 | + else: |
| 294 | + return "Items" |
| 295 | + |
| 296 | + def add(self, code, title=None, msg=None): |
| 297 | + """Add an item to this stat category""" |
| 298 | + self.items.append(CourseInfo(code=code, title=title, msg=msg)) |
| 299 | + |
| 300 | + def get_codes(self): |
| 301 | + """Get the set of unique codes in this category""" |
| 302 | + return {item.code for item in self.items if item.code is not None} |
| 303 | + |
| 304 | + def __len__(self): |
| 305 | + """Return the number of items in this category""" |
| 306 | + return len(self.items) |
| 307 | + |
| 308 | + |
| 309 | +class StatsCollector: |
| 310 | + """Collector for external course sync statistics with named properties""" |
| 311 | + |
| 312 | + def __init__(self): |
| 313 | + self.categories = { |
| 314 | + "courses_created": StatCategory("courses_created"), |
| 315 | + "existing_courses": StatCategory("existing_courses"), |
| 316 | + "course_runs_created": StatCategory("course_runs_created"), |
| 317 | + "course_runs_updated": StatCategory("course_runs_updated"), |
| 318 | + "course_runs_without_prices": StatCategory("course_runs_without_prices"), |
| 319 | + "course_runs_skipped": StatCategory( |
| 320 | + "course_runs_skipped", |
| 321 | + display_name="Course Runs Skipped due to bad data", |
| 322 | + ), |
| 323 | + "course_runs_expired": StatCategory( |
| 324 | + "course_runs_deactivated", display_name="Expired Course Runs" |
| 325 | + ), |
| 326 | + "course_runs_deactivated": StatCategory("course_runs_deactivated"), |
| 327 | + "course_pages_created": StatCategory("course_pages_created"), |
| 328 | + "course_pages_updated": StatCategory("course_pages_updated"), |
| 329 | + "products_created": StatCategory("products_created"), |
| 330 | + "product_versions_created": StatCategory("product_versions_created"), |
| 331 | + "certificates_created": StatCategory( |
| 332 | + "certificates_created", display_name="Certificate Pages Created" |
| 333 | + ), |
| 334 | + "certificates_updated": StatCategory( |
| 335 | + "certificates_updated", display_name="Certificate Pages Updated" |
| 336 | + ), |
| 337 | + } |
| 338 | + |
| 339 | + def add_stat(self, key, code, title=None, msg=None): |
| 340 | + """ |
| 341 | + Add an item to a specific stat category |
| 342 | + """ |
| 343 | + if key in self.categories: |
| 344 | + self.categories[key].add(code, title, msg) |
| 345 | + |
| 346 | + def add_bulk(self, key, codes): |
| 347 | + """ |
| 348 | + Add multiple items within the same category |
| 349 | + """ |
| 350 | + if key in self.categories: |
| 351 | + for code in codes: |
| 352 | + self.add_stat(key, code) |
| 353 | + |
| 354 | + def remove_duplicates(self, source_key, items_to_remove_key): |
| 355 | + """ |
| 356 | + Remove items from one category that exist in another category |
| 357 | + """ |
| 358 | + if ( |
| 359 | + source_key not in self.categories |
| 360 | + or items_to_remove_key not in self.categories |
| 361 | + ): |
| 362 | + return |
| 363 | + |
| 364 | + codes_to_remove = { |
| 365 | + item.code for item in self.categories[items_to_remove_key].items |
| 366 | + } |
| 367 | + |
| 368 | + self.categories[source_key].items = [ |
| 369 | + item |
| 370 | + for item in self.categories[source_key].items |
| 371 | + if item.code not in codes_to_remove |
| 372 | + ] |
| 373 | + |
| 374 | + def log_stats(self, logger): |
| 375 | + """ |
| 376 | + Log all collected statistics |
| 377 | + """ |
| 378 | + for category in self.categories.values(): |
| 379 | + codes = category.get_codes() |
| 380 | + logger.log_style_success( |
| 381 | + f"Number of {category.display_name}: {len(codes)}." |
| 382 | + ) |
| 383 | + logger.log_style_success(f"{category.label}: {codes or 0}\n") |
| 384 | + |
| 385 | + def email_stats(self): |
| 386 | + """ |
| 387 | + Return statistics formatted for email template" |
| 388 | + """ |
| 389 | + return {key: category.items for key, category in self.categories.items()} |
0 commit comments