Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions admin/nodes/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -754,6 +754,7 @@ class ForceArchiveRegistrationsView(NodeMixin, View):
def post(self, request, *args, **kwargs):
# Prevents circular imports that cause admin app to hang at startup
from osf.management.commands.force_archive import verify, archive, DEFAULT_PERMISSIBLE_ADDONS
from osf.models.admin_log_entry import update_admin_log, MANUAL_ARCHIVE_RESTART

registration = self.get_object()
force_archive_params = request.POST
Expand All @@ -779,6 +780,14 @@ def post(self, request, *args, **kwargs):
messages.success(request, f"Registration {registration._id} can be archived.")
else:
try:
update_admin_log(
user_id=request.user.id,
object_id=registration.pk,
object_repr=str(registration),
message=f'Manual archive restart initiated for registration {registration._id}',
action_flag=MANUAL_ARCHIVE_RESTART
)

archive(
registration,
permissible_addons=addons,
Expand Down
145 changes: 145 additions & 0 deletions osf/management/commands/process_manual_restart_approvals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import logging
from datetime import timedelta
from django.core.management.base import BaseCommand
from django.utils import timezone
from osf.models import Registration
from osf.models.admin_log_entry import AdminLogEntry, MANUAL_ARCHIVE_RESTART
from website import settings
from scripts.approve_registrations import approve_past_pendings

logger = logging.getLogger(__name__)


class Command(BaseCommand):
help = 'Process registrations that were manually restarted and may need approval'

def add_arguments(self, parser):
parser.add_argument(
'--dry-run',
action='store_true',
help='Show what would be done without actually doing it',
)
parser.add_argument(
'--hours-back',
type=int,
default=72,
help='How many hours back to look for manual restarts (default: 72)',
)
parser.add_argument(
'--registration-id',
type=str,
help='Process a specific registration ID only',
)

def handle(self, *args, **options):
dry_run = options['dry_run']
hours_back = options['hours_back']
specific_registration = options.get('registration_id')

if dry_run:
self.stdout.write(self.style.WARNING('Running in DRY RUN mode - no changes will be made'))

since = timezone.now() - timedelta(hours=hours_back)

query = AdminLogEntry.objects.filter(
action_flag=MANUAL_ARCHIVE_RESTART,
action_time__gte=since
)

if specific_registration:
try:
reg = Registration.objects.get(_id=specific_registration)
query = query.filter(object_id=reg.pk)
self.stdout.write(f"Processing specific registration: {specific_registration}")
except Registration.DoesNotExist:
self.stdout.write(self.style.ERROR(f"Registration {specific_registration} not found"))
return

manual_restart_logs = query.values_list('object_id', flat=True).distinct()

registrations_to_check = Registration.objects.filter(
pk__in=manual_restart_logs,
)

self.stdout.write(f"Found {registrations_to_check.count()} manually restarted registrations to check")

approvals_ready = []
skipped_registrations = []

for registration in registrations_to_check:
status = self.should_auto_approve(registration)

if status == 'ready':
approval = registration.registration_approval
if approval:
approvals_ready.append(approval)
self.stdout.write(
self.style.SUCCESS(f"✓ Queuing registration {registration._id} for approval")
)
else:
skipped_registrations.append((registration._id, status))
self.stdout.write(
self.style.WARNING(f"⚠ Skipping registration {registration._id}: {status}")
)

if approvals_ready:
if dry_run:
self.stdout.write(
self.style.WARNING(f"DRY RUN: Would approve {len(approvals_ready)} registrations")
)
else:
try:
approve_past_pendings(approvals_ready, dry_run=False)
self.stdout.write(
self.style.SUCCESS(f"✓ Successfully approved {len(approvals_ready)} manually restarted registrations")
)
except Exception as e:
self.stdout.write(
self.style.ERROR(f"✗ Error approving registrations: {e}")
)
else:
self.stdout.write('No registrations ready for approval')

self.stdout.write(f"Total checked: {registrations_to_check.count()}")
self.stdout.write(f"Ready for approval: {len(approvals_ready)}")
self.stdout.write(f"Skipped: {len(skipped_registrations)}")

if skipped_registrations:
self.stdout.write('\nSkipped registrations:')
for reg_id, reason in skipped_registrations:
self.stdout.write(f" - {reg_id}: {reason}")

def should_auto_approve(self, registration):
if registration.is_public:
return 'already public'

if registration.is_registration_approved:
return 'already approved'

if registration.archiving:
return 'still archiving'

archive_job = registration.archive_job
if archive_job and hasattr(archive_job, 'status'):
if archive_job.status not in ['SUCCESS', None]:
return f'archive status: {archive_job.status}'

approval = registration.registration_approval
if not approval:
return 'no approval object'

if approval.is_approved:
return 'approval already approved'

if approval.is_rejected:
return 'approval was rejected'

time_since_initiation = timezone.now() - approval.initiation_date
if time_since_initiation < settings.REGISTRATION_APPROVAL_TIME:
remaining = settings.REGISTRATION_APPROVAL_TIME - time_since_initiation
return f'not ready yet ({remaining} remaining)'

if registration.is_stuck_registration:
return 'registration still stuck'

return 'ready'
2 changes: 2 additions & 0 deletions osf/models/admin_log_entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
PREPRINT_REMOVED = 70
PREPRINT_RESTORED = 71

MANUAL_ARCHIVE_RESTART = 80

def update_admin_log(user_id, object_id, object_repr, message, action_flag=UNKNOWN):
AdminLogEntry.objects.log_action(
user_id=user_id,
Expand Down
74 changes: 74 additions & 0 deletions scripts/check_manual_restart_approval.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import logging
from framework.celery_tasks import app as celery_app
from django.core.management import call_command
from osf.models import Registration

logger = logging.getLogger(__name__)


@celery_app.task(name='scripts.check_manual_restart_approval')
def check_manual_restart_approval(registration_id):
try:
try:
registration = Registration.objects.get(_id=registration_id)
except Registration.DoesNotExist:
logger.error(f"Registration {registration_id} not found")
return f"Registration {registration_id} not found"

if registration.is_public or registration.is_registration_approved:
return f"Registration {registration_id} already approved/public"

if registration.archiving:
logger.info(f"Registration {registration_id} still archiving, retrying in 10 minutes")
check_manual_restart_approval.apply_async(
args=[registration_id],
countdown=600
)
return f"Registration {registration_id} still archiving, scheduled retry"

logger.info(f"Processing manual restart approval for registration {registration_id}")

call_command(
'process_manual_restart_approvals',
registration_id=registration_id,
dry_run=False,
hours_back=24,
verbosity=1
)

return f"Processed manual restart approval check for registration {registration_id}"

except Exception as e:
logger.error(f"Error processing manual restart approval for {registration_id}: {e}")
raise


@celery_app.task(name='scripts.check_manual_restart_approvals_batch')
def check_manual_restart_approvals_batch(hours_back=24):
try:
logger.info(f"Running batch check for manual restart approvals (last {hours_back} hours)")

call_command(
'process_manual_restart_approvals',
dry_run=False,
hours_back=hours_back,
verbosity=1
)

return f"Completed batch manual restart approval check for last {hours_back} hours"

except Exception as e:
logger.error(f"Error in batch manual restart approval check: {e}")
raise


@celery_app.task(name='scripts.delayed_manual_restart_approval')
def delayed_manual_restart_approval(registration_id, delay_minutes=30):
logger.info(f"Scheduling delayed manual restart approval check for {registration_id} in {delay_minutes} minutes")

check_manual_restart_approval.apply_async(
args=[registration_id],
countdown=delay_minutes * 60
)

return f"Scheduled manual restart approval check for {registration_id} in {delay_minutes} minutes"
137 changes: 137 additions & 0 deletions scripts/enhanced_stuck_registration_audit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import logging

from django.core.management import call_command
from framework.celery_tasks import app as celery_app
from osf.models import Registration
from osf.management.commands.force_archive import archive, DEFAULT_PERMISSIBLE_ADDONS
from scripts.stuck_registration_audit import analyze_failed_registration_nodes

logger = logging.getLogger(__name__)


@celery_app.task(name='scripts.enhanced_stuck_registration_audit')
def enhanced_stuck_registration_audit():
logger.info('Starting enhanced stuck registration audit')

try:
logger.info('Processing pending manual restart approvals')
call_command('process_manual_restart_approvals', dry_run=False, hours_back=72)
except Exception as e:
logger.error(f"Error processing manual restart approvals: {e}")

logger.info('Analyzing failed registrations')
failed_registrations = analyze_failed_registration_nodes()

if not failed_registrations:
logger.info('No failed registrations found')
return 'No failed registrations found'

logger.info(f"Found {len(failed_registrations)} failed registrations")

auto_retryable = []
needs_manual_intervention = []

for reg_info in failed_registrations:
registration_id = reg_info['registration']

try:
registration = Registration.objects.get(_id=registration_id)

if should_auto_retry(reg_info, registration):
auto_retryable.append(registration)
logger.info(f"Registration {registration_id} eligible for auto-retry")
else:
needs_manual_intervention.append(reg_info)
logger.info(f"Registration {registration_id} needs manual intervention")

except Registration.DoesNotExist:
logger.warning(f"Registration {registration_id} not found")
needs_manual_intervention.append(reg_info)
continue

successfully_retried = []
failed_auto_retries = []

for reg in auto_retryable:
try:
logger.info(f"Attempting auto-retry for stuck registration {reg._id}")

archive(
reg,
permissible_addons=DEFAULT_PERMISSIBLE_ADDONS,
allow_unconfigured=True,
skip_collisions=True
)

successfully_retried.append(reg._id)
logger.info(f"Successfully auto-retried registration {reg._id}")

except Exception as e:
logger.error(f"Auto-retry failed for registration {reg._id}: {e}")
failed_auto_retries.append({
'registration': reg._id,
'auto_retry_error': str(e),
'original_info': next(info for info in failed_registrations if info['registration'] == reg._id)
})

needs_manual_intervention.extend(failed_auto_retries)

logger.info(f"Auto-retry results: {len(successfully_retried)} successful, {len(failed_auto_retries)} failed")

summary = {
'total_failed': len(failed_registrations),
'auto_retried_success': len(successfully_retried),
'auto_retried_failed': len(failed_auto_retries),
'needs_manual': len(needs_manual_intervention),
'successfully_retried_ids': successfully_retried
}

logger.info(f"Enhanced audit completed: {summary}")
return summary


def should_auto_retry(reg_info, registration):
if not reg_info.get('can_be_reset', False):
return False

addon_list = reg_info.get('addon_list', [])
complex_addons = set(addon_list) - {'osfstorage', 'wiki'}
if complex_addons:
logger.info(f"Registration {registration._id} has complex addons: {complex_addons}")
return False

logs_after_reg = reg_info.get('logs_on_original_after_registration_date', [])
if logs_after_reg:
logger.info(f"Registration {registration._id} has post-registration logs: {logs_after_reg}")
return False

successful_after = reg_info.get('succeeded_registrations_after_failed', [])
if successful_after:
logger.info(f"Registration {registration._id} has successful registrations after failure: {successful_after}")
return False

import django.utils.timezone as timezone
from datetime import timedelta
if registration.registered_date:
age = timezone.now() - registration.registered_date
if age > timedelta(days=30):
logger.info(f"Registration {registration._id} is too old ({age.days} days)")
return False
return True

@celery_app.task(name='scripts.manual_restart_approval_batch')
def manual_restart_approval_batch():
logger.info('Running manual restart approval batch task')

try:
from scripts.check_manual_restart_approval import check_manual_restart_approvals_batch
result = check_manual_restart_approvals_batch.delay(hours_back=24)
return f"Queued manual restart approval batch task: {result.id}"
except Exception as e:
logger.error(f"Error running manual restart approval batch: {e}")
raise


if __name__ == '__main__':
result = enhanced_stuck_registration_audit()
print(f"Audit completed: {result}")
Loading
Loading