Skip to content

Commit ef75213

Browse files
script to move scriteria
1 parent 1538bef commit ef75213

1 file changed

Lines changed: 187 additions & 0 deletions

File tree

scripts/move_criteria.py

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
#!/usr/bin/env python
2+
"""
3+
Script to move/copy review criteria from one course to another.
4+
5+
Usage:
6+
uv run python scripts/move_criteria.py \\
7+
--source-course ml-zoomcamp-2024 \\
8+
--dest-course ml-zoomcamp-2025
9+
10+
# Preview what would be copied without making changes
11+
uv run python scripts/move_criteria.py \\
12+
--source-course ml-zoomcamp-2024 \\
13+
--dest-course ml-zoomcamp-2025 \\
14+
--dry-run
15+
16+
# Move criteria (delete from source after copying)
17+
uv run python scripts/move_criteria.py \\
18+
--source-course ml-zoomcamp-2024 \\
19+
--dest-course ml-zoomcamp-2025 \\
20+
--delete-in-source
21+
"""
22+
23+
import os
24+
import sys
25+
import argparse
26+
27+
# Add parent directory to path so Django can find course_management module
28+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
29+
30+
# Setup Django
31+
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "course_management.settings")
32+
33+
import django
34+
django.setup()
35+
36+
from courses.models import Course, ReviewCriteria
37+
38+
39+
def list_criteria(course: Course) -> list[ReviewCriteria]:
40+
"""List all criteria for a course."""
41+
return list(course.reviewcriteria_set.all().order_by('id'))
42+
43+
44+
def copy_criteria(
45+
source_course: Course,
46+
dest_course: Course,
47+
dry_run: bool = False,
48+
delete_from_source: bool = False
49+
) -> tuple[list[ReviewCriteria], list[ReviewCriteria]]:
50+
"""
51+
Copy criteria from source course to destination course.
52+
53+
Skips criteria that already exist (same description and options).
54+
55+
Returns a tuple of (created_criteria, deleted_criteria).
56+
"""
57+
source_criteria = list_criteria(source_course)
58+
dest_existing = list_criteria(dest_course)
59+
60+
# Build set of existing (description, type) tuples for quick lookup
61+
existing_keys = {
62+
(c.description, c.review_criteria_type)
63+
for c in dest_existing
64+
}
65+
66+
created = []
67+
to_delete = []
68+
69+
for criteria in source_criteria:
70+
key = (criteria.description, criteria.review_criteria_type)
71+
if key in existing_keys:
72+
print(f" ⊘ Skipping existing: {criteria.description}")
73+
continue
74+
75+
if dry_run:
76+
print(f" + Would create: {criteria.description}")
77+
created.append(criteria)
78+
if delete_from_source:
79+
to_delete.append(criteria)
80+
else:
81+
new_criteria = ReviewCriteria.objects.create(
82+
course=dest_course,
83+
description=criteria.description,
84+
options=criteria.options,
85+
review_criteria_type=criteria.review_criteria_type,
86+
)
87+
print(f" ✓ Created: {new_criteria.description}")
88+
created.append(new_criteria)
89+
if delete_from_source:
90+
to_delete.append(criteria)
91+
92+
if delete_from_source and to_delete and not dry_run:
93+
print()
94+
for criteria in to_delete:
95+
print(f" ✗ Deleting from source: {criteria.description}")
96+
criteria.delete()
97+
98+
return created, to_delete
99+
100+
101+
def main():
102+
parser = argparse.ArgumentParser(
103+
description="Move/copy review criteria from one course to another"
104+
)
105+
parser.add_argument(
106+
"--source-course",
107+
required=True,
108+
help="Source course slug"
109+
)
110+
parser.add_argument(
111+
"--dest-course",
112+
required=True,
113+
help="Destination course slug"
114+
)
115+
parser.add_argument(
116+
"--dry-run",
117+
action="store_true",
118+
help="Preview what would be copied without making changes"
119+
)
120+
parser.add_argument(
121+
"--delete-in-source",
122+
action="store_true",
123+
help="Delete criteria from source course after copying"
124+
)
125+
126+
args = parser.parse_args()
127+
128+
# Get source course
129+
try:
130+
source_course = Course.objects.get(slug=args.source_course)
131+
except Course.DoesNotExist:
132+
print(f"Error: Source course '{args.source_course}' not found")
133+
sys.exit(1)
134+
135+
# Get destination course
136+
try:
137+
dest_course = Course.objects.get(slug=args.dest_course)
138+
except Course.DoesNotExist:
139+
print(f"Error: Destination course '{args.dest_course}' not found")
140+
sys.exit(1)
141+
142+
print("=" * 60)
143+
print("Criteria Migration")
144+
print("=" * 60)
145+
print(f"Source course: {source_course.title} ({source_course.slug})")
146+
print(f"Destination course: {dest_course.title} ({dest_course.slug})")
147+
print()
148+
149+
source_criteria = list_criteria(source_course)
150+
dest_criteria = list_criteria(dest_course)
151+
152+
print(f"Source criteria count: {len(source_criteria)}")
153+
print(f"Destination criteria count: {len(dest_criteria)}")
154+
print()
155+
156+
if args.dry_run:
157+
print("DRY RUN - No changes will be made")
158+
print()
159+
160+
print("Migrating criteria:")
161+
print("-" * 60)
162+
163+
created, deleted = copy_criteria(
164+
source_course,
165+
dest_course,
166+
dry_run=args.dry_run,
167+
delete_from_source=args.delete_in_source
168+
)
169+
170+
print("-" * 60)
171+
if args.dry_run:
172+
print(f"Would create {len(created)} criteria")
173+
if args.delete_in_source:
174+
print(f"Would delete {len(deleted)} criteria from source")
175+
else:
176+
print(f"Created {len(created)} criteria")
177+
if args.delete_in_source:
178+
print(f"Deleted {len(deleted)} criteria from source")
179+
180+
# Show updated destination criteria
181+
if not args.dry_run:
182+
dest_criteria_after = list_criteria(dest_course)
183+
print(f"Destination course now has {len(dest_criteria_after)} criteria")
184+
185+
186+
if __name__ == "__main__":
187+
main()

0 commit comments

Comments
 (0)