Skip to content
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

Closed
wants to merge 3 commits into from
Closed

PoC grades sync #6681

wants to merge 3 commits into from

Conversation

marcospri
Copy link
Member

@marcospri marcospri commented Sep 13, 2024

Summary of the process:

  • /POST assignment/grading/sync

    • Returns a 409 if a sync is already in progress
    • Creates a new scheduled sync and starts a new job to kick off that task
  • /GET assignment/grading/sync

    • Returns the last sync we have in the DB, finished or in progress.
  • 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.

FAILED = "failed"


class GradingSync(CreatedUpdatedMixin, Base):
Copy link
Member Author

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):
Copy link
Member Author

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

lms/security.py Show resolved Hide resolved
@@ -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):
Copy link
Member Author

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,
Copy link
Member Author

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
Copy link
Member Author

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 {}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe a 404?

Copy link
Contributor

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"),
Copy link
Member Author

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):
Copy link
Member Author

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:
Copy link
Member Author

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.

Copy link
Contributor

@acelaya acelaya left a 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),
Copy link
Contributor

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?

Copy link
Member Author

@marcospri marcospri Sep 13, 2024

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

Copy link
Contributor

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),
Copy link
Contributor

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?

Copy link
Member Author

@marcospri marcospri Sep 13, 2024

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.

Copy link
Contributor

@acelaya acelaya Sep 13, 2024

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?

Copy link
Member Author

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),
Copy link
Contributor

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?

Copy link
Member Author

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.

Copy link
Contributor

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.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Went for error_details.

Comment on lines +44 to +48
extra: Mapped[JSONB] = mapped_column(
JSONB(),
server_default=text("'{}'::jsonb"),
nullable=False,
)
Copy link
Contributor

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
Copy link
Contributor

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?

Copy link
Member Author

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"

Copy link
Contributor

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.

lms/views/lti/basic_launch.py Show resolved Hide resolved
@marcospri marcospri force-pushed the sync-grades-dashbaord branch from fe680b6 to 5b10d03 Compare September 16, 2024 07:43
Base automatically changed from sync-grades-dashbaord to main September 19, 2024 08:05
@marcospri marcospri closed this Sep 20, 2024
@marcospri marcospri deleted the sync-grades-poc branch November 13, 2024 10:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants