-
Notifications
You must be signed in to change notification settings - Fork 14
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
PoC grades sync #6681
PoC grades sync #6681
Conversation
FAILED = "failed" | ||
|
||
|
||
class GradingSync(CreatedUpdatedMixin, Base): |
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.
Represents a "sync"
status: Mapped[str | None] = varchar_enum(AutoGradingSyncStatus) | ||
|
||
|
||
class GradingSyncGrade(CreatedUpdatedMixin, Base): |
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.
Represents each of the individual grades of a sync
@@ -52,3 +53,21 @@ def calculate_grade( | |||
|
|||
grade = min(100, grade) # Proportional grades are capped at 100% | |||
return round(grade, 2) | |||
|
|||
|
|||
def get_last_synced_grade_for_h_userids(db, h_userids: list[str], assignment_id): |
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.
Query all the previous grades from a list of students. Useful to display it on metrics endpoint.
@@ -87,7 +87,7 @@ def sync_grade( # noqa: PLR0913 | |||
self, | |||
lti_registration: LTIRegistration, | |||
lis_outcome_service_url: str, | |||
grade_timestamp: datetime, | |||
grade_timestamp: str, |
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.
Take the string here directly
GradingSync.status.in_("scheduled", "in_progress"), | ||
) | ||
): | ||
return grading_sync |
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.
TODO we actually need to serialize this.
): | ||
return grading_sync | ||
|
||
return {} |
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.
Maybe a 404?
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.
Yeah, I think we should error here.
if grading_sync := self.db.scalar( | ||
select(GradingSync).where( | ||
GradingSync.assignment_id == assignment.id, | ||
GradingSync.status.in_("scheduled", "in_progress"), |
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.
We should return here the last one, whatever the status is.
renderer="json", | ||
permission=Permissions.GRADE_ASSIGNMENT, | ||
) | ||
def get_auto_grading_sync(self): |
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.
This is the /GET, it returns the last sync.
If it's a completed one, useful to show a "last_synced_at" or similar.
If in progress, useful to report progress.
users = self.request.db.scalars(users_query).all() | ||
|
||
last_sync_grades = {} | ||
if assignment.auto_grading_config: |
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.
Query that last grades synced.
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.
I took a first pass. The logic looks good in general. I just had some questions around the table structure.
sa.Column('lms_user_id', sa.Integer(), nullable=False), | ||
sa.Column('grade', sa.Float(), nullable=True), | ||
sa.Column('extra', postgresql.JSONB(astext_type=sa.Text()), server_default=sa.text("'{}'::jsonb"), nullable=False), | ||
sa.Column('success', sa.Boolean(), nullable=True), |
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.
Would it make sense to track a status
here, like in the parent table? Would we need to know if it's in progress, or just of it succeeded or not?
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.
Yeah we could use status instead of this, in this PR the status is:
Null grade & Null success, scheduled
Null grade & success = False, failed
grade & success = True, succeeded
This is actually simpler:
Null success, scheduled
success = true, succeed
success = false, failed
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.
I don't have a strong opinion here. If you think using a boolean we can infer all required information, fine by me.
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), | ||
sa.Column('grading_sync_id', sa.Integer(), nullable=False), | ||
sa.Column('lms_user_id', sa.Integer(), nullable=False), | ||
sa.Column('grade', sa.Float(), nullable=True), |
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.
Why is grade nullable? Is there some case in which we want to sync with no grade?
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.
Grade is null when we "schedule" the grade sync.
Having the rows in the DB simplifies the queries to know if we are already done, we can just query this table instead of having to store the expected result in the GradeSync table.
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.
Oh, I thought grade
would hold the value that is supposed to be synced when the corresponding task runs. But if I'm getting what you say, this value is set later, once the actual sync with the LMS has succeeded?
If that is the case, how does the celery task know what is the grade that needs to be sent to the LMS?
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.
Yes, you are right, have done a few iterations of this locally and I was describing the previous one.
Grade should not be nullable here
sa.Column('grading_sync_id', sa.Integer(), nullable=False), | ||
sa.Column('lms_user_id', sa.Integer(), nullable=False), | ||
sa.Column('grade', sa.Float(), nullable=True), | ||
sa.Column('extra', postgresql.JSONB(astext_type=sa.Text()), server_default=sa.text("'{}'::jsonb"), nullable=False), |
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.
What is the extra
field for?
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.
Right now it just to store info about the error for failed syncs, we could just name it "error_details" or simlar.
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.
Yeah, I would personally call it error_details
, or at least result_details
, that way it's more clear it's not supposed to be set until the sync has finished, and holds additional details of how it finished and why the status is the one it is.
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.
Went for error_details.
extra: Mapped[JSONB] = mapped_column( | ||
JSONB(), | ||
server_default=text("'{}'::jsonb"), | ||
nullable=False, | ||
) |
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.
Could you add a comment of what is extra for? I think the rest are self-explanatory.
GradingSync.status.in_(["scheduled", "in_progress"]), | ||
) | ||
): | ||
return 409 |
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.
Does this request return an empty body?
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.
Up to us, we could return some basic "grade already in progress"
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.
If we have some standard format for errors, then let's return the most basic one indicating the reason of the error. Otherwise, an empty body should work for now.
fe680b6
to
5b10d03
Compare
Summary of the process:
/POST assignment/grading/sync
scheduled
sync and starts a new job to kick off that task/GET assignment/grading/sync
Once a new sync is scheduled a celery task schedules individual celery task for each grade.
Each finished individual grading either succeeds or fails, it stores its status in the DB.
At the end of each individual sync we check if the whole task is finished and we mark it as so in the DB if that's the case.