diff --git a/.travis.yml b/.travis.yml
index 5beb7a1..8bc8f11 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -6,4 +6,5 @@ before_script:
script:
- cargo fmt --all -- --check
- cargo build
-- cargo test
\ No newline at end of file
+- cargo test -- --test-threads=1
+
diff --git a/docs/database-schema.png b/docs/database-schema.png
index 4e3debf..913df2e 100644
Binary files a/docs/database-schema.png and b/docs/database-schema.png differ
diff --git a/docs/database-schema.xml b/docs/database-schema.xml
index 31060ed..e564c81 100644
--- a/docs/database-schema.xml
+++ b/docs/database-schema.xml
@@ -1 +1 @@
-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
\ No newline at end of file
+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
\ No newline at end of file
diff --git a/docs/extra-docker-info.md b/docs/extra-docker-info.md
index d840edf..f96809f 100644
--- a/docs/extra-docker-info.md
+++ b/docs/extra-docker-info.md
@@ -41,8 +41,9 @@ docker-compose exec backend bash
```
From there all normal Diesel or Rust maintenance commands can be run. Please go to the official documentation for details, a list of commonly used commands are below.
-- `cargo upgrade`
- `cargo fmt`
+- `cargo test -- --test-threads=1`
+- `cargo upgrade`
- `cargo clippy`
- `diesel setup`
- `diesel migration generate {name}`
diff --git a/migrations/2018-12-19-163802_create_students_and_events/down.sql b/migrations/2018-12-19-163802_create_students_and_events/down.sql
new file mode 100644
index 0000000..468999e
--- /dev/null
+++ b/migrations/2018-12-19-163802_create_students_and_events/down.sql
@@ -0,0 +1,4 @@
+-- Remove the member, event, and attendance table
+DROP TABLE attendance; -- Remove this first, because it depends on the other tables
+DROP TABLE member;
+DROP TABLE event;
diff --git a/migrations/2018-12-19-163802_create_students_and_events/up.sql b/migrations/2018-12-19-163802_create_students_and_events/up.sql
new file mode 100644
index 0000000..ecba77d
--- /dev/null
+++ b/migrations/2018-12-19-163802_create_students_and_events/up.sql
@@ -0,0 +1,30 @@
+-- Create a member table
+CREATE TABLE member (
+ ufl_username TEXT PRIMARY KEY,
+ is_info_filled_out BOOLEAN NOT NULL DEFAULT FALSE,
+ first_name TEXT NOT NULL DEFAULT '',
+ last_name TEXT NOT NULL DEFAULT '',
+ discord_username TEXT NOT NULL DEFAULT '',
+ github_username TEXT NOT NULL DEFAULT '',
+ server_username TEXT NOT NULL DEFAULT '',
+ server_key TEXT NOT NULL DEFAULT '',
+ is_acm_shareable BOOLEAN NOT NULL DEFAULT FALSE,
+ is_in_email_list BOOLEAN NOT NULL DEFAULT FALSE
+);
+
+-- Create an event table
+CREATE TABLE event (
+ start_timestamp TIMESTAMPTZ PRIMARY KEY, -- Events can not start at the same time and day
+ title TEXT NOT NULL, -- Must have title
+ location TEXT NOT NULL, -- Must have location
+ description TEXT NOT NULL DEFAULT '',
+ end_timestamp TIMESTAMPTZ NOT NULL, -- Events must have an end time
+ image BYTEA NOT NULL DEFAULT '\000'-- Image binary defaults to 0
+);
+
+-- Create a many to many attendance table
+CREATE TABLE attendance (
+ ufl_username TEXT REFERENCES member,
+ start_timestamp TIMESTAMPTZ REFERENCES event,
+ PRIMARY KEY (ufl_username, start_timestamp)
+);
diff --git a/src/attendance.rs b/src/attendance.rs
new file mode 100644
index 0000000..7b1ce90
--- /dev/null
+++ b/src/attendance.rs
@@ -0,0 +1,11 @@
+use diesel::prelude::*;
+use diesel::sql_types::Timestamptz;
+
+use super::database;
+use super::schema::attendance;
+
+#[derive(Queryable)]
+pub struct Attendance {
+ pub ufl_username: String,
+ pub start_timestamp: Timestamptz,
+}
diff --git a/src/database.rs b/src/database.rs
new file mode 100644
index 0000000..d2d3937
--- /dev/null
+++ b/src/database.rs
@@ -0,0 +1,15 @@
+extern crate diesel;
+
+use diesel::pg::PgConnection;
+use diesel::prelude::*;
+use std::env;
+
+// club_data data is defined in Rocket.toml
+#[database("club_data")]
+pub struct ClubDbConn(diesel::PgConnection);
+
+// Create connection for other functions to use
+pub fn establish_connection() -> PgConnection {
+ let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
+ PgConnection::establish(&database_url).expect(&format!("Error connecting to {}", database_url))
+}
diff --git a/src/event.rs b/src/event.rs
new file mode 100644
index 0000000..47f2270
--- /dev/null
+++ b/src/event.rs
@@ -0,0 +1,16 @@
+use diesel::prelude::*;
+use diesel::sql_types::Bytea;
+use diesel::sql_types::Timestamptz;
+
+use super::database;
+use super::schema::event;
+
+#[derive(Queryable)]
+pub struct Event {
+ pub start_timestamp: Timestamptz,
+ pub title: String,
+ pub location: String,
+ pub description: String,
+ pub end_timestamp: Timestamptz,
+ pub image: Bytea,
+}
diff --git a/src/main.rs b/src/main.rs
index 291e396..48568f3 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,31 +1,34 @@
#![feature(proc_macro_hygiene, decl_macro)]
+// Temporarily silence warnings caused by Diesel (https://github.com/diesel-rs/diesel/issues/1785)
+#![allow(proc_macro_derive_resolution_fallback)]
#[macro_use]
extern crate rocket;
#[macro_use]
extern crate rocket_contrib;
+#[macro_use]
+extern crate diesel;
-use diesel::pg::PgConnection;
-use diesel::prelude::*;
-use rocket_contrib::databases::diesel;
-use std::env;
-
-#[database("club_data")]
-struct ClubDbConn(diesel::PgConnection);
+// Utility local dependencies
+mod database;
+mod schema;
-pub fn establish_connection() -> PgConnection {
- let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
- PgConnection::establish(&database_url).expect(&format!("Error connecting to {}", database_url))
-}
+// Table specifc local dependencies
+mod attendance;
+mod event;
+mod member;
+// Check to see if the server is working
#[get("/")]
fn index() -> &'static str {
"Hello, world!"
}
+// Launch the REST server with the database connection
fn main() {
rocket::ignite()
- .attach(ClubDbConn::fairing())
+ .attach(database::ClubDbConn::fairing())
+ // Note: Be sure to mount all the routes from differnt modules
.mount("/", routes![index])
.launch();
}
diff --git a/src/member.rs b/src/member.rs
new file mode 100644
index 0000000..ce11f23
--- /dev/null
+++ b/src/member.rs
@@ -0,0 +1,137 @@
+use diesel::prelude::*;
+
+use super::database;
+use super::schema::member;
+
+/* Struct Setup */
+
+// Struct for interacting with the member table
+#[derive(Insertable, Queryable)]
+#[table_name = "member"]
+pub struct Member {
+ pub ufl_username: String,
+ pub is_info_filled_out: bool,
+ pub first_name: String,
+ pub last_name: String,
+ pub discord_username: String,
+ pub github_username: String,
+ pub server_username: String,
+ pub server_key: String,
+ pub is_acm_shareable: bool,
+ pub is_in_email_list: bool,
+}
+
+// Support creating new members that supply just a UFL username
+impl Member {
+ fn new(ufl_username: &str) -> Self {
+ Member {
+ ufl_username: ufl_username.to_string(),
+ ..Default::default()
+ }
+ }
+}
+
+// Set default values for Member
+// Note: the UFL username should never be not set (Rust requires all values to have a default)
+impl Default for Member {
+ fn default() -> Member {
+ Member {
+ ufl_username: "".to_string(),
+ is_info_filled_out: false,
+ first_name: "".to_string(),
+ last_name: "".to_string(),
+ github_username: "".to_string(),
+ discord_username: "".to_string(),
+ server_username: "".to_string(),
+ server_key: "".to_string(),
+ is_acm_shareable: false,
+ is_in_email_list: false,
+ }
+ }
+}
+
+/* CRUD and other functions */
+
+// Return all members
+pub fn list_members() -> Vec {
+ let connection = database::establish_connection();
+ let results = member::table
+ .load::(&connection)
+ .expect("Error loading members");
+ results
+}
+
+// Add a member with a UFL username
+pub fn add_member(ufl_username: &str) {
+ let connection = database::establish_connection();
+
+ let new_member = Member::new(&ufl_username);
+
+ diesel::insert_into(member::table)
+ .values(&new_member)
+ .get_result::(&connection)
+ .expect("Error saving new member");
+}
+
+// Remove a member by their username
+pub fn remove_member(ufl_username: &str) {
+ let connection = database::establish_connection();
+
+ let num_deleted =
+ diesel::delete(member::table.filter(member::columns::ufl_username.eq(ufl_username))) //.like(ufl_username)))
+ .execute(&connection)
+ .expect("Error deleting members");
+
+ println!("Deleted {} members", num_deleted);
+}
+
+/* Unit testing */
+
+// Note: Do run the test as `cargo test -- --test-threads=1` to run the database calls in order
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ // Utility function to clear out the whole member table
+ fn clear_table() {
+ let connection = database::establish_connection();
+
+ diesel::delete(member::table)
+ .execute(&connection)
+ .expect("Error deleting all members");
+ }
+
+ // Check to make sure no members exist by default
+ #[test]
+ fn no_members() {
+ clear_table();
+ assert_eq!(Vec::len(&list_members()), 0);
+ }
+
+ // Check that one member exists after they are created
+ #[test]
+ fn one_member() {
+ clear_table();
+ add_member("one_member_test@email.com");
+ assert_eq!(Vec::len(&list_members()), 1);
+ }
+
+ // Check that two members exist agter they are both created
+ #[test]
+ fn two_member() {
+ clear_table();
+ add_member("two_member_test_one@email.com");
+ add_member("two_member_test_two@email.com");
+ assert_eq!(Vec::len(&list_members()), 2);
+ }
+
+ // Checl that a single member can be deleted after being created
+ #[test]
+ fn delete_member() {
+ clear_table();
+ add_member("delete_member_test@email.com");
+ remove_member("delete_member_test@email.com");
+ assert_eq!(Vec::len(&list_members()), 0);
+ }
+
+}
diff --git a/src/schema.rs b/src/schema.rs
new file mode 100644
index 0000000..e22fe1a
--- /dev/null
+++ b/src/schema.rs
@@ -0,0 +1,37 @@
+table! {
+ attendance (ufl_username, start_timestamp) {
+ ufl_username -> Text,
+ start_timestamp -> Timestamptz,
+ }
+}
+
+table! {
+ event (start_timestamp) {
+ start_timestamp -> Timestamptz,
+ title -> Text,
+ location -> Text,
+ description -> Text,
+ end_timestamp -> Timestamptz,
+ image -> Bytea,
+ }
+}
+
+table! {
+ member (ufl_username) {
+ ufl_username -> Text,
+ is_info_filled_out -> Bool,
+ first_name -> Text,
+ last_name -> Text,
+ discord_username -> Text,
+ github_username -> Text,
+ server_username -> Text,
+ server_key -> Text,
+ is_acm_shareable -> Bool,
+ is_in_email_list -> Bool,
+ }
+}
+
+joinable!(attendance -> event (start_timestamp));
+joinable!(attendance -> member (ufl_username));
+
+allow_tables_to_appear_in_same_query!(attendance, event, member,);