Skip to content

Commit 423d1a9

Browse files
Move importing assignments into a celery task (#118)
Closes #110
1 parent 8eb761c commit 423d1a9

5 files changed

Lines changed: 177 additions & 61 deletions

File tree

tin/apps/courses/forms.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,16 @@ def __init__(self, course, *args, **kwargs):
7474
required=False,
7575
)
7676

77+
def serialize_for_task(self):
78+
return {
79+
"folder_ids": [f.id for f in self.cleaned_data["folders"]],
80+
"assignment_ids": [a.id for a in self.cleaned_data["assignments"]],
81+
"hide": self.cleaned_data["hide"],
82+
"shift_due_dates": self.cleaned_data["shift_due_dates"],
83+
"copy_graders": self.cleaned_data["copy_graders"],
84+
"copy_files": self.cleaned_data["copy_files"],
85+
}
86+
7787

7888
class StudentForm(forms.ModelForm):
7989
students = UserMultipleChoiceField(

tin/apps/courses/tasks.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
from pathlib import Path
2+
3+
from celery import shared_task
4+
from django.utils import timezone
5+
6+
from tin.apps.assignments.models import Assignment, Folder
7+
8+
from .models import Course
9+
10+
11+
@shared_task(bind=True)
12+
def import_course_data_tasks(self, target_id, source_id, data):
13+
target_course = Course.objects.get(pk=target_id)
14+
15+
folder_ids = data.get("folder_ids", [])
16+
individual_assignment_ids = data.get("assignment_ids", [])
17+
18+
assignments_to_process = []
19+
20+
folders = Folder.objects.filter(id__in=folder_ids)
21+
for folder in folders:
22+
folder_assignments = list(folder.assignments.all())
23+
folder.pk = None
24+
folder._state.adding = True
25+
folder.course = target_course
26+
folder.save()
27+
28+
for a in folder_assignments:
29+
assignments_to_process.append((a, folder.id))
30+
31+
individual_assignments = Assignment.objects.filter(id__in=individual_assignment_ids)
32+
for a in individual_assignments:
33+
assignments_to_process.append((a, None))
34+
35+
total = len(assignments_to_process)
36+
37+
for index, (old_assignment, new_folder_id) in enumerate(assignments_to_process):
38+
assignment = old_assignment
39+
assignment.pk = None
40+
assignment._state.adding = True
41+
assignment.course = target_course
42+
assignment.folder_id = new_folder_id
43+
assignment.assigned = timezone.now()
44+
assignment.grader_file = None
45+
46+
if data.get("hide"):
47+
assignment.hidden = True
48+
assignment.save()
49+
assignment.make_assignment_dir()
50+
51+
if data.get("copy_graders") and old_assignment.grader_file:
52+
with open(old_assignment.grader_file.path, "rb") as f:
53+
assignment.save_grader_file(f.read())
54+
55+
if data.get("copy_files"):
56+
for _, filename, path, _, _ in old_assignment.list_files():
57+
content = Path(path).read_bytes()
58+
assignment.save_file(content, filename)
59+
60+
self.update_state(
61+
state="PROGRESS",
62+
meta={
63+
"current": index + 1,
64+
"total": total,
65+
"percent": int(((index + 1) / total) * 100) if total > 0 else 100,
66+
},
67+
)
68+
69+
return {"status": "Completed", "total": total}

tin/apps/courses/urls.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,9 @@
2020
path("<int:course_id>/students/manage", views.manage_students_view, name="manage_students"),
2121
path("<int:course_id>/add_period", views.add_period_view, name="add_period"),
2222
path("<int:course_id>/edit_period/<int:period_id>", views.edit_period_view, name="edit_period"),
23+
path(
24+
"<int:course_id>/import/status/<str:task_id>",
25+
views.import_status_view,
26+
name="import_status",
27+
),
2328
]

tin/apps/courses/views.py

Lines changed: 30 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
from datetime import date, timedelta
2-
from pathlib import Path
1+
from datetime import timedelta
32

3+
from celery.result import AsyncResult
4+
from django.http import JsonResponse
45
from django.shortcuts import get_object_or_404, redirect, render
56
from django.utils import timezone
67

@@ -14,6 +15,7 @@
1415
StudentForm,
1516
)
1617
from .models import Course, Period, StudentImport
18+
from .tasks import import_course_data_tasks
1719

1820

1921
# Create your views here.
@@ -178,66 +180,10 @@ def import_from_selected_course(request, course_id, other_course_id):
178180
if request.method == "POST":
179181
form = ImportFromSelectedCourseForm(data=request.POST, course=other_course)
180182
if form.is_valid():
181-
assignments_to_process = []
182-
183-
# Import folders
184-
if form.cleaned_data["folders"]:
185-
for folder in form.cleaned_data["folders"]:
186-
assignments = list(folder.assignments.all())
187-
folder.pk = None
188-
folder._state.adding = True
189-
folder.course = course
190-
folder.save()
191-
for assignment in assignments:
192-
assignments_to_process.append((assignment, course, folder))
193-
194-
# Import assignments
195-
if form.cleaned_data["assignments"]:
196-
for assignment in form.cleaned_data["assignments"]:
197-
assignments_to_process.append((assignment, course, None))
198-
199-
for assignment, course, folder in assignments_to_process:
200-
old_id = assignment.id
201-
202-
# Save as new
203-
assignment.pk = None
204-
assignment._state.adding = True
205-
206-
# Update course, folder, assigned date, and grader file
207-
assignment.course = course
208-
if folder:
209-
assignment.folder = folder
210-
assignment.assigned = timezone.now()
211-
assignment.grader_file = None
212-
213-
# Some user options need to be applied before saving
214-
if form.cleaned_data["hide"]:
215-
assignment.hidden = True
216-
if form.cleaned_data["shift_due_dates"]:
217-
due = assignment.due
218-
try:
219-
assignment.due = due.replace(year=assignment.due.year + 1)
220-
except ValueError: # February 29 -> February 28
221-
assignment.due = due + date(due.year + 1, 3, 1) - date(due.year, 3, 1)
222-
223-
assignment.save()
224-
225-
# Make directory with new ID
226-
assignment.make_assignment_dir()
227-
228-
# Access the old assignment
229-
old_assignment = Assignment.objects.get(id=old_id)
230-
231-
if form.cleaned_data["copy_graders"] and old_assignment.grader_file:
232-
with open(old_assignment.grader_file.path) as f:
233-
assignment.save_grader_file(f.read()) # Save to new directory
234-
235-
if form.cleaned_data["copy_files"]:
236-
for _, filename, path, _, _ in old_assignment.list_files():
237-
content = Path(path).read_bytes()
238-
assignment.save_file(content, filename)
183+
task_data = form.serialize_for_task()
239184

240-
return redirect("courses:show", course.id)
185+
task = import_course_data_tasks.delay(course.id, other_course.id, task_data)
186+
return redirect("courses:import_status", course_id=course.id, task_id=task.id)
241187
else:
242188
form = ImportFromSelectedCourseForm(course=other_course)
243189

@@ -424,3 +370,26 @@ def edit_period_view(request, course_id, period_id):
424370
"courses/edit_create.html",
425371
{"form": form, "course": course, "nav_item": "Edit Period"},
426372
)
373+
374+
375+
@teacher_or_superuser_required
376+
def import_status_view(request, course_id, task_id):
377+
course = get_object_or_404(Course.objects.filter_editable(request.user), id=course_id)
378+
task = AsyncResult(task_id)
379+
380+
if request.headers.get("X-Requested-With") == "XMLHttpRequest":
381+
response_data = {
382+
"state": task.state,
383+
"ready": task.ready(),
384+
}
385+
if task.state == "PROGRESS":
386+
response_data.update(task.info)
387+
elif task.ready():
388+
response_data["result"] = task.result
389+
return JsonResponse(response_data)
390+
391+
return render(
392+
request,
393+
"courses/import_status.html",
394+
{"course": course, "task_id": task_id, "nav_item": "Import Status"},
395+
)
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
{% extends "base.html" %}
2+
{% load static %}
3+
4+
{% block title %}
5+
Turn-In: {{ course.name }}: Import Status
6+
{% endblock %}
7+
8+
{% block main %}
9+
10+
<h2>{{ course.name }}: Import Status</h2>
11+
12+
<div id="status-container">
13+
<p id="status-message">Checking import status...</p>
14+
<div id="progress-container" style="display: none;">
15+
<progress id="progress-bar" value="0" max="100" style="width: 100%;"></progress>
16+
<p id="progress-text"></p>
17+
</div>
18+
</div>
19+
20+
<p id="complete-message" style="display: none;">
21+
<a href="{% url 'courses:show' course.id %}">Return to course</a>
22+
</p>
23+
24+
<script>
25+
$(function() {
26+
function checkStatus() {
27+
$.ajax({
28+
url: window.location.href,
29+
headers: {
30+
"X-Requested-With": "XMLHttpRequest"
31+
},
32+
success: function(data) {
33+
if (data.state === "PROGRESS") {
34+
$("#status-message").text("Importing...");
35+
$("#progress-container").show();
36+
$("#progress-bar").val(data.percent || 0);
37+
$("#progress-text").text("Processing " + (data.current || 0) + " of " + (data.total || 0) + " assignments");
38+
setTimeout(checkStatus, 1000);
39+
} else if (data.state === "PENDING") {
40+
$("#status-message").text("Task pending...");
41+
setTimeout(checkStatus, 1000);
42+
} else if (data.ready) {
43+
$("#status-message").text("Import complete!");
44+
$("#progress-container").hide();
45+
$("#complete-message").show();
46+
} else if (data.state === "FAILURE") {
47+
$("#status-message").text("Import failed. Please try again.");
48+
} else {
49+
setTimeout(checkStatus, 1000);
50+
}
51+
},
52+
error: function(error) {
53+
$("#status-message").text("Error checking status.");
54+
console.error("Error:", error);
55+
}
56+
});
57+
}
58+
59+
checkStatus();
60+
});
61+
</script>
62+
63+
{% endblock %}

0 commit comments

Comments
 (0)