Skip to content

Commit d4b7a7c

Browse files
committed
Add management command to manual archive
1 parent 38ad074 commit d4b7a7c

File tree

7 files changed

+390
-0
lines changed

7 files changed

+390
-0
lines changed

admin/nodes/views.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -754,6 +754,7 @@ class ForceArchiveRegistrationsView(NodeMixin, View):
754754
def post(self, request, *args, **kwargs):
755755
# Prevents circular imports that cause admin app to hang at startup
756756
from osf.management.commands.force_archive import verify, archive, DEFAULT_PERMISSIBLE_ADDONS
757+
from osf.models.admin_log_entry import update_admin_log, MANUAL_ARCHIVE_RESTART
757758

758759
registration = self.get_object()
759760
force_archive_params = request.POST
@@ -779,6 +780,14 @@ def post(self, request, *args, **kwargs):
779780
messages.success(request, f"Registration {registration._id} can be archived.")
780781
else:
781782
try:
783+
update_admin_log(
784+
user_id=request.user.id,
785+
object_id=registration.pk,
786+
object_repr=str(registration),
787+
message=f'Manual archive restart initiated for registration {registration._id}',
788+
action_flag=MANUAL_ARCHIVE_RESTART
789+
)
790+
782791
archive(
783792
registration,
784793
permissible_addons=addons,
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import logging
2+
from datetime import timedelta
3+
from django.core.management.base import BaseCommand
4+
from django.utils import timezone
5+
from osf.models import Registration
6+
from osf.models.admin_log_entry import AdminLogEntry, MANUAL_ARCHIVE_RESTART
7+
from website import settings
8+
from scripts.approve_registrations import approve_past_pendings
9+
10+
logger = logging.getLogger(__name__)
11+
12+
13+
class Command(BaseCommand):
14+
help = 'Process registrations that were manually restarted and may need approval'
15+
16+
def add_arguments(self, parser):
17+
parser.add_argument(
18+
'--dry-run',
19+
action='store_true',
20+
help='Show what would be done without actually doing it',
21+
)
22+
parser.add_argument(
23+
'--hours-back',
24+
type=int,
25+
default=72,
26+
help='How many hours back to look for manual restarts (default: 72)',
27+
)
28+
parser.add_argument(
29+
'--registration-id',
30+
type=str,
31+
help='Process a specific registration ID only',
32+
)
33+
34+
def handle(self, *args, **options):
35+
dry_run = options['dry_run']
36+
hours_back = options['hours_back']
37+
specific_registration = options.get('registration_id')
38+
39+
if dry_run:
40+
self.stdout.write(self.style.WARNING('Running in DRY RUN mode - no changes will be made'))
41+
42+
since = timezone.now() - timedelta(hours=hours_back)
43+
44+
query = AdminLogEntry.objects.filter(
45+
action_flag=MANUAL_ARCHIVE_RESTART,
46+
action_time__gte=since
47+
)
48+
49+
if specific_registration:
50+
try:
51+
reg = Registration.objects.get(_id=specific_registration)
52+
query = query.filter(object_id=reg.pk)
53+
self.stdout.write(f"Processing specific registration: {specific_registration}")
54+
except Registration.DoesNotExist:
55+
self.stdout.write(self.style.ERROR(f"Registration {specific_registration} not found"))
56+
return
57+
58+
manual_restart_logs = query.values_list('object_id', flat=True).distinct()
59+
60+
registrations_to_check = Registration.objects.filter(
61+
pk__in=manual_restart_logs,
62+
)
63+
64+
self.stdout.write(f"Found {registrations_to_check.count()} manually restarted registrations to check")
65+
66+
approvals_ready = []
67+
skipped_registrations = []
68+
69+
for registration in registrations_to_check:
70+
status = self.should_auto_approve(registration)
71+
72+
if status == 'ready':
73+
approval = registration.registration_approval
74+
if approval:
75+
approvals_ready.append(approval)
76+
self.stdout.write(
77+
self.style.SUCCESS(f"✓ Queuing registration {registration._id} for approval")
78+
)
79+
else:
80+
skipped_registrations.append((registration._id, status))
81+
self.stdout.write(
82+
self.style.WARNING(f"⚠ Skipping registration {registration._id}: {status}")
83+
)
84+
85+
if approvals_ready:
86+
if dry_run:
87+
self.stdout.write(
88+
self.style.WARNING(f"DRY RUN: Would approve {len(approvals_ready)} registrations")
89+
)
90+
else:
91+
try:
92+
approve_past_pendings(approvals_ready, dry_run=False)
93+
self.stdout.write(
94+
self.style.SUCCESS(f"✓ Successfully approved {len(approvals_ready)} manually restarted registrations")
95+
)
96+
except Exception as e:
97+
self.stdout.write(
98+
self.style.ERROR(f"✗ Error approving registrations: {e}")
99+
)
100+
else:
101+
self.stdout.write('No registrations ready for approval')
102+
103+
self.stdout.write(f"Total checked: {registrations_to_check.count()}")
104+
self.stdout.write(f"Ready for approval: {len(approvals_ready)}")
105+
self.stdout.write(f"Skipped: {len(skipped_registrations)}")
106+
107+
if skipped_registrations:
108+
self.stdout.write('\nSkipped registrations:')
109+
for reg_id, reason in skipped_registrations:
110+
self.stdout.write(f" - {reg_id}: {reason}")
111+
112+
def should_auto_approve(self, registration):
113+
if registration.is_public:
114+
return 'already public'
115+
116+
if registration.is_registration_approved:
117+
return 'already approved'
118+
119+
if registration.archiving:
120+
return 'still archiving'
121+
122+
archive_job = registration.archive_job
123+
if archive_job and hasattr(archive_job, 'status'):
124+
if archive_job.status not in ['SUCCESS', None]:
125+
return f'archive status: {archive_job.status}'
126+
127+
approval = registration.registration_approval
128+
if not approval:
129+
return 'no approval object'
130+
131+
if approval.is_approved:
132+
return 'approval already approved'
133+
134+
if approval.is_rejected:
135+
return 'approval was rejected'
136+
137+
time_since_initiation = timezone.now() - approval.initiation_date
138+
if time_since_initiation < settings.REGISTRATION_APPROVAL_TIME:
139+
remaining = settings.REGISTRATION_APPROVAL_TIME - time_since_initiation
140+
return f'not ready yet ({remaining} remaining)'
141+
142+
if registration.is_stuck_registration:
143+
return 'registration still stuck'
144+
145+
return 'ready'

osf/models/admin_log_entry.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
PREPRINT_REMOVED = 70
3535
PREPRINT_RESTORED = 71
3636

37+
MANUAL_ARCHIVE_RESTART = 80
38+
3739
def update_admin_log(user_id, object_id, object_repr, message, action_flag=UNKNOWN):
3840
AdminLogEntry.objects.log_action(
3941
user_id=user_id,
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import logging
2+
from framework.celery_tasks import app as celery_app
3+
from django.core.management import call_command
4+
from osf.models import Registration
5+
6+
logger = logging.getLogger(__name__)
7+
8+
9+
@celery_app.task(name='scripts.check_manual_restart_approval')
10+
def check_manual_restart_approval(registration_id):
11+
try:
12+
try:
13+
registration = Registration.objects.get(_id=registration_id)
14+
except Registration.DoesNotExist:
15+
logger.error(f"Registration {registration_id} not found")
16+
return f"Registration {registration_id} not found"
17+
18+
if registration.is_public or registration.is_registration_approved:
19+
return f"Registration {registration_id} already approved/public"
20+
21+
if registration.archiving:
22+
logger.info(f"Registration {registration_id} still archiving, retrying in 10 minutes")
23+
check_manual_restart_approval.apply_async(
24+
args=[registration_id],
25+
countdown=600
26+
)
27+
return f"Registration {registration_id} still archiving, scheduled retry"
28+
29+
logger.info(f"Processing manual restart approval for registration {registration_id}")
30+
31+
call_command(
32+
'process_manual_restart_approvals',
33+
registration_id=registration_id,
34+
dry_run=False,
35+
hours_back=24,
36+
verbosity=1
37+
)
38+
39+
return f"Processed manual restart approval check for registration {registration_id}"
40+
41+
except Exception as e:
42+
logger.error(f"Error processing manual restart approval for {registration_id}: {e}")
43+
raise
44+
45+
46+
@celery_app.task(name='scripts.check_manual_restart_approvals_batch')
47+
def check_manual_restart_approvals_batch(hours_back=24):
48+
try:
49+
logger.info(f"Running batch check for manual restart approvals (last {hours_back} hours)")
50+
51+
call_command(
52+
'process_manual_restart_approvals',
53+
dry_run=False,
54+
hours_back=hours_back,
55+
verbosity=1
56+
)
57+
58+
return f"Completed batch manual restart approval check for last {hours_back} hours"
59+
60+
except Exception as e:
61+
logger.error(f"Error in batch manual restart approval check: {e}")
62+
raise
63+
64+
65+
@celery_app.task(name='scripts.delayed_manual_restart_approval')
66+
def delayed_manual_restart_approval(registration_id, delay_minutes=30):
67+
logger.info(f"Scheduling delayed manual restart approval check for {registration_id} in {delay_minutes} minutes")
68+
69+
check_manual_restart_approval.apply_async(
70+
args=[registration_id],
71+
countdown=delay_minutes * 60
72+
)
73+
74+
return f"Scheduled manual restart approval check for {registration_id} in {delay_minutes} minutes"
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import logging
2+
3+
from django.core.management import call_command
4+
from framework.celery_tasks import app as celery_app
5+
from osf.models import Registration
6+
from osf.management.commands.force_archive import archive, DEFAULT_PERMISSIBLE_ADDONS
7+
from scripts.stuck_registration_audit import analyze_failed_registration_nodes
8+
9+
logger = logging.getLogger(__name__)
10+
11+
12+
@celery_app.task(name='scripts.enhanced_stuck_registration_audit')
13+
def enhanced_stuck_registration_audit():
14+
logger.info('Starting enhanced stuck registration audit')
15+
16+
try:
17+
logger.info('Processing pending manual restart approvals')
18+
call_command('process_manual_restart_approvals', dry_run=False, hours_back=72)
19+
except Exception as e:
20+
logger.error(f"Error processing manual restart approvals: {e}")
21+
22+
logger.info('Analyzing failed registrations')
23+
failed_registrations = analyze_failed_registration_nodes()
24+
25+
if not failed_registrations:
26+
logger.info('No failed registrations found')
27+
return 'No failed registrations found'
28+
29+
logger.info(f"Found {len(failed_registrations)} failed registrations")
30+
31+
auto_retryable = []
32+
needs_manual_intervention = []
33+
34+
for reg_info in failed_registrations:
35+
registration_id = reg_info['registration']
36+
37+
try:
38+
registration = Registration.objects.get(_id=registration_id)
39+
40+
if should_auto_retry(reg_info, registration):
41+
auto_retryable.append(registration)
42+
logger.info(f"Registration {registration_id} eligible for auto-retry")
43+
else:
44+
needs_manual_intervention.append(reg_info)
45+
logger.info(f"Registration {registration_id} needs manual intervention")
46+
47+
except Registration.DoesNotExist:
48+
logger.warning(f"Registration {registration_id} not found")
49+
needs_manual_intervention.append(reg_info)
50+
continue
51+
52+
successfully_retried = []
53+
failed_auto_retries = []
54+
55+
for reg in auto_retryable:
56+
try:
57+
logger.info(f"Attempting auto-retry for stuck registration {reg._id}")
58+
59+
archive(
60+
reg,
61+
permissible_addons=DEFAULT_PERMISSIBLE_ADDONS,
62+
allow_unconfigured=True,
63+
skip_collisions=True
64+
)
65+
66+
successfully_retried.append(reg._id)
67+
logger.info(f"Successfully auto-retried registration {reg._id}")
68+
69+
except Exception as e:
70+
logger.error(f"Auto-retry failed for registration {reg._id}: {e}")
71+
failed_auto_retries.append({
72+
'registration': reg._id,
73+
'auto_retry_error': str(e),
74+
'original_info': next(info for info in failed_registrations if info['registration'] == reg._id)
75+
})
76+
77+
needs_manual_intervention.extend(failed_auto_retries)
78+
79+
logger.info(f"Auto-retry results: {len(successfully_retried)} successful, {len(failed_auto_retries)} failed")
80+
81+
summary = {
82+
'total_failed': len(failed_registrations),
83+
'auto_retried_success': len(successfully_retried),
84+
'auto_retried_failed': len(failed_auto_retries),
85+
'needs_manual': len(needs_manual_intervention),
86+
'successfully_retried_ids': successfully_retried
87+
}
88+
89+
logger.info(f"Enhanced audit completed: {summary}")
90+
return summary
91+
92+
93+
def should_auto_retry(reg_info, registration):
94+
if not reg_info.get('can_be_reset', False):
95+
return False
96+
97+
addon_list = reg_info.get('addon_list', [])
98+
complex_addons = set(addon_list) - {'osfstorage', 'wiki'}
99+
if complex_addons:
100+
logger.info(f"Registration {registration._id} has complex addons: {complex_addons}")
101+
return False
102+
103+
logs_after_reg = reg_info.get('logs_on_original_after_registration_date', [])
104+
if logs_after_reg:
105+
logger.info(f"Registration {registration._id} has post-registration logs: {logs_after_reg}")
106+
return False
107+
108+
successful_after = reg_info.get('succeeded_registrations_after_failed', [])
109+
if successful_after:
110+
logger.info(f"Registration {registration._id} has successful registrations after failure: {successful_after}")
111+
return False
112+
113+
import django.utils.timezone as timezone
114+
from datetime import timedelta
115+
if registration.registered_date:
116+
age = timezone.now() - registration.registered_date
117+
if age > timedelta(days=30):
118+
logger.info(f"Registration {registration._id} is too old ({age.days} days)")
119+
return False
120+
return True
121+
122+
@celery_app.task(name='scripts.manual_restart_approval_batch')
123+
def manual_restart_approval_batch():
124+
logger.info('Running manual restart approval batch task')
125+
126+
try:
127+
from scripts.check_manual_restart_approval import check_manual_restart_approvals_batch
128+
result = check_manual_restart_approvals_batch.delay(hours_back=24)
129+
return f"Queued manual restart approval batch task: {result.id}"
130+
except Exception as e:
131+
logger.error(f"Error running manual restart approval batch: {e}")
132+
raise
133+
134+
135+
if __name__ == '__main__':
136+
result = enhanced_stuck_registration_audit()
137+
print(f"Audit completed: {result}")

0 commit comments

Comments
 (0)