Skip to content

backend: Add user email preference storing #1901

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

Merged
merged 5 commits into from
Nov 22, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE crate_owners DROP COLUMN email_notifications;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE crate_owners ADD COLUMN email_notifications BOOLEAN NOT NULL DEFAULT TRUE;
1 change: 1 addition & 0 deletions src/controllers/crate_owner_invitation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ fn accept_invite(
owner_id: user_id,
created_by: pending_crate_owner.invited_by_user_id,
owner_kind: OwnerKind::User as i32,
email_notifications: true,
})
.on_conflict(crate_owners::table.primary_key())
.do_update()
Expand Down
14 changes: 5 additions & 9 deletions src/controllers/krate/search.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use diesel_full_text_search::*;

use crate::controllers::helpers::Paginate;
use crate::controllers::prelude::*;
use crate::models::{Crate, CrateBadge, CrateVersions, OwnerKind, Version};
use crate::models::{Crate, CrateBadge, CrateOwner, CrateVersions, OwnerKind, Version};
use crate::schema::*;
use crate::views::EncodableCrate;

Expand Down Expand Up @@ -118,21 +118,17 @@ pub fn search(req: &mut dyn Request) -> CargoResult<Response> {
} else if let Some(user_id) = params.get("user_id").and_then(|s| s.parse::<i32>().ok()) {
query = query.filter(
crates::id.eq_any(
crate_owners::table
CrateOwner::by_owner_kind(OwnerKind::User)
.select(crate_owners::crate_id)
.filter(crate_owners::owner_id.eq(user_id))
.filter(crate_owners::deleted.eq(false))
.filter(crate_owners::owner_kind.eq(OwnerKind::User as i32)),
.filter(crate_owners::owner_id.eq(user_id)),
),
);
} else if let Some(team_id) = params.get("team_id").and_then(|s| s.parse::<i32>().ok()) {
query = query.filter(
crates::id.eq_any(
crate_owners::table
CrateOwner::by_owner_kind(OwnerKind::Team)
.select(crate_owners::crate_id)
.filter(crate_owners::owner_id.eq(team_id))
.filter(crate_owners::deleted.eq(false))
.filter(crate_owners::owner_kind.eq(OwnerKind::Team as i32)),
.filter(crate_owners::owner_id.eq(team_id)),
),
);
} else if params.get("following").is_some() {
Expand Down
85 changes: 80 additions & 5 deletions src/controllers/user/me.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
use std::collections::HashMap;

use crate::controllers::prelude::*;

use crate::controllers::helpers::*;
use crate::email;
use crate::util::bad_request;
use crate::util::errors::CargoError;

use crate::models::{Email, Follow, NewEmail, User, Version};
use crate::schema::{crates, emails, follows, users, versions};
use crate::views::{EncodableMe, EncodableVersion};
use crate::models::{CrateOwner, Email, Follow, NewEmail, OwnerKind, User, Version};
use crate::schema::{crate_owners, crates, emails, follows, users, versions};
use crate::views::{EncodableMe, EncodableVersion, OwnedCrate};

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

let id = req.user()?.id;
let user_id = req.user()?.id;
let conn = req.db_conn()?;

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

let owned_crates = crate_owners::table
.inner_join(crates::table)
.filter(crate_owners::owner_id.eq(user_id))
.filter(crate_owners::owner_kind.eq(OwnerKind::User as i32))
.select((crates::id, crates::name, crate_owners::email_notifications))
.order(crates::name.asc())
.load(&*conn)?
.into_iter()
.map(|(id, name, email_notifications)| OwnedCrate {
id,
name,
email_notifications,
})
.collect();

let verified = verified.unwrap_or(false);
let verification_sent = verified || verification_sent;
let user = User { email, ..user };

Ok(req.json(&EncodableMe {
user: user.encodable_private(verified, verification_sent),
owned_crates,
}))
}

Expand Down Expand Up @@ -212,3 +230,60 @@ pub fn regenerate_token_and_send(req: &mut dyn Request) -> CargoResult<Response>
}
Ok(req.json(&R { ok: true }))
}

/// Handles `PUT /me/email_notifications` route
pub fn update_email_notifications(req: &mut dyn Request) -> CargoResult<Response> {
use self::crate_owners::dsl::*;
use diesel::pg::upsert::excluded;

#[derive(Deserialize)]
struct CrateEmailNotifications {
id: i32,
email_notifications: bool,
}

let mut body = String::new();
req.body().read_to_string(&mut body)?;
let updates: HashMap<i32, bool> = serde_json::from_str::<Vec<CrateEmailNotifications>>(&body)
.map_err(|_| bad_request("invalid json request"))?
.iter()
.map(|c| (c.id, c.email_notifications))
.collect();

let user = req.user()?;
let conn = req.db_conn()?;

// Build inserts from existing crates beloning to the current user
let to_insert = CrateOwner::by_owner_kind(OwnerKind::User)
.filter(owner_id.eq(user.id))
.select((crate_id, owner_id, owner_kind, email_notifications))
.load(&*conn)?
.into_iter()
// Remove records whose `email_notifications` will not change from their current value
.map(
|(c_id, o_id, o_kind, e_notifications): (i32, i32, i32, bool)| {
let current_e_notifications = *updates.get(&c_id).unwrap_or(&e_notifications);
(
crate_id.eq(c_id),
owner_id.eq(o_id),
owner_kind.eq(o_kind),
email_notifications.eq(current_e_notifications),
)
},
)
.collect::<Vec<_>>();

// Upsert crate owners; this should only actually exectute updates
diesel::insert_into(crate_owners)
.values(&to_insert)
.on_conflict((crate_id, owner_id, owner_kind))
.do_update()
.set(email_notifications.eq(excluded(email_notifications)))
.execute(&*conn)?;

#[derive(Serialize)]
struct R {
ok: bool,
}
Ok(req.json(&R { ok: true }))
}
11 changes: 6 additions & 5 deletions src/models/krate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ impl<'a> NewCrate<'a> {
owner_id: user_id,
created_by: user_id,
owner_kind: OwnerKind::User as i32,
email_notifications: true,
};
diesel::insert_into(crate_owners::table)
.values(&owner)
Expand Down Expand Up @@ -400,18 +401,17 @@ impl Crate {
}

pub fn owners(&self, conn: &PgConnection) -> CargoResult<Vec<Owner>> {
let base_query = CrateOwner::belonging_to(self).filter(crate_owners::deleted.eq(false));
let users = base_query
let users = CrateOwner::by_owner_kind(OwnerKind::User)
.filter(crate_owners::crate_id.eq(self.id))
.inner_join(users::table)
.select(users::all_columns)
.filter(crate_owners::owner_kind.eq(OwnerKind::User as i32))
.load(conn)?
.into_iter()
.map(Owner::User);
let teams = base_query
let teams = CrateOwner::by_owner_kind(OwnerKind::Team)
.filter(crate_owners::crate_id.eq(self.id))
.inner_join(teams::table)
.select(teams::all_columns)
.filter(crate_owners::owner_kind.eq(OwnerKind::Team as i32))
.load(conn)?
.into_iter()
.map(Owner::Team);
Expand Down Expand Up @@ -455,6 +455,7 @@ impl Crate {
owner_id: owner.id(),
created_by: req_user.id,
owner_kind: OwnerKind::Team as i32,
email_notifications: true,
})
.on_conflict(crate_owners::table.primary_key())
.do_update()
Expand Down
17 changes: 17 additions & 0 deletions src/models/owner.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use diesel::pg::Pg;
use diesel::prelude::*;

use crate::app::App;
Expand All @@ -19,6 +20,22 @@ pub struct CrateOwner {
pub owner_id: i32,
pub created_by: i32,
pub owner_kind: i32,
pub email_notifications: bool,
}

type BoxedQuery<'a> = crate_owners::BoxedQuery<'a, Pg, crate_owners::SqlType>;

impl CrateOwner {
/// Returns a base crate owner query filtered by the owner kind argument. This query also
/// filters out deleted records.
pub fn by_owner_kind(kind: OwnerKind) -> BoxedQuery<'static> {
use self::crate_owners::dsl::*;

crate_owners
.filter(deleted.eq(false))
.filter(owner_kind.eq(kind as i32))
.into_boxed()
}
}

#[derive(Debug, Clone, Copy)]
Expand Down
5 changes: 2 additions & 3 deletions src/models/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,10 @@ impl User {
}

pub fn owning(krate: &Crate, conn: &PgConnection) -> CargoResult<Vec<Owner>> {
let base_query = CrateOwner::belonging_to(krate).filter(crate_owners::deleted.eq(false));
let users = base_query
let users = CrateOwner::by_owner_kind(OwnerKind::User)
.inner_join(users::table)
.select(users::all_columns)
.filter(crate_owners::owner_kind.eq(OwnerKind::User as i32))
.filter(crate_owners::crate_id.eq(krate.id))
.load(conn)?
.into_iter()
.map(Owner::User);
Expand Down
4 changes: 4 additions & 0 deletions src/router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ pub fn build_router(app: &App) -> R404 {
"/me/crate_owner_invitations/:crate_id",
C(crate_owner_invitation::handle_invite),
);
api_router.put(
"/me/email_notifications",
C(user::me::update_email_notifications),
);
api_router.get("/summary", C(krate::metadata::summary));
api_router.put("/confirm/:email_token", C(user::me::confirm_user_email));
api_router.put(
Expand Down
6 changes: 6 additions & 0 deletions src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,12 @@ table! {
///
/// (Automatically generated by Diesel.)
owner_kind -> Int4,
/// The `email_notifications` column of the `crate_owners` table.
///
/// Its SQL type is `Bool`.
///
/// (Automatically generated by Diesel.)
email_notifications -> Bool,
}
}

Expand Down
3 changes: 2 additions & 1 deletion src/tasks/dump_db/dump-db.toml
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,9 @@ owner_id = "public"
created_at = "public"
created_by = "private"
deleted = "private"
updated_at = "public"
updated_at = "private"
owner_kind = "public"
email_notifications = "private"

[crates.columns]
id = "public"
Expand Down
1 change: 1 addition & 0 deletions src/tests/all.rs
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ fn add_team_to_crate(t: &Team, krate: &Crate, u: &User, conn: &PgConnection) ->
owner_id: t.id,
created_by: u.id,
owner_kind: 1, // Team owner kind is 1 according to owner.rs
email_notifications: true,
};

diesel::insert_into(crate_owners::table)
Expand Down
Loading