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

Migrate Grades v2 to Next.js #795

Draft
wants to merge 23 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
ccb859a
Bootstrap rust application
junlarsen Feb 9, 2024
3d13526
Implement crawling of all faculties
junlarsen Feb 9, 2024
7c69bfe
Implement synchronization of departments
junlarsen Feb 9, 2024
dd78366
Chunk jobs across multiple threads
junlarsen Feb 9, 2024
b996bb5
Ship MVP version of the Rust syncer
junlarsen Feb 9, 2024
fedb20b
Fix group by chunking
junlarsen Feb 10, 2024
8f623bf
Replace theading with futures join_all
junlarsen Feb 10, 2024
869f99d
Synchronize everything since 2004
junlarsen Feb 10, 2024
7588bec
Bootstrap Next.js application
junlarsen Feb 7, 2024
63bfc1c
Implement hkdir query for subjects and departments
junlarsen Feb 7, 2024
4ce4b11
Implement fetcing subject grdaes
junlarsen Feb 7, 2024
0dd8efa
Rename trpc to server
junlarsen Feb 7, 2024
e5de81e
Populate faculty names into database
junlarsen Feb 7, 2024
d72033b
Refactor faculty population into job service
junlarsen Feb 7, 2024
87d02ef
Add logging to subject sync job
junlarsen Feb 7, 2024
2929ef6
Speed up subject synchronization with async priority queue
junlarsen Feb 7, 2024
5b03125
Fetch departments in parallel as well
junlarsen Feb 7, 2024
f7c9d75
Prevent duplicate write on subject grades
junlarsen Feb 8, 2024
185621a
Query most popular or filtered subjects
junlarsen Feb 8, 2024
ad52270
Build average grade for subject while populating
junlarsen Feb 8, 2024
ea0f1ff
Short circuit grades without any registrations
junlarsen Feb 8, 2024
63b1903
Fix parameters
junlarsen Feb 9, 2024
332cbdb
Ship Rust synchronizer
junlarsen Feb 10, 2024
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
2,355 changes: 2,355 additions & 0 deletions apps/grades-sync/Cargo.lock

Large diffs are not rendered by default.

21 changes: 21 additions & 0 deletions apps/grades-sync/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[package]
name = "grades-sync"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
sqlx = { version = "0.7", features = ["runtime-tokio", "tls-rustls", "uuid", "json", "migrate", "postgres"] }
tokio = { version = "1", features = ["full"] }
reqwest = { version = "0.11", features = ["json"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
anyhow = "1.0.79"
dotenv = "0.15.0"
log = { version = "0.4.20", features = [] }
env_logger = "0.11.1"
async-trait = "0.1.77"
regex = "1.10.3"
itertools = "0.12.1"
futures = "0.3.30"
11 changes: 11 additions & 0 deletions apps/grades-sync/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Grades Sync

Asynchronous grades synchronizer against HKDir's API.

## Environment variables

```env
DATABASE_URL=postgres://...
```

The `RUST_LOG` variable can be set to `info` or `debug` to enable logging.
25 changes: 25 additions & 0 deletions apps/grades-sync/migrations/20240209190449_schema.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
START TRANSACTION;

CREATE EXTENSION IF NOT EXISTS "fuzzystrmatch";

CREATE TABLE IF NOT EXISTS faculty
(
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
ref_id TEXT NOT NULL,
name TEXT NOT NULL,

CONSTRAINT faculty_ref_id_unique UNIQUE (ref_id)
);

CREATE TABLE IF NOT EXISTS department
(
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
ref_id TEXT NOT NULL,
faculty_id UUID NOT NULL,
name TEXT NOT NULL,

CONSTRAINT department_fk_faculty_id FOREIGN KEY (faculty_id) REFERENCES faculty (id),
CONSTRAINT department_uq_ref_id UNIQUE (ref_id)
);

COMMIT;
23 changes: 23 additions & 0 deletions apps/grades-sync/migrations/20240209195028_subject.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
BEGIN TRANSACTION;

CREATE TABLE IF NOT EXISTS subject
(
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
ref_id TEXT NOT NULL,
name TEXT NOT NULL,
slug TEXT NOT NULL,
department_id UUID NOT NULL,

instruction_language TEXT NOT NULL,
educational_level TEXT NOT NULL,
credits REAL NOT NULL,

average_grade REAL NOT NULL DEFAULT 0.0,
total_students INT NOT NULL DEFAULT 0,
failed_students INT NOT NULL DEFAULT 0,

CONSTRAINT subject_fk_department_id FOREIGN KEY (department_id) REFERENCES department (id),
CONSTRAINT subject_uq_ref_id UNIQUE (ref_id)
);

COMMIT;
25 changes: 25 additions & 0 deletions apps/grades-sync/migrations/20240209212106_grades.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
START TRANSACTION;

CREATE TYPE subject_grading_season AS ENUM ('WINTER', 'SPRING', 'SUMMER', 'AUTUMN');

CREATE TABLE IF NOT EXISTS subject_season_grade
(
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
subject_id UUID NOT NULL,
season subject_grading_season NOT NULL,
year INTEGER NOT NULL,

graded_a INTEGER,
graded_b INTEGER,
graded_c INTEGER,
graded_d INTEGER,
graded_e INTEGER,
graded_f INTEGER,
graded_pass INTEGER,
graded_fail INTEGER,

CONSTRAINT subject_season_grade_fk_subject_id FOREIGN KEY (subject_id) REFERENCES subject (id),
CONSTRAINT subject_season_grade_unique UNIQUE (subject_id, season, year)
);

COMMIT;
39 changes: 39 additions & 0 deletions apps/grades-sync/queries/departments.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"tabell_id": 210,
"api_versjon": 1,
"statuslinje": "N",
"kodetekst": "J",
"desimal_separator": ".",
"variabler": [
"*"
],
"sortBy": [
"Nivå"
],
"filter": [
{
"variabel": "Institusjonskode",
"selection": {
"filter": "item",
"values": [
"1150"
],
"exclude": [
""
]
}
},
{
"variabel": "Avdelingskode",
"selection": {
"filter": "all",
"values": [
"*"
],
"exclude": [
"000000"
]
}
}
]
}
55 changes: 55 additions & 0 deletions apps/grades-sync/queries/grades.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{
"tabell_id": 308,
"api_versjon": 1,
"statuslinje": "N",
"kodetekst": "J",
"desimal_separator": ".",
"variabler": [
"*"
],
"groupBy": [
"Årstall",
"Semester",
"Karakter",
"Emnekode",
"Institusjonskode"
],
"filter": [
{
"variabel": "Institusjonskode",
"selection": {
"filter": "item",
"values": [
"1150"
],
"exclude": [
""
]
}
},
{
"variabel": "Emnekode",
"selection": {
"filter": "all",
"values": [
"*"
],
"exclude": [
""
]
}
},
{
"variabel": "Semester",
"selection": {
"filter": "all",
"values": [
"*"
],
"exclude": [
""
]
}
}
]
}
67 changes: 67 additions & 0 deletions apps/grades-sync/queries/subjects.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
{
"tabell_id": 208,
"api_versjon": 1,
"statuslinje": "N",
"kodetekst": "J",
"desimal_separator": ".",
"variabler": [
"*"
],
"sortBy": [
"Årstall",
"Institusjonskode",
"Avdelingskode"
],
"filter": [
{
"variabel": "Institusjonskode",
"selection": {
"filter": "item",
"values": [
"1150"
],
"exclude": [
""
]
}
},
{
"variabel": "Nivåkode",
"selection": {
"filter": "item",
"values": [
"HN",
"LN"
],
"exclude": [
""
]
}
},
{
"variabel": "Avdelingskode",
"selection": {
"filter": "all",
"values": [
"*"
],
"exclude": [
"000000"
]
}
},
{
"variabel": "Oppgave (ny fra h2012)",
"selection": {
"filter": "all",
"values": [
"*"
],
"exclude": [
"1",
"2"
]
}
}
]
}
67 changes: 67 additions & 0 deletions apps/grades-sync/src/department_repository.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
use crate::pg::Database;
use async_trait::async_trait;
use sqlx::types::Uuid;
use sqlx::FromRow;

#[derive(Debug, FromRow)]
pub struct Department {
pub id: Uuid,
pub name: String,
pub ref_id: String,
pub faculty_id: Uuid,
}

#[async_trait]
pub trait DepartmentRepository: Sync {
async fn create_department(
&self,
name: String,
ref_id: String,
faculty_id: Uuid,
) -> Result<Department, sqlx::Error>;
async fn get_department_by_ref_id(&self, ref_id: String) -> Result<Department, sqlx::Error>;
}

pub struct DepartmentRepositoryImpl<'a> {
db: &'a Database,
}

impl<'a> DepartmentRepositoryImpl<'a> {
pub fn new(db: &'a Database) -> Self {
Self { db }
}
}

#[async_trait]
impl<'a> DepartmentRepository for DepartmentRepositoryImpl<'a> {
async fn create_department(
&self,
name: String,
ref_id: String,
faculty_id: Uuid,
) -> Result<Department, sqlx::Error> {
sqlx::query_as::<_, Department>(
r#"
INSERT INTO department (name, ref_id, faculty_id) VALUES ($1, $2, $3)
ON CONFLICT (ref_id) DO UPDATE SET name = $1, ref_id = $2, faculty_id = $3
RETURNING *;
"#,
)
.bind(name)
.bind(ref_id)
.bind(faculty_id)
.fetch_one(self.db)
.await
}

async fn get_department_by_ref_id(&self, ref_id: String) -> Result<Department, sqlx::Error> {
sqlx::query_as::<_, Department>(
r#"
SELECT * FROM department WHERE ref_id = $1;
"#,
)
.bind(ref_id)
.fetch_one(self.db)
.await
}
}
43 changes: 43 additions & 0 deletions apps/grades-sync/src/faculty_repository.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
use crate::pg::Database;
use async_trait::async_trait;
use sqlx::types::Uuid;
use sqlx::FromRow;

#[derive(Debug, FromRow)]
pub struct Faculty {
pub id: Uuid,
pub name: String,
pub ref_id: String,
}

#[async_trait]
pub trait FacultyRepository: Sync {
async fn create_faculty(&self, name: String, ref_id: String) -> Result<Faculty, sqlx::Error>;
}

pub struct FacultyRepositoryImpl<'a> {
db: &'a Database,
}

impl<'a> FacultyRepositoryImpl<'a> {
pub fn new(db: &'a Database) -> Self {
Self { db }
}
}

#[async_trait]
impl<'a> FacultyRepository for FacultyRepositoryImpl<'a> {
async fn create_faculty(&self, name: String, ref_id: String) -> Result<Faculty, sqlx::Error> {
sqlx::query_as::<_, Faculty>(
r#"
INSERT INTO faculty (name, ref_id) VALUES ($1, $2)
ON CONFLICT (ref_id) DO UPDATE SET name = $1, ref_id = $2
RETURNING *;
"#,
)
.bind(name)
.bind(ref_id)
.fetch_one(self.db)
.await
}
}
Loading
Loading