Skip to content

Commit

Permalink
Add location stats (#40)
Browse files Browse the repository at this point in the history
Refs #39.
  • Loading branch information
dbrgn authored Jun 11, 2020
1 parent 57df762 commit b8a9b4b
Show file tree
Hide file tree
Showing 6 changed files with 267 additions and 11 deletions.
186 changes: 176 additions & 10 deletions src/data.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
use std::env;
use std::io;

use diesel::dsl::{count, max};
use diesel::prelude::*;
use diesel::result::QueryResult;
use diesel::sql_types::{Double, Integer, Text};
use diesel::{sql_function, sql_query, PgConnection};
use diesel_geography::sql_types::Geography;
use diesel_geography::types::GeogPoint;
use diesel::{
dsl::{count, max},
prelude::*,
result::QueryResult,
sql_types::{Double, Integer, Text},
{sql_function, sql_query, PgConnection},
};
use diesel_geography::{sql_types::Geography, types::GeogPoint};
use log::error;
use rocket_contrib::database;

use crate::models::{Flight, Glider, Location, LocationWithDistance, User};
use crate::models::{NewFlight, NewGlider, NewLocation};
use crate::schema::{flights, gliders, locations, users};
use crate::{
models::{
Flight, Glider, Location, LocationWithCount, LocationWithDistance, NewFlight, NewGlider, NewLocation,
User,
},
schema::{flights, gliders, locations, users},
};

sql_function! {
/// The pgcrypto "crypt" function.
Expand Down Expand Up @@ -188,6 +193,40 @@ pub fn get_locations_for_user(conn: &PgConnection, user: &User) -> Vec<Location>
.expect("Error loading locations")
}

#[derive(Debug, PartialEq)]
pub enum LocationOrderBy {
Launches,
Landings,
}

/// Retrieve all visited locations for the specified user, including launch or landing count.
///
/// Entries with a count of 0 will not be included.
pub fn get_locations_with_stats_for_user(
conn: &PgConnection,
user: &User,
order_by: LocationOrderBy,
limit: i32,
) -> Vec<LocationWithCount> {
sql_query(&format!(
"SELECT l.*, count(f.*) as count
FROM locations l
LEFT JOIN flights f on f.{} = l.id
WHERE f.user_id = $1 AND l.user_id = $1
GROUP BY l.id
ORDER BY count DESC
LIMIT $2",
match order_by {
LocationOrderBy::Launches => "launch_at",
LocationOrderBy::Landings => "landing_at",
},
))
.bind::<Integer, _>(user.id)
.bind::<Integer, _>(limit)
.load(conn)
.expect("Error loading locations")
}

/// Retrieve all locations for the specified user within a specified radius
/// from the specified coordinates.
pub fn get_locations_around_point(
Expand Down Expand Up @@ -403,4 +442,131 @@ mod tests {
assert!(user.is_some());
assert_eq!(user.unwrap().id, ctx.testuser1.user.id);
}

#[test]
fn test_get_locations_with_stats_for_user() {
let ctx = test_utils::DbTestContext::new();

// No locations
let l = get_locations_with_stats_for_user(
&*ctx.force_get_conn(),
&ctx.testuser1.user,
LocationOrderBy::Launches,
99,
);
assert_eq!(l.len(), 0);

// Locations, but no associated flights
let locations = diesel::insert_into(locations::table)
.values(vec![
NewLocation {
name: "Selun".into(),
user_id: ctx.testuser1.user.id,
..Default::default()
},
NewLocation {
name: "Etzel".into(),
user_id: ctx.testuser1.user.id,
..Default::default()
},
NewLocation {
name: "Altendorf".into(),
user_id: ctx.testuser1.user.id,
..Default::default()
},
NewLocation {
name: "Hummel".into(),
user_id: ctx.testuser1.user.id,
..Default::default()
},
NewLocation {
name: "Stöcklichrüz".into(),
user_id: ctx.testuser2.user.id,
..Default::default()
},
NewLocation {
name: "Pfäffikon".into(),
user_id: ctx.testuser2.user.id,
..Default::default()
},
])
.get_results::<Location>(&*ctx.force_get_conn())
.expect("Could not create flight");
let l = get_locations_with_stats_for_user(
&*ctx.force_get_conn(),
&ctx.testuser1.user,
LocationOrderBy::Launches,
99,
);
assert_eq!(l.len(), 0);

// Add some flights
diesel::insert_into(flights::table)
.values(vec![
NewFlight {
// Selun - Unknown
user_id: ctx.testuser1.user.id,
launch_at: Some(locations[0].id),
landing_at: None,
..Default::default()
},
NewFlight {
// Selun - Altendorf
user_id: ctx.testuser1.user.id,
launch_at: Some(locations[0].id),
landing_at: Some(locations[2].id),
..Default::default()
},
NewFlight {
// Selun - Altendorf
user_id: ctx.testuser1.user.id,
launch_at: Some(locations[0].id),
landing_at: Some(locations[2].id),
..Default::default()
},
NewFlight {
// Etzel - Altendorf
user_id: ctx.testuser1.user.id,
launch_at: Some(locations[1].id),
landing_at: Some(locations[2].id),
..Default::default()
},
NewFlight {
// Etzel - Etzel (toplanding)
user_id: ctx.testuser1.user.id,
launch_at: Some(locations[1].id),
landing_at: Some(locations[1].id),
..Default::default()
},
NewFlight {
// Stöcklichrüz - Pfäffikon (other user)
user_id: ctx.testuser2.user.id,
launch_at: Some(locations[4].id),
landing_at: Some(locations[5].id),
..Default::default()
},
])
.execute(&*ctx.force_get_conn())
.expect("Could not create flight");
let l_launches = get_locations_with_stats_for_user(
&*ctx.force_get_conn(),
&ctx.testuser1.user,
LocationOrderBy::Launches,
99,
)
.into_iter()
.map(|l| (l.name, l.count))
.collect::<Vec<_>>();
assert_eq!(l_launches, vec![("Selun".into(), 3), ("Etzel".into(), 2)]);
let l_landings = get_locations_with_stats_for_user(
&*ctx.force_get_conn(),
&ctx.testuser1.user,
LocationOrderBy::Landings,
99,
)
.into_iter()
.map(|l| (l.name, l.count))
.collect::<Vec<_>>();
assert_eq!(l_landings, vec![("Altendorf".into(), 3), ("Etzel".into(), 1)]);
}
}
3 changes: 3 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ mod models;
mod optionresult;
mod process_igc;
mod schema;
mod stats;
mod templates;
#[cfg(test)] mod test_utils;

Expand Down Expand Up @@ -142,6 +143,8 @@ fn main() {
locations::edit_form,
locations::edit,
process_igc::process_igc,
stats::stats,
stats::stats_nologin,
],
)
// Profile
Expand Down
19 changes: 18 additions & 1 deletion src/models.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use chrono::{DateTime, NaiveDate, Utc};
use diesel::sql_types::{Double, Integer, Text};
use diesel::sql_types::{BigInt, Double, Integer, Text};
use diesel::{Associations, Identifiable, Queryable};
use diesel_geography::sql_types::Geography;
use diesel_geography::types::GeogPoint;
Expand Down Expand Up @@ -96,6 +96,23 @@ pub struct LocationWithDistance {
pub distance: f64,
}

/// Locations with a count (e.g. landing count).
#[derive(QueryableByName, Serialize, PartialEq, Debug, Clone)]
pub struct LocationWithCount {
#[sql_type = "Integer"]
pub id: i32,
#[sql_type = "Text"]
pub name: String,
#[sql_type = "Text"]
pub country: String,
#[sql_type = "Integer"]
pub elevation: i32,
#[sql_type = "Integer"]
pub user_id: i32,
#[sql_type = "BigInt"]
pub count: i64,
}

#[derive(Identifiable, Queryable, Associations, AsChangeset, Serialize, PartialEq, Debug, Clone)]
#[belongs_to(User, foreign_key = "user_id")]
#[belongs_to(Glider, foreign_key = "glider_id")]
Expand Down
45 changes: 45 additions & 0 deletions src/stats.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//! Stats views.
use rocket::{get, response::Redirect};
use rocket_contrib::templates::Template;
use serde::Serialize;

use crate::{
auth,
data::{self, LocationOrderBy},
models::{LocationWithCount, User},
};

// Contexts

#[derive(Serialize)]
struct StatsContext {
user: User,
launch_locations: Vec<LocationWithCount>,
landing_locations: Vec<LocationWithCount>,
}

// Views

#[get("/stats")]
pub(crate) fn stats(db: data::Database, user: auth::AuthUser) -> Template {
let user = user.into_inner();

// Get all locations
let launch_locations = data::get_locations_with_stats_for_user(&db, &user, LocationOrderBy::Launches, 10);
let landing_locations =
data::get_locations_with_stats_for_user(&db, &user, LocationOrderBy::Landings, 10);

// Render template
let context = StatsContext {
user,
launch_locations,
landing_locations,
};
Template::render("stats", &context)
}

#[get("/stats", rank = 2)]
pub(crate) fn stats_nologin() -> Redirect {
Redirect::to("/auth/login")
}
1 change: 1 addition & 0 deletions templates/base.tera
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
<a class="navbar-item" href="/flights/">My Flights</a>
<a class="navbar-item" href="/gliders/">My Gliders</a>
<a class="navbar-item" href="/locations/">My Locations</a>
<a class="navbar-item" href="/stats/">Stats</a>
<a class="navbar-item" href="/flights/add/">Submit flight</a>
{% endif %}
</div>
Expand Down
24 changes: 24 additions & 0 deletions templates/stats.tera
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{% extends 'base' %}

{% import 'macros' as macros %}

{% block content %}
<h2 class="title is-2">Stats</h2>

<section>
<div class="columns">
<div class="column">
<h3 class="title is-4">Top Launch Sites</h3>
<ul>
{% for location in launch_locations %}<li>{{ macros::flag(country_code=location.country) }} {{ location.name }} ({{ location.count }})</li>{% endfor %}
</ul>
</div>
<div class="column">
<h3 class="title is-4">Top Landing Sites</h3>
<ul>
{% for location in landing_locations %}<li>{{ macros::flag(country_code=location.country) }} {{ location.name }} ({{ location.count }})</li>{% endfor %}
</ul>
</div>
</div>
</section>
{% endblock %}

0 comments on commit b8a9b4b

Please sign in to comment.