Skip to content

Commit e557e78

Browse files
authored
Merge pull request #110 from amfoss/develop
Implement status update tracking and improve attendance summary logic
2 parents a0f7725 + 3b313ec commit e557e78

File tree

10 files changed

+127
-110
lines changed

10 files changed

+127
-110
lines changed

.env.sample

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ POSTGRES_DB=
55
POSTGRES_HOST=localhost
66

77
# Root env
8-
DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:5432/${POSTGRES_DB}
8+
ROOT_DB_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:5432/${POSTGRES_DB}
99
RUST_ENV=development
1010
ROOT_SECRET=insecuresecret123 # Used to verify origin of attendance mutations
1111
ROOT_PORT=3000
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
CREATE TABLE StatusUpdateHistory (
2+
update_id SERIAL PRIMARY KEY,
3+
member_id INT REFERENCES Member(member_id) ON DELETE CASCADE,
4+
date DATE NOT NULL,
5+
is_updated BOOLEAN NOT NULL DEFAULT FALSE,
6+
UNIQUE (member_id, date)
7+
);
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- Add migration script here
2+
DROP TABLE if EXISTS AttendanceSummary

src/daily_task/mod.rs

Lines changed: 33 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use chrono::{Datelike, NaiveDate, NaiveTime};
1+
use chrono::NaiveTime;
22
use chrono_tz::Asia::Kolkata;
33
use sqlx::PgPool;
44
use std::sync::Arc;
@@ -44,13 +44,16 @@ async fn execute_daily_task(pool: Arc<PgPool>) {
4444
.await;
4545

4646
match members {
47-
Ok(members) => update_attendance(members, &pool).await,
47+
Ok(members) => {
48+
update_attendance(&members, &pool).await;
49+
update_status_history(&members, &pool).await;
50+
}
4851
// TODO: Handle this
4952
Err(e) => error!("Failed to fetch members: {:?}", e),
5053
};
5154
}
5255

53-
async fn update_attendance(members: Vec<Member>, pool: &PgPool) {
56+
async fn update_attendance(members: &Vec<Member>, pool: &PgPool) {
5457
#[allow(deprecated)]
5558
let today = chrono::Utc::now()
5659
.with_timezone(&Kolkata)
@@ -60,7 +63,7 @@ async fn update_attendance(members: Vec<Member>, pool: &PgPool) {
6063

6164
for member in members {
6265
let attendance = sqlx::query(
63-
"INSERT INTO Attendance (member_id, date, is_present, time_in, time_out)
66+
"INSERT INTO Attendance (member_id, date, is_present, time_in, time_out)
6467
VALUES ($1, $2, $3, $4, $5)
6568
ON CONFLICT (member_id, date) DO NOTHING",
6669
)
@@ -88,115 +91,42 @@ async fn update_attendance(members: Vec<Member>, pool: &PgPool) {
8891
}
8992
// This could have been called in `execute_daily_task()` but that would require us to loop through members twice.
9093
// Whether or not inserting attendance failed, Root will attempt to update AttendanceSummary. This can potentially fail too since insertion failed earlier. However, these two do not depend on each other and one of them failing is no reason to avoid trying the other.
91-
update_attendance_summary(member.member_id, pool).await;
9294
}
9395
}
9496

95-
async fn update_attendance_summary(member_id: i32, pool: &PgPool) {
96-
debug!("Updating summary for member #{}", member_id);
97+
async fn update_status_history(members: &Vec<Member>, pool: &PgPool) {
9798
#[allow(deprecated)]
9899
let today = chrono::Utc::now()
99100
.with_timezone(&Kolkata)
100101
.date()
101102
.naive_local();
102-
let yesterday = today - chrono::Duration::days(1);
103-
104-
let was_present_yesterday = sqlx::query_scalar::<_, bool>(
105-
r#"
106-
SELECT is_present
107-
FROM Attendance
108-
WHERE member_id = $1 AND date = $2
109-
"#,
110-
)
111-
.bind(member_id)
112-
.bind(yesterday)
113-
.fetch_one(pool)
114-
.await;
115-
116-
match was_present_yesterday {
117-
Ok(true) => {
118-
update_days_attended(member_id, today, pool).await;
119-
}
120-
Ok(false) => {
121-
debug!(
122-
"Member ID: {} was absent yesterday, days_attended remains the same.",
123-
member_id
124-
);
125-
}
126-
Err(e) => {
127-
error!("Could not fetch records from DB. Error: {}", e);
128-
}
129-
}
130-
}
103+
debug!("Updating Status Update History on {}", today);
131104

132-
async fn update_days_attended(member_id: i32, today: NaiveDate, pool: &PgPool) {
133-
// Convert year and month into i32 cause SQLx cannot encode u32 into database types
134-
let month: i32 = (today.month0() + 1) as i32;
135-
let year: i32 = today.year_ce().1 as i32;
136-
137-
let existing_days_attended = sqlx::query_scalar::<_, i32>(
138-
r#"
139-
SELECT days_attended
140-
FROM AttendanceSummary
141-
WHERE member_id = $1
142-
AND year = $2
143-
AND month = $3
144-
"#,
145-
)
146-
.bind(member_id)
147-
.bind(year)
148-
.bind(month)
149-
.fetch_optional(pool)
150-
.await;
151-
152-
match existing_days_attended {
153-
Ok(Some(days_attended)) => {
154-
sqlx::query(
155-
r#"
156-
UPDATE AttendanceSummary
157-
SET days_attended = days_attended + 1
158-
WHERE member_id = $1
159-
AND year = $2
160-
AND month = $3
161-
"#,
162-
)
163-
.bind(member_id)
164-
.bind(year)
165-
.bind(month)
166-
.execute(pool)
167-
.await
168-
.unwrap();
169-
170-
debug!(
171-
"Updated days_attended for member ID: {}. New days_attended: {}",
172-
member_id,
173-
days_attended + 1
174-
);
175-
}
176-
Ok(None) => {
177-
sqlx::query(
178-
r#"
179-
INSERT INTO AttendanceSummary (member_id, year, month, days_attended)
180-
VALUES ($1, $2, $3, 1)
181-
"#,
182-
)
183-
.bind(member_id)
184-
.bind(year)
185-
.bind(month)
186-
.execute(pool)
187-
.await
188-
.unwrap();
105+
for member in members {
106+
let status_update = sqlx::query(
107+
"INSERT INTO StatusUpdateHistory (member_id, date, is_updated)
108+
VALUES ($1, $2, $3)
109+
ON CONFLICT (member_id, date) DO NOTHING",
110+
)
111+
.bind(member.member_id)
112+
.bind(today)
113+
.bind(false)
114+
.execute(pool)
115+
.await;
189116

190-
debug!(
191-
"Created new streak for member ID: {} for the month.",
192-
member_id
193-
);
194-
}
195-
Err(e) => {
196-
error!(
197-
"Error checking or updating streak for member ID {}: {:?}",
198-
member_id, e
199-
);
117+
match status_update {
118+
Ok(_) => {
119+
debug!(
120+
"Status update record added for member ID: {}",
121+
member.member_id
122+
);
123+
}
124+
Err(e) => {
125+
error!(
126+
"Failed to insert status update history for member ID: {}: {:?}",
127+
member.member_id, e
128+
);
129+
}
200130
}
201131
}
202132
}

src/graphql/mutations/streak_mutations.rs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ use std::sync::Arc;
33
use async_graphql::{Context, Object, Result};
44
use sqlx::PgPool;
55

6-
use crate::models::status_update_streak::{StatusUpdateStreak as Streak, StreakInput};
6+
use crate::models::status_update::{StatusUpdateStreak as Streak, StreakInput};
7+
use chrono_tz::Asia::Kolkata;
78

89
#[derive(Default)]
910
pub struct StreakMutations;
@@ -30,6 +31,8 @@ impl StreakMutations {
3031

3132
let updated_streak = query.fetch_one(pool.as_ref()).await?;
3233

34+
update_status_history(pool.as_ref(), input.member_id).await?;
35+
3336
Ok(updated_streak)
3437
}
3538

@@ -53,3 +56,27 @@ impl StreakMutations {
5356
Ok(updated_streak)
5457
}
5558
}
59+
60+
async fn update_status_history(pool: &PgPool, member_id: i32) -> Result<()> {
61+
#[allow(deprecated)]
62+
let yesterday = chrono::Utc::now()
63+
.with_timezone(&Kolkata)
64+
.date()
65+
.naive_local()
66+
- chrono::Duration::days(1);
67+
68+
sqlx::query(
69+
"
70+
UPDATE StatusUpdateHistory
71+
SET is_updated = TRUE
72+
WHERE member_id = $1
73+
AND date = $2
74+
",
75+
)
76+
.bind(member_id)
77+
.bind(yesterday)
78+
.execute(pool)
79+
.await?;
80+
81+
Ok(())
82+
}

src/graphql/queries/member_queries.rs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use crate::models::{
66
attendance::{AttendanceInfo, AttendanceSummaryInfo},
77
member::Member,
88
project::Project,
9-
status_update_streak::StatusUpdateStreakInfo,
9+
status_update::StatusUpdateStreakInfo,
1010
};
1111

1212
#[derive(Default)]
@@ -62,7 +62,23 @@ impl Member {
6262
let pool = ctx.data::<Arc<PgPool>>().expect("Pool must be in context.");
6363

6464
sqlx::query_as::<_, AttendanceSummaryInfo>(
65-
"SELECT year, month, days_attended FROM AttendanceSummary WHERE member_id = $1",
65+
"SELECT
66+
to_char(month_date, 'YYYY') AS year,
67+
to_char(month_date, 'MM') AS month,
68+
count(*) AS days_attended
69+
FROM (
70+
SELECT
71+
date_trunc('month', date) AS month_date,
72+
member_id
73+
FROM
74+
attendance
75+
WHERE
76+
is_present = TRUE
77+
AND member_id = $1
78+
) AS monthly_data
79+
GROUP BY
80+
month_date,
81+
member_id;",
6682
)
6783
.bind(self.member_id)
6884
.fetch_all(pool.as_ref())

src/graphql/queries/streak_queries.rs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use std::sync::Arc;
22

3-
use crate::models::status_update_streak::StatusUpdateStreak as Streak;
3+
use crate::models::status_update::StatusUpdateHistory;
4+
use crate::models::status_update::StatusUpdateStreak as Streak;
45
use async_graphql::{Context, Object, Result};
56
use sqlx::PgPool;
67

@@ -29,4 +30,29 @@ impl StreakQueries {
2930
.await?,
3031
)
3132
}
33+
34+
async fn status_update_history_by_member_id(
35+
&self,
36+
ctx: &Context<'_>,
37+
member_id: i32,
38+
) -> Result<StatusUpdateHistory> {
39+
let pool = ctx.data::<Arc<PgPool>>().expect("Pool must be in context.");
40+
41+
Ok(sqlx::query_as::<_, StatusUpdateHistory>(
42+
"SELECT * FROM StatusUpdateHistory WHERE member_id = $1",
43+
)
44+
.bind(member_id)
45+
.fetch_one(pool.as_ref())
46+
.await?)
47+
}
48+
49+
async fn status_update_history(&self, ctx: &Context<'_>) -> Result<Vec<StatusUpdateHistory>> {
50+
let pool = ctx.data::<Arc<PgPool>>().expect("Pool must be in context.");
51+
52+
Ok(
53+
sqlx::query_as::<_, StatusUpdateHistory>("SELECT * FROM StatusUpdateHistory")
54+
.fetch_all(pool.as_ref())
55+
.await?,
56+
)
57+
}
3258
}

src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ impl Config {
3131
Self {
3232
env: std::env::var("ROOT_ENV").unwrap_or_else(|_| "development".to_string()),
3333
secret_key: std::env::var("ROOT_SECRET").expect("ROOT_SECRET must be set."),
34-
database_url: std::env::var("DATABASE_URL").expect("DATABASE_URL must be set."),
34+
database_url: std::env::var("ROOT_DB_URL").expect("ROOT_DB_URL must be set."),
3535
port: std::env::var("ROOT_PORT").expect("ROOT_PORT must be set."),
3636
}
3737
}

src/models/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
pub mod attendance;
22
pub mod member;
33
pub mod project;
4-
pub mod status_update_streak;
4+
pub mod status_update;

src/models/status_update_streak.rs renamed to src/models/status_update.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use async_graphql::{InputObject, SimpleObject};
2+
use chrono::NaiveDate;
23
use sqlx::FromRow;
34

45
#[derive(SimpleObject, FromRow)]
@@ -18,3 +19,11 @@ pub struct StatusUpdateStreakInfo {
1819
pub struct StreakInput {
1920
pub member_id: i32,
2021
}
22+
23+
#[derive(SimpleObject, FromRow)]
24+
pub struct StatusUpdateHistory {
25+
pub update_id: i32,
26+
pub member_id: i32,
27+
pub is_updated: bool,
28+
pub date: NaiveDate,
29+
}

0 commit comments

Comments
 (0)