| @@ -0,0 +1,100 @@ | |||||
| use std::collections::HashMap; | |||||
| use serde::{Deserialize, Serialize}; | |||||
| use actix_web::{web, HttpRequest, HttpResponse, Responder}; | |||||
| use chrono::NaiveDateTime; | |||||
| use crate::models::models::Comment; | |||||
| use crate::repositories::comment::{create_comment, get_issue_comments}; | |||||
| use crate::repositories::session::get_username_from_session; | |||||
| #[derive(Deserialize)] | |||||
| pub struct AddCommentRequest { | |||||
| pub content: String, | |||||
| pub parent: Option<i64>, | |||||
| pub issue_id: i32, | |||||
| } | |||||
| #[derive(Serialize)] | |||||
| pub struct AddCommentResponse { | |||||
| pub content: String, | |||||
| pub parent: Option<i64>, | |||||
| pub telegram_handle: String, | |||||
| pub created_at: NaiveDateTime, | |||||
| } | |||||
| pub async fn add_comment(req: HttpRequest, data: web::Json<AddCommentRequest>) -> impl Responder { | |||||
| if let Some(session) = req.cookie("session") { | |||||
| if let Some(username) = get_username_from_session(session.value().to_string()) { | |||||
| let new_comment = create_comment(data.content.clone(), data.parent, username.clone(), data.issue_id); | |||||
| HttpResponse::Ok().json( | |||||
| AddCommentResponse { | |||||
| content: new_comment.content, | |||||
| parent: new_comment.parent, | |||||
| telegram_handle: username.clone(), | |||||
| created_at: new_comment.created_at.unwrap() | |||||
| }) | |||||
| } else { | |||||
| HttpResponse::Unauthorized().into() | |||||
| } | |||||
| } else { | |||||
| HttpResponse::Unauthorized().into() | |||||
| } | |||||
| } | |||||
| #[derive(Deserialize)] | |||||
| pub struct GetCommentsRequest { | |||||
| pub issue_id: i32, | |||||
| } | |||||
| #[derive(Serialize, Clone)] | |||||
| pub struct CommentWithChildren { | |||||
| pub comment: Comment, | |||||
| pub children: Vec<CommentWithChildren>, | |||||
| } | |||||
| #[derive(Serialize)] | |||||
| pub struct GetCommentsResponse { | |||||
| pub comments: Vec<CommentWithChildren>, | |||||
| } | |||||
| fn build_comment_tree(comments: Vec<Comment>) -> Vec<CommentWithChildren> { | |||||
| let mut comment_map: HashMap<i64, CommentWithChildren> = HashMap::new(); | |||||
| let mut child_map: HashMap<i64, Vec<i64>> = HashMap::new(); | |||||
| let mut root_ids: Vec<i64> = Vec::new(); | |||||
| for comment in comments { | |||||
| let id = comment.id; | |||||
| let comment_with_children = CommentWithChildren { | |||||
| comment: comment.clone(), | |||||
| children: Vec::new(), | |||||
| }; | |||||
| comment_map.insert(id, comment_with_children); | |||||
| if let Some(parent_id) = comment.parent { | |||||
| child_map.entry(parent_id).or_default().push(id); | |||||
| } else { | |||||
| root_ids.push(id); | |||||
| } | |||||
| } | |||||
| fn build_tree(id: i64, comment_map: &mut HashMap<i64, CommentWithChildren>, child_map: &HashMap<i64, Vec<i64>>) -> CommentWithChildren { | |||||
| let mut comment = comment_map.remove(&id).unwrap(); | |||||
| if let Some(child_ids) = child_map.get(&id) { | |||||
| comment.children = child_ids.iter() | |||||
| .map(|&child_id| build_tree(child_id, comment_map, child_map)) | |||||
| .collect(); | |||||
| } | |||||
| comment | |||||
| } | |||||
| root_ids.into_iter() | |||||
| .map(|id| build_tree(id, &mut comment_map, &child_map)) | |||||
| .collect() | |||||
| } | |||||
| pub async fn get_comments(data: web::Query<GetCommentsRequest>) -> impl Responder { | |||||
| let comments = build_comment_tree(get_issue_comments(data.issue_id)); | |||||
| HttpResponse::Ok().json(GetCommentsResponse { | |||||
| comments | |||||
| }) | |||||
| } | |||||
| @@ -0,0 +1,153 @@ | |||||
| use serde::{Deserialize, Serialize}; | |||||
| use actix_web::{web, HttpRequest, HttpResponse, Responder}; | |||||
| use crate::models::models::IssueWithSummaryAndVotes; | |||||
| use crate::repositories; | |||||
| use crate::repositories::issue::{get_issue_vote_equity, get_vote_for_user, record_vote_for_issue, FilterOptions}; | |||||
| use crate::repositories::session::get_user_id; | |||||
| #[derive(Serialize, Deserialize, Clone)] | |||||
| enum IssueVote { | |||||
| Positive, | |||||
| Negative, | |||||
| } | |||||
| #[derive(Deserialize)] | |||||
| pub struct CreateIssueRequest { | |||||
| title: String, | |||||
| paragraphs: Vec<String>, | |||||
| } | |||||
| #[derive(Serialize)] | |||||
| pub struct CreateIssueResponse { | |||||
| title: String, | |||||
| paragraphs: Vec<String>, | |||||
| } | |||||
| pub async fn add_issue(req: HttpRequest, data: web::Json<CreateIssueRequest>) -> impl Responder { | |||||
| if let Some(session_id) = req.cookie("session") { | |||||
| if let Some(session) = repositories::session::get_session(session_id.value().to_string()) { | |||||
| repositories::issue::create_issue(&data.title, &data.paragraphs, &session.username); | |||||
| let resp = CreateIssueResponse { | |||||
| title: String::from(&data.title), | |||||
| paragraphs: data.paragraphs.clone(), | |||||
| }; | |||||
| return HttpResponse::Ok().json(resp); | |||||
| } | |||||
| HttpResponse::Unauthorized().into() | |||||
| } else { | |||||
| HttpResponse::Unauthorized().into() | |||||
| } | |||||
| } | |||||
| #[derive(Deserialize)] | |||||
| pub struct IssuesPaginationRequest { | |||||
| min_positive_votes: Option<i64>, | |||||
| min_votes: Option<i64>, | |||||
| offset: i32, | |||||
| limit: i16, | |||||
| } | |||||
| #[derive(Serialize)] | |||||
| pub struct IssuesPaginationResponse { | |||||
| issues: Vec<IssueWithSummaryAndVotes>, | |||||
| } | |||||
| pub async fn list_issues(req: web::Query<IssuesPaginationRequest>) -> impl Responder { | |||||
| let issues = repositories::issue::get_issues( | |||||
| Option::from(FilterOptions { | |||||
| min_positive_votes: req.min_positive_votes, | |||||
| min_votes: req.min_votes, | |||||
| }), | |||||
| req.offset, | |||||
| req.limit | |||||
| ); | |||||
| HttpResponse::Ok().json(IssuesPaginationResponse { issues }) | |||||
| } | |||||
| #[derive(Deserialize)] | |||||
| pub struct GetParagraphsRequest { | |||||
| issue_id: i32, | |||||
| } | |||||
| #[derive(Serialize)] | |||||
| pub struct GetParagraphsResponse { | |||||
| paragraphs: Vec<Option<String>>, | |||||
| } | |||||
| pub async fn get_paragraphs(query: web::Query<GetParagraphsRequest>) -> impl Responder { | |||||
| let paragraphs = repositories::issue::get_paragraphs(query.issue_id); | |||||
| HttpResponse::Ok().json(GetParagraphsResponse { paragraphs }) | |||||
| } | |||||
| #[derive(Deserialize, Clone)] | |||||
| pub struct VoteIssueRequest { | |||||
| issue_id: i32, | |||||
| vote: IssueVote, | |||||
| } | |||||
| #[derive(Serialize)] | |||||
| pub struct VoteIssueResponse { | |||||
| issue_id: i32, | |||||
| equity: i64, | |||||
| positive: Option<bool>, | |||||
| } | |||||
| pub async fn vote_issue(req: HttpRequest, data: web::Json<VoteIssueRequest>) -> impl Responder { | |||||
| let positive_vote = match data.vote { | |||||
| IssueVote::Positive => true, | |||||
| IssueVote::Negative => false, | |||||
| }; | |||||
| if let Some(session_id) = req.cookie("session") { | |||||
| if let Some(user_id) = get_user_id(session_id.value().to_string()) { | |||||
| record_vote_for_issue(positive_vote, data.issue_id, user_id); | |||||
| let resp = VoteIssueResponse { | |||||
| issue_id: data.issue_id, | |||||
| equity: get_issue_vote_equity(data.issue_id), | |||||
| positive: match data.vote { | |||||
| IssueVote::Positive => Some(true), | |||||
| IssueVote::Negative => Some(false), | |||||
| }, | |||||
| }; | |||||
| HttpResponse::Ok().json(resp) | |||||
| } else { | |||||
| HttpResponse::Unauthorized().into() | |||||
| } | |||||
| } else { | |||||
| HttpResponse::Unauthorized().into() | |||||
| } | |||||
| } | |||||
| #[derive(Deserialize)] | |||||
| pub struct GetUserVoteRequest { | |||||
| issue_id: i32, | |||||
| } | |||||
| #[derive(Serialize)] | |||||
| pub struct GetUserVoteResponse { | |||||
| vote: Option<IssueVote>, | |||||
| } | |||||
| pub async fn get_user_vote(req: HttpRequest, query: web::Query<GetUserVoteRequest>) -> impl Responder { | |||||
| let mut resp = GetUserVoteResponse { vote: None }; | |||||
| if let Some(session_cookie) = req.cookie("session") { | |||||
| return if let Some(user_id) = get_user_id(session_cookie.value().to_string()) { | |||||
| let vote = get_vote_for_user(query.issue_id, user_id); | |||||
| resp = GetUserVoteResponse { | |||||
| vote: match vote { | |||||
| Some(val) => { | |||||
| if val { | |||||
| Some(IssueVote::Positive) | |||||
| } else { | |||||
| Some(IssueVote::Negative) | |||||
| } | |||||
| }, | |||||
| None => None, | |||||
| } | |||||
| }; | |||||
| HttpResponse::Ok().json(resp) | |||||
| } else { | |||||
| HttpResponse::Ok().json(GetUserVoteResponse { vote: None }) | |||||
| } | |||||
| } | |||||
| HttpResponse::Ok().json(resp) | |||||
| } | |||||
| @@ -0,0 +1,3 @@ | |||||
| pub(crate) mod issue; | |||||
| pub(crate) mod session; | |||||
| pub(crate) mod comment; | |||||
| @@ -0,0 +1,45 @@ | |||||
| use actix_web::{web, HttpRequest, HttpResponse, Responder}; | |||||
| use serde::{Deserialize, Serialize}; | |||||
| use crate::repositories; | |||||
| use crate::repositories::session::get_username_from_session; | |||||
| #[derive(Deserialize, Clone)] | |||||
| pub struct AuthenticateRequest { | |||||
| user_id: i64, | |||||
| auth_date: i64, | |||||
| username: String, | |||||
| first_name: String, | |||||
| last_name: String, | |||||
| photo_url: String, | |||||
| } | |||||
| #[derive(Serialize)] | |||||
| pub struct AuthenticateResponse { | |||||
| session_id: String, | |||||
| } | |||||
| pub async fn authenticate(req: web::Json<AuthenticateRequest>) -> impl Responder { | |||||
| let session_id = repositories::session::authenticate_session( | |||||
| req.clone().user_id, | |||||
| req.clone().auth_date, | |||||
| req.clone().username, | |||||
| req.clone().first_name, | |||||
| req.clone().last_name, | |||||
| req.clone().photo_url | |||||
| ); | |||||
| let resp = AuthenticateResponse { session_id }; | |||||
| HttpResponse::Ok().json(resp) | |||||
| } | |||||
| #[derive(Serialize)] | |||||
| pub struct GetUsernameResponse { | |||||
| username: Option<String>, | |||||
| } | |||||
| pub async fn get_username(req: HttpRequest) -> impl Responder { | |||||
| if let Some(session_cookie) = req.cookie("session") { | |||||
| let username = get_username_from_session(session_cookie.value().to_string()); | |||||
| return HttpResponse::Ok().json(GetUsernameResponse { username }) | |||||
| } | |||||
| HttpResponse::Unauthorized().into() | |||||
| } | |||||
| @@ -0,0 +1,12 @@ | |||||
| use std::env; | |||||
| use diesel::Connection; | |||||
| use diesel::PgConnection; | |||||
| use dotenvy::dotenv; | |||||
| pub fn establish_connection() -> PgConnection { | |||||
| dotenv().ok(); | |||||
| let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set"); | |||||
| PgConnection::establish(&database_url) | |||||
| .unwrap_or_else(|_| panic!("Error connecting to {}", database_url)) | |||||
| } | |||||
| @@ -0,0 +1 @@ | |||||
| pub(crate) mod db; | |||||
| @@ -0,0 +1 @@ | |||||
| pub mod models; | |||||
| @@ -0,0 +1,149 @@ | |||||
| use diesel::sql_types::Integer; | |||||
| use diesel::sql_types::Nullable; | |||||
| use diesel::sql_types::SmallInt; | |||||
| use diesel::sql_types::Timestamp; | |||||
| use diesel::sql_types::Text; | |||||
| use diesel::sql_types::BigInt; | |||||
| use chrono::NaiveDateTime; | |||||
| use diesel::{Insertable, Queryable, QueryableByName, Selectable}; | |||||
| use serde::Serialize; | |||||
| #[derive(Queryable, Selectable, Serialize, Clone)] | |||||
| #[diesel(table_name = crate::schema::issues)] | |||||
| #[diesel(check_for_backend(diesel::pg::Pg))] | |||||
| pub struct Issue { | |||||
| pub id: i32, | |||||
| pub title: String, | |||||
| pub paragraph_count: Option<i16>, | |||||
| pub telegram_handle: String, | |||||
| pub created_at: Option<NaiveDateTime> | |||||
| } | |||||
| #[derive(QueryableByName, Serialize, Clone)] | |||||
| pub struct IssueWithSummaryAndVotes { | |||||
| #[diesel(sql_type = Integer)] | |||||
| pub id: i32, | |||||
| #[diesel(sql_type = Text)] | |||||
| pub title: String, | |||||
| #[diesel(sql_type = Text)] | |||||
| pub summary: String, | |||||
| #[diesel(sql_type = Nullable<SmallInt>)] | |||||
| pub paragraph_count: Option<i16>, | |||||
| #[diesel(sql_type = Text)] | |||||
| pub telegram_handle: String, | |||||
| #[diesel(sql_type = Nullable<Timestamp>)] | |||||
| pub created_at: Option<NaiveDateTime>, | |||||
| #[diesel(sql_type = BigInt)] | |||||
| pub total_votes: i64, | |||||
| #[diesel(sql_type = BigInt)] | |||||
| pub positive_votes: i64, | |||||
| } | |||||
| #[derive(Insertable)] | |||||
| #[diesel(table_name = crate::schema::issues)] | |||||
| pub struct NewIssue { | |||||
| pub title: String, | |||||
| pub paragraph_count: i16, | |||||
| pub telegram_handle: String, | |||||
| } | |||||
| #[derive(Queryable, Selectable)] | |||||
| #[diesel(table_name = crate::schema::sessions)] | |||||
| #[diesel(check_for_backend(diesel::pg::Pg))] | |||||
| pub struct Session { | |||||
| pub id: i64, | |||||
| pub session_id: String, | |||||
| pub auth_date: Option<i64>, | |||||
| pub username: Option<String>, | |||||
| pub first_name: Option<String>, | |||||
| pub last_name: Option<String>, | |||||
| pub photo_url: Option<String> | |||||
| } | |||||
| #[derive(Insertable)] | |||||
| #[diesel(table_name = crate::schema::sessions)] | |||||
| pub struct NewSession { | |||||
| pub user_id: Option<i64>, | |||||
| pub session_id: String, | |||||
| pub auth_date: i64, | |||||
| pub username: Option<String>, | |||||
| pub first_name: Option<String>, | |||||
| pub last_name: Option<String>, | |||||
| pub photo_url: Option<String> | |||||
| } | |||||
| #[derive(Queryable, Selectable, Serialize, Clone)] | |||||
| #[diesel(table_name = crate::schema::paragraphs)] | |||||
| #[diesel(check_for_backend(diesel::pg::Pg))] | |||||
| pub struct Paragraph { | |||||
| pub id: i64, | |||||
| pub content: String, | |||||
| pub index: i32, | |||||
| pub post_id: i32, | |||||
| } | |||||
| #[derive(Insertable)] | |||||
| #[diesel(table_name = crate::schema::paragraphs)] | |||||
| pub struct NewParagraph { | |||||
| pub content: Option<String>, | |||||
| pub index: i32, | |||||
| pub post_id: i32, | |||||
| } | |||||
| #[derive(Queryable, Selectable, Serialize, Clone)] | |||||
| #[diesel(table_name = crate::schema::issue_votes)] | |||||
| #[diesel(check_for_backend(diesel::pg::Pg))] | |||||
| pub struct IssueVote { | |||||
| pub id: i64, | |||||
| pub positive: Option<bool>, | |||||
| pub issue_id: i32, | |||||
| pub user_id: i64, | |||||
| } | |||||
| #[derive(Insertable)] | |||||
| #[diesel(table_name = crate::schema::issue_votes)] | |||||
| pub struct NewIssueVote { | |||||
| pub positive: bool, | |||||
| pub issue_id: i32, | |||||
| pub user_id: i64, | |||||
| } | |||||
| #[derive(Queryable, Selectable, Serialize, Clone)] | |||||
| #[diesel(table_name = crate::schema::comments)] | |||||
| #[diesel(check_for_backend(diesel::pg::Pg))] | |||||
| pub struct Comment { | |||||
| pub id: i64, | |||||
| pub content: String, | |||||
| pub parent: Option<i64>, | |||||
| pub telegram_handle: String, | |||||
| pub issue_id: i32, | |||||
| pub created_at: Option<NaiveDateTime> | |||||
| } | |||||
| #[derive(Insertable)] | |||||
| #[diesel(table_name = crate::schema::comments)] | |||||
| pub struct NewComment { | |||||
| pub content: String, | |||||
| pub parent: Option<i64>, | |||||
| pub telegram_handle: String, | |||||
| pub issue_id: i32, | |||||
| pub created_at: NaiveDateTime | |||||
| } | |||||
| #[derive(Queryable, Selectable, Serialize, Clone)] | |||||
| #[diesel(table_name = crate::schema::comment_votes)] | |||||
| #[diesel(check_for_backend(diesel::pg::Pg))] | |||||
| pub struct CommentVote { | |||||
| pub id: i64, | |||||
| pub positive: Option<bool>, | |||||
| pub comment_id: i64, | |||||
| pub user_id: i64, | |||||
| } | |||||
| #[derive(Insertable)] | |||||
| #[diesel(table_name = crate::schema::comment_votes)] | |||||
| pub struct NewCommentVote { | |||||
| pub positive: Option<bool>, | |||||
| pub comment_id: Option<i64>, | |||||
| pub user_id: Option<i64>, | |||||
| } | |||||
| @@ -0,0 +1,38 @@ | |||||
| use crate::db::db::establish_connection; | |||||
| use crate::models::models::{Comment, NewComment}; | |||||
| use crate::schema::comments; | |||||
| use chrono::Utc; | |||||
| use diesel::{alias, ExpressionMethods, JoinOnDsl, QueryDsl, RunQueryDsl, SelectableHelper, NullableExpressionMethods}; | |||||
| use crate::schema; | |||||
| pub fn get_issue_comments(issue_id: i32) -> Vec<Comment> { | |||||
| let connection = &mut establish_connection(); | |||||
| let (c1, c2) = alias!(comments as c1, comments as c2); | |||||
| comments::table | |||||
| .left_join(c1.on(comments::parent.eq(comments::id.nullable()))) | |||||
| .filter(comments::issue_id.eq(issue_id)) | |||||
| .select(Comment::as_select()) | |||||
| .load::<Comment>(connection) | |||||
| .unwrap_or_else(|_| Vec::new()) | |||||
| } | |||||
| pub fn create_comment(content: String, parent: Option<i64>, telegram_handle: String, issue_id: i32) -> Comment { | |||||
| let connection = &mut establish_connection(); | |||||
| let new_comment = NewComment { | |||||
| content, | |||||
| parent, | |||||
| telegram_handle, | |||||
| issue_id, | |||||
| created_at: Utc::now().naive_utc() | |||||
| }; | |||||
| let insertion = diesel::insert_into(comments::table) | |||||
| .values(&new_comment) | |||||
| .returning(Comment::as_select()) | |||||
| .get_result(connection) | |||||
| .expect("Error saving comment"); | |||||
| insertion | |||||
| } | |||||
| @@ -0,0 +1,161 @@ | |||||
| use crate::db::db::establish_connection; | |||||
| use crate::models::models::{Issue, IssueWithSummaryAndVotes, NewIssue, NewIssueVote, NewParagraph, Paragraph}; | |||||
| use crate::schema::issue_votes::{issue_id, positive}; | |||||
| use crate::schema::paragraphs; | |||||
| use crate::schema::paragraphs::{content, index}; | |||||
| use crate::schema::{issue_votes, issues}; | |||||
| use chrono::NaiveDateTime; | |||||
| use diesel; | |||||
| use diesel::associations::HasTable; | |||||
| use diesel::dsl::count; | |||||
| use diesel::result::DatabaseErrorKind; | |||||
| use diesel::result::Error::DatabaseError; | |||||
| use diesel::{sql_query, BoolExpressionMethods, ExpressionMethods, QueryDsl, RunQueryDsl, SelectableHelper}; | |||||
| use serde::Deserialize; | |||||
| #[derive(Deserialize)] | |||||
| pub struct FilterOptions { | |||||
| pub min_positive_votes: Option<i64>, | |||||
| pub min_votes: Option<i64>, | |||||
| } | |||||
| pub fn get_issues(filters: Option<FilterOptions>, offset: i32, limit: i16) -> Vec<IssueWithSummaryAndVotes> { | |||||
| let connection = &mut establish_connection(); | |||||
| let query = sql_query("select i.id, title, content as summary, i.paragraph_count, telegram_handle, i.created_at, | |||||
| count(v.id) as total_votes, | |||||
| sum(case when v.positive then 1 else 0 end) as positive_votes | |||||
| from issues i | |||||
| inner join paragraphs p on i.id = p.post_id | |||||
| inner join issue_votes v on i.id = v.issue_id | |||||
| where p.index = 0 | |||||
| group by v.issue_id, title, telegram_handle, content, i.id;"); | |||||
| let mut issues_with_content: Vec<IssueWithSummaryAndVotes> = query | |||||
| .load::<IssueWithSummaryAndVotes>(connection) | |||||
| .unwrap_or_default() | |||||
| .into_iter() | |||||
| .collect(); | |||||
| if let Some(filter_info) = filters { | |||||
| if let Some(positive_votes) = filter_info.min_positive_votes { | |||||
| issues_with_content = issues_with_content | |||||
| .iter() | |||||
| .filter(|i| i.positive_votes >= positive_votes) | |||||
| .map(|i| i.clone()) | |||||
| .collect() | |||||
| } | |||||
| if let Some(min_votes) = filter_info.min_votes { | |||||
| issues_with_content = issues_with_content | |||||
| .iter() | |||||
| .filter(|i| i.total_votes >= min_votes) | |||||
| .map(|i| i.clone()) | |||||
| .collect() | |||||
| } | |||||
| } | |||||
| let len = issues_with_content.len(); | |||||
| let i1 = offset.max(0) as usize; | |||||
| let i2 = i1.saturating_add(limit as usize).min(len); | |||||
| issues_with_content[i1..i2].to_vec() | |||||
| } | |||||
| pub fn create_issue(title: &String, paragraphs: &Vec<String>, telegram_handle: &Option<String>) -> Option<Issue> { | |||||
| let connection = &mut establish_connection(); | |||||
| if let Some(tg_handle) = telegram_handle { | |||||
| let new_issue = NewIssue { | |||||
| title: title.to_string(), | |||||
| paragraph_count: paragraphs.len() as i16, | |||||
| telegram_handle: tg_handle.to_string(), | |||||
| }; | |||||
| let insertion = diesel::insert_into(issues::table) | |||||
| .values(&new_issue) | |||||
| .returning(Issue::as_select()) | |||||
| .get_result(connection) | |||||
| .expect("Error saving new issue"); | |||||
| let _ = diesel::insert_into(paragraphs::table) | |||||
| .values(paragraphs.iter().enumerate().map(|(i, p)| | |||||
| NewParagraph { | |||||
| content: Some(p.clone()), | |||||
| index: i as i32, | |||||
| post_id: insertion.id | |||||
| }).collect::<Vec<NewParagraph>>()) | |||||
| .returning(Paragraph::as_select()) | |||||
| .get_results(connection) | |||||
| .expect("Error saving paragraphs"); | |||||
| return Some(insertion) | |||||
| } | |||||
| None | |||||
| } | |||||
| pub fn get_paragraphs(other_issue_id: i32) -> Vec<Option<String>> { | |||||
| let connection = &mut establish_connection(); | |||||
| paragraphs::table | |||||
| .filter(paragraphs::post_id.eq(other_issue_id)) | |||||
| .order(index) | |||||
| .select(content) | |||||
| .load::<String>(connection) | |||||
| .unwrap_or_default() | |||||
| .into_iter() | |||||
| .map(Some) | |||||
| .collect::<Vec<Option<String>>>() | |||||
| } | |||||
| pub fn record_vote_for_issue(other_positive: bool, other_issue_id: i32, user_id: i64) -> bool { | |||||
| let connection = &mut establish_connection(); | |||||
| let result = diesel::insert_into(issue_votes::table) | |||||
| .values(NewIssueVote { positive: other_positive, issue_id: other_issue_id, user_id }) | |||||
| .execute(connection); | |||||
| match result { | |||||
| Ok(_) => true, | |||||
| Err(DatabaseError(kind, _)) => { | |||||
| if let DatabaseErrorKind::UniqueViolation = kind { | |||||
| let _ = diesel::update( | |||||
| issue_votes::table | |||||
| .filter( | |||||
| issue_votes::user_id.eq(user_id).and(issue_votes::issue_id.eq(issue_id)) | |||||
| ) | |||||
| ) | |||||
| .set(issue_votes::positive.eq(positive)) | |||||
| .execute(connection); | |||||
| return true | |||||
| } | |||||
| false | |||||
| }, | |||||
| _ => false | |||||
| } | |||||
| } | |||||
| pub fn get_issue_vote_equity(issue_identifier: i32) -> i64 { | |||||
| let connection = &mut establish_connection(); | |||||
| let positive_votes: i64 = issue_votes::table | |||||
| .filter(issue_votes::issue_id.eq(issue_identifier).and(issue_votes::positive.eq(true))) | |||||
| .count() | |||||
| .get_result(connection) | |||||
| .unwrap(); | |||||
| let negative_votes: i64 = issue_votes::table | |||||
| .filter(issue_votes::issue_id.eq(issue_identifier).and(issue_votes::positive.eq(false))) | |||||
| .count() | |||||
| .get_result(connection) | |||||
| .unwrap(); | |||||
| positive_votes - negative_votes | |||||
| } | |||||
| pub fn get_vote_for_user(other_issue_id: i32, other_user_id: i64) -> Option<bool> { | |||||
| let connection = &mut establish_connection(); | |||||
| let result: Option<bool> = issue_votes::table | |||||
| .filter(issue_votes::issue_id.eq(other_issue_id).and(issue_votes::user_id.eq(other_user_id))) | |||||
| .limit(1) | |||||
| .select(issue_votes::positive) | |||||
| .get_result::<Option<bool>>(connection) | |||||
| .unwrap_or(None); | |||||
| result | |||||
| } | |||||
| @@ -0,0 +1,3 @@ | |||||
| pub(crate) mod issue; | |||||
| pub(crate) mod session; | |||||
| pub(crate) mod comment; | |||||
| @@ -0,0 +1,66 @@ | |||||
| use diesel::{OptionalExtension, QueryDsl, RunQueryDsl, SelectableHelper}; | |||||
| use diesel::ExpressionMethods; | |||||
| use uuid::Uuid; | |||||
| use crate::db::db::establish_connection; | |||||
| use crate::models::models::{NewSession, Session}; | |||||
| use crate::schema::sessions; | |||||
| use crate::schema::sessions::dsl::sessions as sessions_dsl; | |||||
| use crate::schema::sessions::session_id; | |||||
| pub fn get_session(sess_id: String) -> Option<Session> { | |||||
| let connection = &mut establish_connection(); | |||||
| sessions_dsl | |||||
| .filter(session_id.eq(sess_id)) | |||||
| .select(Session::as_select()) | |||||
| .first(connection) | |||||
| .optional() | |||||
| .expect("Error loading session") | |||||
| } | |||||
| pub fn authenticate_session(user_id: i64, auth_date: i64, username: String, first_name: String, last_name: String, photo_url: String) -> String { | |||||
| let sess_id: Uuid = Uuid::new_v4(); | |||||
| let connection = &mut establish_connection(); | |||||
| let new_session = NewSession { | |||||
| user_id: Some(user_id), | |||||
| session_id: sess_id.clone().to_string(), | |||||
| auth_date, | |||||
| username: Some(username), | |||||
| first_name: Some(first_name), | |||||
| last_name: Some(last_name), | |||||
| photo_url: Some(photo_url), | |||||
| }; | |||||
| let result = diesel::insert_into(sessions::table) | |||||
| .values(&new_session) | |||||
| .returning(Session::as_select()) | |||||
| .get_result(connection) | |||||
| .unwrap(); | |||||
| result.session_id | |||||
| } | |||||
| pub fn get_user_id(sess_id: String) -> Option<i64> { | |||||
| let connection = &mut establish_connection(); | |||||
| let user_id: Option<i64> = sessions::table | |||||
| .filter(session_id.eq(sess_id)) | |||||
| .limit(1) | |||||
| .select(sessions::user_id) | |||||
| .get_result(connection) | |||||
| .optional() | |||||
| .unwrap_or(None); | |||||
| user_id | |||||
| } | |||||
| pub fn get_username_from_session(sess_id: String) -> Option<String> { | |||||
| let connection = &mut establish_connection(); | |||||
| let username = sessions::table | |||||
| .filter(sessions::session_id.eq(sess_id)) | |||||
| .limit(1) | |||||
| .select(sessions::username) | |||||
| .get_result(connection) | |||||
| .unwrap_or_default(); | |||||
| username | |||||
| } | |||||