Skip to content
Closed
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
22 changes: 19 additions & 3 deletions app/controllers/assignments_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,26 @@ def toggle_enabled
end

if @assignment.update(enabled: params[:enabled])
render json: { success: true }, status: :ok
if request.headers["HX-Request"]
head_with_flash(:ok, :notice, "Assignment updated successfully.")
else
render json: { success: true }, status: :ok
end
else
flash[:alert] = "Failed to update assignment: #{@assignment.errors.full_messages.to_sentence}"
render json: { redirect_to: course_path(course) }, status: :unprocessable_content
error_message = "Failed to update assignment: #{@assignment.errors.full_messages.to_sentence}"
if request.headers["HX-Request"]
head_with_flash(:unprocessable_entity, :alert, error_message)
else
flash[:alert] = error_message
render json: { redirect_to: course_path(course) }, status: :unprocessable_content
end
end
end

private

def head_with_flash(status, flash_type, message)
trigger = { flash: { type: flash_type.to_s, message: message } }.to_json
head status, "HX-Trigger" => trigger
end
end
14 changes: 12 additions & 2 deletions app/controllers/courses_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -72,15 +72,25 @@ def sync_assignments
return render json: { error: 'Course not found.' }, status: :not_found unless @course

@course.sync_assignments(@user)
render json: { message: 'Assignments synced successfully.' }, status: :ok
if request.headers["HX-Request"]
flash[:notice] = "Assignments synced successfully."
head :ok, "HX-Refresh" => "true"
else
render json: { message: 'Assignments synced successfully.' }, status: :ok
end
end

def sync_enrollments
return render json: { error: 'Course not found.' }, status: :not_found unless @course
return render json: { error: 'You do not have permission.' }, status: :forbidden unless @is_course_admin

@course.sync_all_enrollments_from_canvas(@user.id)
render json: { message: 'Users synced successfully.' }, status: :ok
if request.headers["HX-Request"]
flash[:notice] = "Users synced successfully."
head :ok, "HX-Refresh" => "true"
else
render json: { message: 'Users synced successfully.' }, status: :ok
end
end

def enrollments
Expand Down
20 changes: 17 additions & 3 deletions app/controllers/user_to_courses_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,19 @@ def toggle_allow_extended_requests
@enrollment = @course.user_to_courses.find(params[:id])

if @enrollment.update(allow_extended_requests: params[:allow_extended_requests])
render json: { success: true }, status: :ok
if request.headers["HX-Request"]
head_with_flash(:ok, :notice, "Extended requests updated successfully.")
else
render json: { success: true }, status: :ok
end
else
flash[:alert] = "Failed to update enrollment: #{@enrollment.errors.full_messages.to_sentence}"
render json: { redirect_to: course_path(@course) }, status: :unprocessable_content
error_message = "Failed to update enrollment: #{@enrollment.errors.full_messages.to_sentence}"
if request.headers["HX-Request"]
head_with_flash(:unprocessable_entity, :alert, error_message)
else
flash[:alert] = error_message
render json: { redirect_to: course_path(@course) }, status: :unprocessable_content
end
end
end

Expand All @@ -22,4 +31,9 @@ def ensure_course_admin

render json: { error: 'You must be an instructor or Lead TA.', redirect_to: course_path(@course) }, status: :forbidden
end

def head_with_flash(status, flash_type, message)
trigger = { flash: { type: flash_type.to_s, message: message } }.to_json
head status, "HX-Trigger" => trigger
end
end
39 changes: 38 additions & 1 deletion app/javascript/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,41 @@ import "controllers"
import "@rails/ujs"
import "rails-ujs-override"
import "@popperjs/core"
import "bootstrap"
import "bootstrap"
import htmx from "htmx.org"

// Configure HTMX to send the Rails CSRF token with every request
document.addEventListener("htmx:configRequest", (event) => {
const token = document.querySelector('meta[name="csrf-token"]')?.content
if (token) {
event.detail.headers["X-CSRF-Token"] = token
}
})

// Handle HX-Trigger flash events dispatched by the server
document.addEventListener("htmx:afterRequest", (event) => {
const triggerHeader = event.detail.xhr?.getResponseHeader("HX-Trigger")
if (!triggerHeader) return
try {
const triggers = JSON.parse(triggerHeader)
if (triggers.flash) {
const { type, message } = triggers.flash
window.dispatchEvent(new CustomEvent("flash", { detail: { type, message } }))
}
} catch {
// non-JSON trigger header; ignore
}
})

// Roll back checkbox state and show alert on network send errors.
// Note: HTTP response errors (non-2xx) are handled per-element via hx-on::htmx:response-error.
// This handler covers network-level failures (no HTTP response received) only.
document.addEventListener("htmx:sendError", (event) => {
const elt = event.detail.elt
if (elt && elt.type === "checkbox") {
elt.checked = !elt.checked
}
window.dispatchEvent(new CustomEvent("flash", { detail: { type: "alert", message: "Network error. Please try again." } }))
})

window.htmx = htmx
77 changes: 1 addition & 76 deletions app/javascript/controllers/assignment_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,7 @@ import "datatables.net-responsive-bs5";

// Connects to data-controller="assignment"
export default class extends Controller {
static targets = ["checkbox"]
static values = { courseId: Number }

connect() {
this.checkboxTargets.forEach((checkbox) => {
checkbox.addEventListener("change", (event) => this.toggleAssignment(event, checkbox))
})

if (!DataTable.isDataTable('#assignments-table')) {
new DataTable('#assignments-table', {
paging: true,
Expand All @@ -23,72 +16,4 @@ export default class extends Controller {
});
}
}

async toggleAssignment(event, checkbox) {
const assignmentId = checkbox.dataset.assignmentId;
const url = checkbox.dataset.url;
const enabled = checkbox.checked;
const role = checkbox.dataset.role; // Get the role
const userId = checkbox.dataset.userId; // Get the user ID

try {
const token = document.querySelector('meta[name="csrf-token"]').content;

const response = await fetch(url, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
"X-CSRF-Token": token,
},
body: JSON.stringify({
enabled: enabled,
role: role, // Pass the role
user_id: userId, // Pass the user ID
}),
});

const data = await response.json();

if (!response.ok) {
if (data.redirect_to) {
window.location.href = data.redirect_to;
return;
}
throw new Error(data.error || 'Error updating assignment');
}

console.log(`Assignment ${assignmentId} enabled: ${enabled}`);
} catch (error) {
console.error("Error updating assignment:", error);
checkbox.checked = !enabled; // rollback checkbox if error
}
}

sync(event) {
const button = event.currentTarget;
button.disabled = true;
const courseId = this.courseIdValue;
const token = document.querySelector('meta[name="csrf-token"]').content;
fetch(`/courses/${courseId}/sync_assignments`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-Token": token,
},
})
.then((response) => {
if (!response.ok) {
throw new Error("Failed to sync assignments.");
}
return response.json();
})
.then((data) => {
flash("notice", data.message || "Assignments synced successfully.");
location.reload();
})
.catch((error) => {
flash("alert", error.message || "An error occurred while syncing assignments.");
location.reload();
});
}
}
}
73 changes: 1 addition & 72 deletions app/javascript/controllers/enrollments_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@ import "datatables.net-responsive";
import "datatables.net-responsive-bs5";

export default class extends Controller {
static targets = ["checkbox"]
static values = { courseId: Number }

connect() {
if (!DataTable.isDataTable('#enrollments-table')) {
// Define a custom sorting function for the Role column
Expand All @@ -33,73 +30,5 @@ export default class extends Controller {
});
}
}

async toggleExtended(event) {
const checkbox = event.currentTarget;
const url = checkbox.dataset.url;
const allowExtended = checkbox.checked;

try {
const token = document.querySelector('meta[name="csrf-token"]')?.content || '';

const response = await fetch(url, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
"X-CSRF-Token": token,
},
body: JSON.stringify({
allow_extended_requests: allowExtended,
}),
});

const data = await response.json();

if (!response.ok) {
if (data.redirect_to) {
window.location.href = data.redirect_to;
return;
}
throw new Error(data.error || 'Error updating enrollment');
}

const td = checkbox.closest('td');
if (td) td.dataset.order = allowExtended ? '1' : '0';
this._dispatchFlash('notice', `Extended requests ${allowExtended ? 'enabled' : 'disabled'}.`);
} catch (error) {
console.error("Error updating enrollment:", error);
checkbox.checked = !allowExtended;
}
}

_dispatchFlash(type, message) {
window.dispatchEvent(new CustomEvent('flash', { detail: { type: type, message: message } }));
}

sync() {
const button = event.currentTarget;
button.disabled = true;
const courseId = this.courseIdValue;
const token = document.querySelector('meta[name="csrf-token"]').content; fetch(`/courses/${courseId}/sync_enrollments`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-Token": token,
},
})
.then((response) => {
if (!response.ok) {
throw new Error(`Failed to sync enrollments. ${response.status} - ${response.statusText}`);
}
return response.json();
})
.then((data) => {
flash("notice", data.message || "Enrollments synced successfully.");
location.reload();
})
.catch((error) => {
flash("alert", error.message || "An error occurred while syncing enrollments.");
location.reload();
});
}
}

Loading