-
Notifications
You must be signed in to change notification settings - Fork 67
Versioning and History #1346
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Versioning and History #1346
Changes from 13 commits
d50ca6b
dabb1b3
1cfe018
538bac8
9d22700
6d72196
9f53081
7e9c3c3
c513c13
85f309b
24abca4
129c981
970ed88
4e3254f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,7 +20,16 @@ defmodule Cadet.Assessments do | |
| CourseRegistrations | ||
| } | ||
|
|
||
| alias Cadet.Assessments.{Answer, Assessment, Query, Question, Submission, SubmissionVotes} | ||
| alias Cadet.Assessments.{ | ||
| Answer, | ||
| Assessment, | ||
| Query, | ||
| Question, | ||
| Submission, | ||
| SubmissionVotes, | ||
| Version | ||
| } | ||
|
|
||
| alias Cadet.Autograder.GradingJob | ||
| alias Cadet.Courses.{Group, AssessmentConfig} | ||
| alias Cadet.Jobs.Log | ||
|
|
@@ -1284,7 +1293,8 @@ defmodule Cadet.Assessments do | |
| with {:ok, _team} <- find_team(question.assessment.id, cr_id), | ||
| {:ok, submission} <- find_or_create_submission(cr, question.assessment), | ||
| {:status, true} <- {:status, force_submit or submission.status != :submitted}, | ||
| {:ok, _answer} <- insert_or_update_answer(submission, question, raw_answer, cr_id) do | ||
| {:ok, answer} <- insert_or_update_answer(submission, question, raw_answer, cr_id), | ||
| {:ok, _version} <- insert_version(question, answer, raw_answer) do | ||
| Logger.info("Successfully answered question #{question.id} for user #{cr_id}") | ||
| update_submission_status_router(submission, question) | ||
|
|
||
|
|
@@ -3610,4 +3620,150 @@ defmodule Cadet.Assessments do | |
|
|
||
| Repo.one(query) | ||
| end | ||
|
|
||
| def get_version( | ||
| question = %Question{}, | ||
| cr = %CourseRegistration{} | ||
| ) do | ||
| {:ok, team} = find_team(question.assessment.id, cr.id) | ||
|
|
||
| case team do | ||
| %Team{} -> | ||
| Version | ||
| |> join(:inner, [v], a in assoc(v, :answer)) | ||
| |> join(:inner, [v, a], s in assoc(a, :submission)) | ||
| |> where([v, a, s], a.question_id == ^question.id) | ||
| |> where([v, a, s], s.team_id == ^team.id) | ||
| |> Repo.all() | ||
|
|
||
| nil -> | ||
| Version | ||
| |> join(:inner, [v], a in assoc(v, :answer)) | ||
| |> join(:inner, [v, a], s in assoc(a, :submission)) | ||
| |> where([v, a, s], a.question_id == ^question.id) | ||
| |> where([v, a, s], s.student_id == ^cr.id) | ||
| |> Repo.all() | ||
| end | ||
| end | ||
|
||
|
|
||
| def save_version( | ||
| question = %Question{}, | ||
| cr = %CourseRegistration{id: cr_id}, | ||
| raw_version | ||
| ) do | ||
| if question.type == :voting do | ||
| {:error, {:bad_request, "Cannot save version for voting question"}} | ||
| end | ||
|
|
||
| with {:ok, _team} <- find_team(question.assessment.id, cr_id), | ||
| {:ok, submission} <- find_or_create_submission(cr, question.assessment), | ||
| {:ok, answer} <- find_or_create_answer(question, submission, raw_version), | ||
| {:ok, _version} <- insert_version(question, answer, raw_version) do | ||
| Logger.info("Successfully saved version for answer #{question.id} for user #{cr_id}") | ||
|
|
||
| # placeholder | ||
| {:ok, nil} | ||
| else | ||
| {:error, :team_not_found} -> | ||
| Logger.error("Team not found for question #{question.id} and user #{cr_id}") | ||
| {:error, {:bad_request, "Your existing Team has been deleted!"}} | ||
| end | ||
|
|
||
| {:ok, nil} | ||
| end | ||
|
Comment on lines
+3655
to
+3678
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There are a couple of critical bugs in this function's control flow:
The function should be restructured to ensure correct control flow and error handling. |
||
|
|
||
| defp find_or_create_answer( | ||
| question = %Question{}, | ||
| submission = %Submission{}, | ||
| raw_version | ||
| ) do | ||
| case find_answer(question, submission) do | ||
| {:ok, answer} -> {:ok, answer} | ||
| {:error, _} -> create_new_answer(question, submission, raw_version) | ||
| end | ||
| end | ||
|
|
||
| defp find_answer(question = %Question{}, submission = %Submission{}) do | ||
| answer = | ||
| Answer | ||
| |> where(submission_id: ^submission.id) | ||
| |> where(question_id: ^question.id) | ||
| |> Repo.one() | ||
|
|
||
| if answer do | ||
| {:ok, answer} | ||
| else | ||
| {:error, nil} | ||
| end | ||
| end | ||
|
|
||
| defp create_new_answer( | ||
| question = %Question{}, | ||
| submission = %Submission{}, | ||
| raw_version | ||
| ) do | ||
| answer_content = build_answer_content(raw_version, question.type) | ||
|
|
||
| %Answer{} | ||
| |> Answer.changeset(%{ | ||
| answer: answer_content, | ||
| question_id: question.id, | ||
| submission_id: submission.id, | ||
| type: question.type | ||
| }) | ||
| |> Repo.insert() | ||
| end | ||
|
|
||
| defp insert_version( | ||
| question = %Question{}, | ||
| answer = %Answer{}, | ||
| raw_version | ||
| ) do | ||
| version_content = build_answer_content(raw_version, question.type) | ||
|
|
||
| %Version{} | ||
| |> Version.changeset(%{ | ||
| version: version_content, | ||
| answer_id: answer.id | ||
| }) | ||
| |> Repo.insert() | ||
| end | ||
|
|
||
| def name_version( | ||
| question = %Question{}, | ||
| cr = %CourseRegistration{id: cr_id}, | ||
| version_id, | ||
| name | ||
| ) do | ||
| {:ok, team} = find_team(question.assessment.id, cr.id) | ||
|
|
||
| version = | ||
| case team do | ||
| %Team{} -> | ||
| Version | ||
| |> join(:inner, [v], a in assoc(v, :answer)) | ||
| |> join(:inner, [v, a], s in assoc(a, :submission)) | ||
| |> where([v, a, s], v.id == ^version_id) | ||
| |> where([v, a, s], s.team_id == ^team.id) | ||
| |> Repo.one() | ||
|
|
||
| nil -> | ||
| Version | ||
| |> join(:inner, [v], a in assoc(v, :answer)) | ||
| |> join(:inner, [v, a], s in assoc(a, :submission)) | ||
| |> where([v, a, s], v.id == ^version_id) | ||
| |> where([v, a, s], s.student_id == ^cr.id) | ||
| |> Repo.one() | ||
| end | ||
|
|
||
| case version do | ||
| nil -> | ||
| {:error, {:not_found, "Version not found"}} | ||
|
|
||
| version -> | ||
| version | ||
| |> Version.changeset(%{name: name}) | ||
| |> Repo.update() | ||
| end | ||
| end | ||
|
Comment on lines
+3737
to
+3780
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This function has similar issues to
This should be refactored to handle the error and reduce duplication. |
||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| defmodule Cadet.Assessments.Version do | ||
| use Ecto.Schema | ||
| import Ecto.Changeset | ||
|
|
||
| alias Cadet.Assessments.Answer | ||
|
|
||
| schema "versions" do | ||
| field(:version, :map) | ||
| field(:name, :string) | ||
| field(:restored, :boolean, default: false) | ||
| field(:restored_from, :id) | ||
|
|
||
| belongs_to(:answer, Answer) | ||
|
|
||
| timestamps() | ||
| end | ||
|
|
||
| @doc false | ||
| def changeset(version, attrs) do | ||
| version | ||
| |> cast(attrs, [:version, :name, :restored, :answer_id]) | ||
| |> validate_required([:version, :restored, :answer_id]) | ||
| end | ||
| end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| defmodule CadetWeb.VersionsController do | ||
| @moduledoc """ | ||
| Handles code versioning and history | ||
| """ | ||
| use CadetWeb, :controller | ||
| use PhoenixSwagger | ||
| require Logger | ||
|
|
||
| alias Cadet.Assessments | ||
|
|
||
| def history(conn, %{"questionid" => question_id}) do | ||
| course_reg = conn.assigns[:course_reg] | ||
|
|
||
| Logger.info( | ||
| "Fetching all versions for question #{question_id} for user #{course_reg.id} in course #{course_reg.course_id}" | ||
| ) | ||
|
|
||
| with {:question, question} when not is_nil(question) <- | ||
| {:question, Assessments.get_question(question_id)}, | ||
| {:versions, versions} <- | ||
| {:versions, Assessments.get_version(question, course_reg)} do | ||
| conn | ||
| |> put_status(:ok) | ||
| |> put_resp_content_type("application/json") | ||
| |> render("index.json", versions: versions) | ||
| else | ||
| {:question, nil} -> | ||
| conn | ||
| |> put_status(:not_found) | ||
| |> text("Question not found") | ||
| end | ||
|
Comment on lines
+11
to
+42
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This action doesn't handle potential errors from After refactoring |
||
| end | ||
|
|
||
| def save(conn, %{"questionid" => question_id, "version" => version}) do | ||
| course_reg = conn.assigns[:course_reg] | ||
|
|
||
| with {:question, question} when not is_nil(question) <- | ||
| {:question, Assessments.get_question(question_id)}, | ||
| {:ok, _nil} <- Assessments.save_version(question, course_reg, version) do | ||
| text(conn, "OK") | ||
| else | ||
| {:question, nil} -> | ||
| conn | ||
| |> put_status(:not_found) | ||
| |> text("Question not found") | ||
|
|
||
| {:error, {status, message}} -> | ||
| conn | ||
| |> put_status(status) | ||
| |> text(message) | ||
| end | ||
| end | ||
|
|
||
| def name(conn, %{ | ||
| "questionid" => question_id, | ||
| "versionid" => version_id, | ||
| "name" => name | ||
| }) do | ||
| course_reg = conn.assigns[:course_reg] | ||
|
|
||
| with {:question, question} when not is_nil(question) <- | ||
| {:question, Assessments.get_question(question_id)}, | ||
| {:ok, _nil} <- Assessments.name_version(question, course_reg, version_id, name) do | ||
| text(conn, "OK") | ||
| else | ||
| {:question, nil} -> | ||
| conn | ||
| |> put_status(:not_found) | ||
| |> text("Question not found") | ||
|
|
||
| {:error, {status, message}} -> | ||
| conn | ||
| |> put_status(status) | ||
| |> text(message) | ||
| end | ||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| defmodule CadetWeb.VersionsView do | ||
| use CadetWeb, :view | ||
|
|
||
| def render("index.json", %{versions: versions}) do | ||
| render_many(versions, CadetWeb.VersionsView, "show.json", as: :version) | ||
| end | ||
|
|
||
| def render("show.json", %{version: version}) do | ||
| transform_map_for_view(version, %{ | ||
| id: :id, | ||
| name: :name, | ||
| restored: :restored, | ||
| restored_from: :restored_from, | ||
| answer_id: :answer_id, | ||
| inserted_at: :inserted_at, | ||
| updated_at: :updated_at, | ||
| version: :version | ||
| }) | ||
| end | ||
| end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -108,6 +108,13 @@ defmodule Mix.Tasks.Cadet.Token do | |
| course_id: course.id, | ||
| role: role | ||
| }) | ||
|
|
||
| %User{} | ||
| |> User.changeset(%{ | ||
| name: "Test#{role_capitalized}", | ||
| username: "test_#{role}", | ||
| provider: "test" | ||
| }) | ||
|
Comment on lines
+111
to
+117
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ? What is this for? |
||
| |> Repo.insert!() | ||
|
|
||
| new_user | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| defmodule Cadet.Repo.Migrations.CreateVersions do | ||
| use Ecto.Migration | ||
| import Ecto.Query, only: [from: 2] | ||
|
|
||
| def change do | ||
| create table(:versions) do | ||
| add(:version, :map) | ||
|
||
| add(:name, :string) | ||
| add(:restored, :boolean, default: false, null: false) | ||
| add(:answer_id, references(:answers, on_delete: :delete_all)) | ||
| add(:restored_from, references(:versions, on_delete: :nothing)) | ||
|
|
||
| timestamps() | ||
| end | ||
|
|
||
| create(index(:versions, [:answer_id])) | ||
| create(index(:versions, [:restored_from])) | ||
|
|
||
| # Backfill data from answers table | ||
| execute(fn -> | ||
|
||
| answers = | ||
| from(a in "answers", | ||
| select: %{ | ||
| id: a.id, | ||
| answer: a.answer, | ||
| inserted_at: a.inserted_at, | ||
| updated_at: a.updated_at | ||
| }, | ||
| join: q in "questions", | ||
| on: q.id == a.question_id, | ||
| where: q.type != "voting" | ||
| ) | ||
| |> repo().all() | ||
|
|
||
| versions = | ||
| answers | ||
| |> Enum.map(fn a -> | ||
| %{ | ||
| answer_id: a.id, | ||
| version: a.answer, | ||
| inserted_at: a.inserted_at, | ||
| updated_at: a.updated_at | ||
| } | ||
| end) | ||
|
|
||
| repo().insert_all("versions", versions) | ||
| end) | ||
| end | ||
| end | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For clarity and to follow Elixir/Ecto conventions,
has_manyassociations should use a plural name. The association toVersionshould be named:versions.