diff --git a/src/core/management/commands/appversion.py b/src/core/management/commands/appversion.py index c44b38ed9f0c4bd2b1c223887e242346db47a09d..588e704dc3412d29623c999b445b6bb8193aa58a 100644 --- a/src/core/management/commands/appversion.py +++ b/src/core/management/commands/appversion.py @@ -1,19 +1,19 @@ from django.conf import settings -from django.core.management.base import BaseCommand +from django_rich.management import RichCommand -class Command(BaseCommand): +class Command(RichCommand): def handle(self, *args, **options): version_data = settings.APP_VERSION_INFO if version_data.get('tag') == 'DEV': - self.stdout.write('DEVELOPMENT VERSION', self.style.WARNING) + self.console.print('DEVELOPMENT VERSION', style='yellow bold') return if version_data.get('ci') is True: - self.stdout.write('CI BUILD', self.style.SUCCESS) + self.console.print('CI BUILD', style='green bold') for k, v in version_data.items(): if v is None or v == '' or k in ['ci']: continue - self.stdout.write(self.style.MIGRATE_HEADING(str(k)) + ': ' + str(v)) + self.console.print(f'[cyan bold]{k:s}[/cyan bold]: {v:s}') diff --git a/src/core/management/commands/create_conference.py b/src/core/management/commands/create_conference.py index ad5f4069a517a105c2e88f97e11727094d23ed54..7023a6969c9f5a953e665434e46b43e52b389616 100644 --- a/src/core/management/commands/create_conference.py +++ b/src/core/management/commands/create_conference.py @@ -1,12 +1,14 @@ from argparse import ArgumentTypeError, BooleanOptionalAction from datetime import datetime +from rich.console import Console from zoneinfo import ZoneInfo from django.contrib.auth.models import Group from django.core.management import call_command -from django.core.management.base import BaseCommand, CommandError +from django.core.management.base import CommandError from django.utils.text import slugify +from django_rich.management import RichCommand from core.models.assemblies import Assembly from core.models.badges import BadgeCategory @@ -78,7 +80,7 @@ INTRO_PAGES = { } -def seed_conference(conf: Conference, year: int = 0): +def seed_conference(conf: Conference, console: Console, year: int = 0): if Assembly.objects.filter(conference=conf).exists(): raise ValueError('Conference already has assemblies?!') @@ -374,7 +376,7 @@ def _validate_date(s: str) -> datetime: raise ArgumentTypeError(f"Not a valid date: {s!r}. Use format 'YYYY'") from exc -class Command(BaseCommand): +class Command(RichCommand): def add_arguments(self, parser): parser.add_argument( 'name', @@ -430,7 +432,10 @@ class Command(BaseCommand): elif not (year := options.get('year')): year = datetime.now() # noqa: DTZ005 year = year.year - print(f'Creating conference {slug} ({name}) for year {year}') + + self.console.print( + f'Creating conference [bold blue]{slug}[/bold blue] (name: [bold blue]{name}[/bold blue]){ f' for year {year}' if year != 0 else ""}' + ) # check if the conference already exists if Conference.objects.filter(slug=slug).exists(): @@ -443,35 +448,39 @@ class Command(BaseCommand): bootstrap = answer.lower() == 'y' if bootstrap: - self.stdout.write(self.style.WARNING('Groups will be loaded and may be duplicated!')) + self.console.print('Groups will be loaded and may be duplicated!', style='yellow bold') else: - self.stdout.write(self.style.WARNING('Bootstrap will not be applied.')) + self.console.print('Bootstrap will not be applied.', style='yellow bold') # load our bootstrap fixtures if bootstrap or (bootstrap is None and not Group.objects.exists()): - self.stdout.write('Loading bootstrap data...') - call_command('loaddata', 'bootstrap_auth_groups.json') - self.stdout.write(self.style.SUCCESS('Bootstrap data loaded.')) + with self.console.status('Bootstrapping groups..') as status: + call_command('loaddata', 'bootstrap_auth_groups.json') + self.console.print('Creating conference... [green]done[/green]', style='green bold') elif bootstrap is None and Group.objects.exists(): - self.stdout.write(self.style.SUCCESS('Groups are already present in the database.')) + self.console.print('Groups are already present in the database.', style='green bold') - # create the new conference - conf = Conference.objects.create( - slug=slug, - name=name, - is_public=True, - ) + with self.console.status('Creating conference...') as status: + # create the new conference + conf = Conference.objects.create( + slug=slug, + name=name, + is_public=True, + ) - # for CCC Jahresendveranstaltung we can guess some timestamps from the current year alone - if year > 1970: - conf.start = datetime(year, 12, 27, 10, 0, 0, tzinfo=ZoneInfo('Europe/Berlin')) - conf.end = datetime(year, 12, 30, 18, 0, 0, tzinfo=ZoneInfo('Europe/Berlin')) - conf.registration_deadline = datetime(year, 12, 1, 0, 0, 0, tzinfo=ZoneInfo('Europe/Berlin')) + # for CCC Jahresendveranstaltung we can guess some timestamps from the current year alone + if year > 1970: + status.update('Setting deadline...') + conf.start = datetime(year, 12, 27, 10, 0, 0, tzinfo=ZoneInfo('Europe/Berlin')) + conf.end = datetime(year, 12, 30, 18, 0, 0, tzinfo=ZoneInfo('Europe/Berlin')) + conf.registration_deadline = datetime(year, 12, 1, 0, 0, 0, tzinfo=ZoneInfo('Europe/Berlin')) + self.console.print('Setting deadline... [green]done[/green]', style='bold') - # save everything - conf.save() - self.stdout.write(self.style.SUCCESS(f'Created conference {conf.pk}')) + # save everything + conf.save() + self.console.print('Creating conference... [green]done[/green]', style='bold') # fill in initial data - seed_conference(conf, year=year) - self.stdout.write(self.style.SUCCESS(f'Initial data set up for "{conf.slug}"')) + with self.console.status('Setting up initial data for [blue bold]{conf.slug}[/blue bold]...') as status: + seed_conference(conf, console=self.console, year=year) + self.console.print(f'Setting up initial data for [blue bold]{conf.slug}[/blue bold]... [green]done[/green]', style='bold') diff --git a/src/core/management/commands/housekeeping.py b/src/core/management/commands/housekeeping.py index a81d4bf19ab3f08fb83fd20a2218902be2e03670..80ce7091fcf2fdd537a88711095b42b8805ce8d0 100644 --- a/src/core/management/commands/housekeeping.py +++ b/src/core/management/commands/housekeeping.py @@ -1,8 +1,10 @@ +import sys import time -from django.core.management.base import BaseCommand, CommandError +from django.core.management.base import CommandError from django.db.models import Max from django.utils import timezone +from django_rich.management import RichCommand from core.models.conference import Conference, ConferenceExportCache from core.models.messages import DirectMessage @@ -11,7 +13,11 @@ from core.models.schedules import ScheduleSource, ScheduleSourceImport from core.models.voucher import Voucher -class Command(BaseCommand): +def _no_traceback_exception_hook(exc_type, exc_val, traceback): + """Hook to prevent traceback on KeyboardInterrupt.""" + + +class Command(RichCommand): def add_arguments(self, parser): parser.add_argument('--forever', action='store_true', help='repeat the housekeeping forever (until Ctrl+C is pressed)') parser.add_argument('--forever-delay', type=int, default=300, help='seconds to wait between housekeeping runs') @@ -20,19 +26,19 @@ class Command(BaseCommand): def _housekeeping_directmessages(self): # clear all direct messages which are after their expiry date - print('Deleting messages ... ', end='', flush=True) + self.console.print('Deleting messages ... ', end='', style='red') deleted_msgs_count, _ = DirectMessage.objects.filter(autodelete_after__isnull=False, autodelete_after__lte=timezone.now()).delete() - print(deleted_msgs_count) + self.console.print(deleted_msgs_count) def _housekeeping_vouchers(self): # do auto-assignments - print('Auto-assigning vouchers ... ', end='', flush=True) + self.console.print('Auto-assigning vouchers ... ', end='', style='green') vouchers_assigned = Voucher.do_auto_assignments() - print(vouchers_assigned) + self.console.print(vouchers_assigned) def _housekeeping_scheduleimports(self): # schedules - print('Schedule imports ... ', end='', flush=True) + self.console.print('Schedule imports ... ', end='', style='blue') schedule_results = {} schedule_failure, schedule_success, schedule_skipped = 0, 0, 0 for schedule in ScheduleSource.objects.all(): @@ -63,7 +69,7 @@ class Command(BaseCommand): schedule_failure += 1 # print schedule import results - print(schedule_success, '/', len(schedule_results), ' (', schedule_skipped, ' skipped)', sep='') + self.console.print(f'{schedule_success}/{len(schedule_results)} ({schedule_skipped} skipped)', style='cyan bold') for k, v in schedule_results.items(): print(' ', k, ' => ', v, sep='') @@ -94,12 +100,12 @@ class Command(BaseCommand): entry.save(update_fields=['needs_regeneration']) changed += 1 - print('Flagged', changed, 'out of', total, 'cached exports for regeneration.') + self.console.print(f'Flagged {changed} out of {total} cached exports for regeneration.', style='yellow') def _housekeeping_wikimports(self): for c in Conference.objects.all(): for ns in StaticPageNamespace.objects.filter(conference=c).exclude(upstream_url__isnull=True).all(): - print(f'Importing "{c.slug}" wiki namespace "{ns.prefix}" ...') + self.console.print(f'Importing "{c.slug}" wiki namespace "{ns.prefix}" ...', style='blue') ns.fetch_upstream() def handle(self, *args, **options): @@ -109,27 +115,29 @@ class Command(BaseCommand): if forever: if forever_delay <= 0: raise CommandError('The --forever-delay value must a positive value (in seconds).') - print(f'Running housekeeping forever each {forever_delay}s:', end='\n\n') - - while True: - if forever: - print(timezone.now().isoformat()) - - # call the individual housekeeping methods - self._housekeeping_directmessages() - self._housekeeping_vouchers() - if not options.get('skip_schedule_imports'): - self._housekeeping_scheduleimports() - self._housekeeping_exportcache() - if not options.get('skip_wiki_imports'): - self._housekeeping_wikimports() - - if forever: - print() # empty line - try: + self.console.print(f'Running housekeeping forever each {forever_delay}s:', end='\n\n') + + try: + while True: + if forever: + self.console.print(timezone.now().isoformat(), style='orange1', highlight=False) + + # call the individual housekeeping methods + self._housekeeping_directmessages() + self._housekeeping_vouchers() + if not options.get('skip_schedule_imports'): + self._housekeeping_scheduleimports() + self._housekeeping_exportcache() + if not options.get('skip_wiki_imports'): + self._housekeeping_wikimports() + + if forever: + print() # empty line time.sleep(forever_delay) - except KeyboardInterrupt: - print('Aborted.') + else: break - else: - break + except KeyboardInterrupt: + if sys.excepthook is sys.__excepthook__: + sys.excepthook = _no_traceback_exception_hook + print('Aborted.') + raise diff --git a/src/core/management/commands/import_mapservice_resultfile.py b/src/core/management/commands/import_mapservice_resultfile.py index 241054f0e1a95a9ea7bd1a9530f97de983d18ca2..41d1953b27f3fd18c5fdf7e57b2920879f075218 100644 --- a/src/core/management/commands/import_mapservice_resultfile.py +++ b/src/core/management/commands/import_mapservice_resultfile.py @@ -20,5 +20,5 @@ class Command(BaseCommand): data = json.load(options.get('filename')) status_code, response = MapServiceView.handle_mapservice_push_request(conference, data) - print('RESPONSE (', status_code, '):', sep='') + print(f'RESPONSE ({status_code}):') print(response) diff --git a/src/core/management/commands/rerender_markdown.py b/src/core/management/commands/rerender_markdown.py index 9dce899decaceb6e9c5f28835259587daedcb3a0..996f3cb5bb716ac2203fba1ff528280e0835429e 100644 --- a/src/core/management/commands/rerender_markdown.py +++ b/src/core/management/commands/rerender_markdown.py @@ -1,33 +1,38 @@ -from django.core.management.base import BaseCommand +from django_rich.management import RichCommand from core.markdown import render_markdown_ex, store_relationships from core.models import Assembly, ConferenceMember, Event, Room, StaticPage -class Command(BaseCommand): +class Command(RichCommand): def handle(self, *args, **options): # markdown is usually rerendered in the save() method, so calling save() is sufficient here for most models - for event in Event.objects.all(): - event.save(update_fields=['description']) + with self.console.status('Rerendering markdown for all events') as status: + for event in Event.objects.all(): + event.save(update_fields=['description']) - for room in Room.objects.all(): - room.save(update_fields=['description']) + status.update('Rerendering markdown for all rooms') + for room in Room.objects.all(): + room.save(update_fields=['description']) - for assembly in Assembly.objects.all(): - assembly.save(update_fields=['description']) + status.update('Rerendering markdown for all assemblies') + for assembly in Assembly.objects.all(): + assembly.save(update_fields=['description']) - for user in ConferenceMember.objects.all(): - user.save(update_fields=['description']) + status.update('Rerendering markdown for all conference members') + for user in ConferenceMember.objects.all(): + user.save(update_fields=['description']) - for page in StaticPage.objects.all(): - if page.public_revision > 0: - public_page = page.revisions.filter(revision=page.public_revision).first() - if public_page is None: - page.body_html = '' + status.update('Rerendering markdown for all static pages') + for page in StaticPage.objects.all(): + if page.public_revision > 0: + public_page = page.revisions.filter(revision=page.public_revision).first() + if public_page is None: + page.body_html = '' + else: + render_result = render_markdown_ex(page.conference, public_page.body) + page.body_html = render_result.document + store_relationships(page.conference, page, render_result) else: - render_result = render_markdown_ex(page.conference, public_page.body) - page.body_html = render_result.document - store_relationships(page.conference, page, render_result) - else: - page.body_html = '' - page.save(update_fields=['body_html']) + page.body_html = '' + page.save(update_fields=['body_html']) diff --git a/src/core/management/commands/stats.py b/src/core/management/commands/stats.py deleted file mode 100644 index 7eb4aca693fed9c2bbffbcb6c32cf9c2538cd79e..0000000000000000000000000000000000000000 --- a/src/core/management/commands/stats.py +++ /dev/null @@ -1,37 +0,0 @@ -from django.core.management.base import BaseCommand - -from core.models import Assembly, Badge, Conference, Event, Room, UserBadge - - -class Command(BaseCommand): - def handle(self, *args, **options): - for conf in Conference.objects.all(): - print('#', conf.slug, '(' + conf.name + ')') - - print( - '- assemblies:', - Assembly.objects.filter(conference=conf).count(), - 'total,', - Assembly.objects.filter(conference=conf, state__in=Assembly.PUBLIC_STATES).count(), - 'accepted', - ) - - print( - '- events:', - Event.objects.filter(conference=conf).count(), - 'total,', - Event.objects.filter(conference=conf, is_public=True).count(), - 'visible', - ) - - print('- rooms:', Room.objects.filter(conference=conf).count(), 'total') - for rt in Room.BACKEND_ROOMTYPES: - print(' -', Room.objects.filter(conference=conf, room_type=rt).count(), rt.label) - - print('- badges (created):', Badge.objects.filter(conference=conf).count()) - - badges_redeemed = { - 'achievement': UserBadge.objects.filter(badge__is_achievement=True, badge__conference=conf).count(), - 'sticker': UserBadge.objects.filter(badge__is_achievement=False, badge__conference=conf).count(), - } - print(f'- badges (redeemed): {badges_redeemed["achievement"]} achievements, {badges_redeemed["sticker"]} stickers') diff --git a/src/core/management/commands/test_schedule_import.py b/src/core/management/commands/test_schedule_import.py index 8108cf4abe05fda1bbe57061241932e14541c953..5c8be0207b1749b04ca954cb4b11f8dcc3ec0d89 100644 --- a/src/core/management/commands/test_schedule_import.py +++ b/src/core/management/commands/test_schedule_import.py @@ -3,13 +3,13 @@ import json from django.conf import settings from django.core.exceptions import SuspiciousOperation -from django.core.management.base import BaseCommand from django.db import transaction +from django_rich.management import RichCommand from core.models.conference import Conference -class Command(BaseCommand): +class Command(RichCommand): def add_arguments(self, parser): parser.add_argument('--data-file', '-f', type=argparse.FileType('r'), help='the data file to load') @@ -27,4 +27,4 @@ class Command(BaseCommand): except SuspiciousOperation: pass - print(json.dumps(activity, indent=2)) + self.console.print_json(activity)