Skip to content

Commit 3dc6074

Browse files
committed
Auto merge of #1901 - DSpeckhals:email-preference-storing, r=carols10cents
backend: Add user email preference storing This PR addresses the first step in implementing #1895: "Backend: email preference storing." This commit is the first step in the process to send crate owners an email notification when a new version of one of their crates is published. A database migration adds a `email_notifications` column to the `crate_owners` table, and thus the property was added to the corresponding `CrateOwner` struct. This new property is defaulted to `true`. Because a user may not want to receive a version publish notification for all of their crates, an API endpoint was added to allow them to toggle email notifications for each crate. The front end implementation will be in a forthcoming commit, as well as the actual sending of these notifications.
2 parents 9042ebf + 2468eab commit 3dc6074

File tree

14 files changed

+274
-24
lines changed

14 files changed

+274
-24
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ALTER TABLE crate_owners DROP COLUMN email_notifications;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ALTER TABLE crate_owners ADD COLUMN email_notifications BOOLEAN NOT NULL DEFAULT TRUE;

src/controllers/crate_owner_invitation.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ fn accept_invite(
6969
owner_id: user_id,
7070
created_by: pending_crate_owner.invited_by_user_id,
7171
owner_kind: OwnerKind::User as i32,
72+
email_notifications: true,
7273
})
7374
.on_conflict(crate_owners::table.primary_key())
7475
.do_update()

src/controllers/krate/search.rs

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use diesel_full_text_search::*;
55

66
use crate::controllers::helpers::Paginate;
77
use crate::controllers::prelude::*;
8-
use crate::models::{Crate, CrateBadge, CrateVersions, OwnerKind, Version};
8+
use crate::models::{Crate, CrateBadge, CrateOwner, CrateVersions, OwnerKind, Version};
99
use crate::schema::*;
1010
use crate::views::EncodableCrate;
1111

@@ -118,21 +118,17 @@ pub fn search(req: &mut dyn Request) -> CargoResult<Response> {
118118
} else if let Some(user_id) = params.get("user_id").and_then(|s| s.parse::<i32>().ok()) {
119119
query = query.filter(
120120
crates::id.eq_any(
121-
crate_owners::table
121+
CrateOwner::by_owner_kind(OwnerKind::User)
122122
.select(crate_owners::crate_id)
123-
.filter(crate_owners::owner_id.eq(user_id))
124-
.filter(crate_owners::deleted.eq(false))
125-
.filter(crate_owners::owner_kind.eq(OwnerKind::User as i32)),
123+
.filter(crate_owners::owner_id.eq(user_id)),
126124
),
127125
);
128126
} else if let Some(team_id) = params.get("team_id").and_then(|s| s.parse::<i32>().ok()) {
129127
query = query.filter(
130128
crates::id.eq_any(
131-
crate_owners::table
129+
CrateOwner::by_owner_kind(OwnerKind::Team)
132130
.select(crate_owners::crate_id)
133-
.filter(crate_owners::owner_id.eq(team_id))
134-
.filter(crate_owners::deleted.eq(false))
135-
.filter(crate_owners::owner_kind.eq(OwnerKind::Team as i32)),
131+
.filter(crate_owners::owner_id.eq(team_id)),
136132
),
137133
);
138134
} else if params.get("following").is_some() {

src/controllers/user/me.rs

Lines changed: 80 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1+
use std::collections::HashMap;
2+
13
use crate::controllers::prelude::*;
24

35
use crate::controllers::helpers::*;
46
use crate::email;
57
use crate::util::bad_request;
68
use crate::util::errors::CargoError;
79

8-
use crate::models::{Email, Follow, NewEmail, User, Version};
9-
use crate::schema::{crates, emails, follows, users, versions};
10-
use crate::views::{EncodableMe, EncodableVersion};
10+
use crate::models::{CrateOwner, Email, Follow, NewEmail, OwnerKind, User, Version};
11+
use crate::schema::{crate_owners, crates, emails, follows, users, versions};
12+
use crate::views::{EncodableMe, EncodableVersion, OwnedCrate};
1113

1214
/// Handles the `GET /me` route.
1315
pub fn me(req: &mut dyn Request) -> CargoResult<Response> {
@@ -22,11 +24,11 @@ pub fn me(req: &mut dyn Request) -> CargoResult<Response> {
2224
// perhaps adding `req.mut_extensions().insert(user)` to the
2325
// update_user route, however this somehow does not seem to work
2426

25-
let id = req.user()?.id;
27+
let user_id = req.user()?.id;
2628
let conn = req.db_conn()?;
2729

2830
let (user, verified, email, verification_sent) = users::table
29-
.find(id)
31+
.find(user_id)
3032
.left_join(emails::table)
3133
.select((
3234
users::all_columns,
@@ -36,12 +38,28 @@ pub fn me(req: &mut dyn Request) -> CargoResult<Response> {
3638
))
3739
.first::<(User, Option<bool>, Option<String>, bool)>(&*conn)?;
3840

41+
let owned_crates = crate_owners::table
42+
.inner_join(crates::table)
43+
.filter(crate_owners::owner_id.eq(user_id))
44+
.filter(crate_owners::owner_kind.eq(OwnerKind::User as i32))
45+
.select((crates::id, crates::name, crate_owners::email_notifications))
46+
.order(crates::name.asc())
47+
.load(&*conn)?
48+
.into_iter()
49+
.map(|(id, name, email_notifications)| OwnedCrate {
50+
id,
51+
name,
52+
email_notifications,
53+
})
54+
.collect();
55+
3956
let verified = verified.unwrap_or(false);
4057
let verification_sent = verified || verification_sent;
4158
let user = User { email, ..user };
4259

4360
Ok(req.json(&EncodableMe {
4461
user: user.encodable_private(verified, verification_sent),
62+
owned_crates,
4563
}))
4664
}
4765

@@ -212,3 +230,60 @@ pub fn regenerate_token_and_send(req: &mut dyn Request) -> CargoResult<Response>
212230
}
213231
Ok(req.json(&R { ok: true }))
214232
}
233+
234+
/// Handles `PUT /me/email_notifications` route
235+
pub fn update_email_notifications(req: &mut dyn Request) -> CargoResult<Response> {
236+
use self::crate_owners::dsl::*;
237+
use diesel::pg::upsert::excluded;
238+
239+
#[derive(Deserialize)]
240+
struct CrateEmailNotifications {
241+
id: i32,
242+
email_notifications: bool,
243+
}
244+
245+
let mut body = String::new();
246+
req.body().read_to_string(&mut body)?;
247+
let updates: HashMap<i32, bool> = serde_json::from_str::<Vec<CrateEmailNotifications>>(&body)
248+
.map_err(|_| bad_request("invalid json request"))?
249+
.iter()
250+
.map(|c| (c.id, c.email_notifications))
251+
.collect();
252+
253+
let user = req.user()?;
254+
let conn = req.db_conn()?;
255+
256+
// Build inserts from existing crates beloning to the current user
257+
let to_insert = CrateOwner::by_owner_kind(OwnerKind::User)
258+
.filter(owner_id.eq(user.id))
259+
.select((crate_id, owner_id, owner_kind, email_notifications))
260+
.load(&*conn)?
261+
.into_iter()
262+
// Remove records whose `email_notifications` will not change from their current value
263+
.map(
264+
|(c_id, o_id, o_kind, e_notifications): (i32, i32, i32, bool)| {
265+
let current_e_notifications = *updates.get(&c_id).unwrap_or(&e_notifications);
266+
(
267+
crate_id.eq(c_id),
268+
owner_id.eq(o_id),
269+
owner_kind.eq(o_kind),
270+
email_notifications.eq(current_e_notifications),
271+
)
272+
},
273+
)
274+
.collect::<Vec<_>>();
275+
276+
// Upsert crate owners; this should only actually exectute updates
277+
diesel::insert_into(crate_owners)
278+
.values(&to_insert)
279+
.on_conflict((crate_id, owner_id, owner_kind))
280+
.do_update()
281+
.set(email_notifications.eq(excluded(email_notifications)))
282+
.execute(&*conn)?;
283+
284+
#[derive(Serialize)]
285+
struct R {
286+
ok: bool,
287+
}
288+
Ok(req.json(&R { ok: true }))
289+
}

src/models/krate.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ impl<'a> NewCrate<'a> {
189189
owner_id: user_id,
190190
created_by: user_id,
191191
owner_kind: OwnerKind::User as i32,
192+
email_notifications: true,
192193
};
193194
diesel::insert_into(crate_owners::table)
194195
.values(&owner)
@@ -394,18 +395,17 @@ impl Crate {
394395
}
395396

396397
pub fn owners(&self, conn: &PgConnection) -> CargoResult<Vec<Owner>> {
397-
let base_query = CrateOwner::belonging_to(self).filter(crate_owners::deleted.eq(false));
398-
let users = base_query
398+
let users = CrateOwner::by_owner_kind(OwnerKind::User)
399+
.filter(crate_owners::crate_id.eq(self.id))
399400
.inner_join(users::table)
400401
.select(users::all_columns)
401-
.filter(crate_owners::owner_kind.eq(OwnerKind::User as i32))
402402
.load(conn)?
403403
.into_iter()
404404
.map(Owner::User);
405-
let teams = base_query
405+
let teams = CrateOwner::by_owner_kind(OwnerKind::Team)
406+
.filter(crate_owners::crate_id.eq(self.id))
406407
.inner_join(teams::table)
407408
.select(teams::all_columns)
408-
.filter(crate_owners::owner_kind.eq(OwnerKind::Team as i32))
409409
.load(conn)?
410410
.into_iter()
411411
.map(Owner::Team);
@@ -449,6 +449,7 @@ impl Crate {
449449
owner_id: owner.id(),
450450
created_by: req_user.id,
451451
owner_kind: OwnerKind::Team as i32,
452+
email_notifications: true,
452453
})
453454
.on_conflict(crate_owners::table.primary_key())
454455
.do_update()

src/models/owner.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use diesel::pg::Pg;
12
use diesel::prelude::*;
23

34
use crate::app::App;
@@ -19,6 +20,22 @@ pub struct CrateOwner {
1920
pub owner_id: i32,
2021
pub created_by: i32,
2122
pub owner_kind: i32,
23+
pub email_notifications: bool,
24+
}
25+
26+
type BoxedQuery<'a> = crate_owners::BoxedQuery<'a, Pg, crate_owners::SqlType>;
27+
28+
impl CrateOwner {
29+
/// Returns a base crate owner query filtered by the owner kind argument. This query also
30+
/// filters out deleted records.
31+
pub fn by_owner_kind(kind: OwnerKind) -> BoxedQuery<'static> {
32+
use self::crate_owners::dsl::*;
33+
34+
crate_owners
35+
.filter(deleted.eq(false))
36+
.filter(owner_kind.eq(kind as i32))
37+
.into_boxed()
38+
}
2239
}
2340

2441
#[derive(Debug, Clone, Copy)]

src/models/user.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,11 +129,10 @@ impl User {
129129
}
130130

131131
pub fn owning(krate: &Crate, conn: &PgConnection) -> CargoResult<Vec<Owner>> {
132-
let base_query = CrateOwner::belonging_to(krate).filter(crate_owners::deleted.eq(false));
133-
let users = base_query
132+
let users = CrateOwner::by_owner_kind(OwnerKind::User)
134133
.inner_join(users::table)
135134
.select(users::all_columns)
136-
.filter(crate_owners::owner_kind.eq(OwnerKind::User as i32))
135+
.filter(crate_owners::crate_id.eq(krate.id))
137136
.load(conn)?
138137
.into_iter()
139138
.map(Owner::User);

src/router.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,10 @@ pub fn build_router(app: &App) -> R404 {
8989
"/me/crate_owner_invitations/:crate_id",
9090
C(crate_owner_invitation::handle_invite),
9191
);
92+
api_router.put(
93+
"/me/email_notifications",
94+
C(user::me::update_email_notifications),
95+
);
9296
api_router.get("/summary", C(krate::metadata::summary));
9397
api_router.put("/confirm/:email_token", C(user::me::confirm_user_email));
9498
api_router.put(

src/schema.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,12 @@ table! {
261261
///
262262
/// (Automatically generated by Diesel.)
263263
owner_kind -> Int4,
264+
/// The `email_notifications` column of the `crate_owners` table.
265+
///
266+
/// Its SQL type is `Bool`.
267+
///
268+
/// (Automatically generated by Diesel.)
269+
email_notifications -> Bool,
264270
}
265271
}
266272

src/tasks/dump_db/dump-db.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,9 @@ owner_id = "public"
6666
created_at = "public"
6767
created_by = "private"
6868
deleted = "private"
69-
updated_at = "public"
69+
updated_at = "private"
7070
owner_kind = "public"
71+
email_notifications = "private"
7172

7273
[crates.columns]
7374
id = "public"

src/tests/all.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,7 @@ fn add_team_to_crate(t: &Team, krate: &Crate, u: &User, conn: &PgConnection) ->
242242
owner_id: t.id,
243243
created_by: u.id,
244244
owner_kind: 1, // Team owner kind is 1 according to owner.rs
245+
email_notifications: true,
245246
};
246247

247248
diesel::insert_into(crate_owners::table)

0 commit comments

Comments
 (0)