Collections (#688)

* initial draft; unfinished

* images, fixes

* fixes

* println

* revisions

* fixes

* alternate context setup version

* rev

* partial revs

* rev

* clippy ,fmt

* fmt/clippy/prepare

* fixes

* revs
This commit is contained in:
Wyatt Verchere
2023-09-13 22:22:32 -07:00
committed by GitHub
parent 35cd277fcf
commit 9bd2cb3c7e
30 changed files with 2579 additions and 24 deletions

127
src/models/collections.rs Normal file
View File

@@ -0,0 +1,127 @@
use super::{
ids::{Base62Id, ProjectId},
users::UserId,
};
use crate::database;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
/// The ID of a specific collection, encoded as base62 for usage in the API
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(from = "Base62Id")]
#[serde(into = "Base62Id")]
pub struct CollectionId(pub u64);
/// A collection returned from the API
#[derive(Serialize, Deserialize, Clone)]
pub struct Collection {
/// The ID of the collection, encoded as a base62 string.
pub id: CollectionId,
/// The person that has ownership of this collection.
pub user: UserId,
/// The title or name of the collection.
pub title: String,
/// A short description of the collection.
pub description: String,
/// An icon URL for the collection.
pub icon_url: Option<String>,
/// Color of the collection.
pub color: Option<u32>,
/// The status of the collectin (eg: whether collection is public or not)
pub status: CollectionStatus,
/// The date at which the collection was first published.
pub created: DateTime<Utc>,
/// The date at which the collection was updated.
pub updated: DateTime<Utc>,
/// A list of ProjectIds that are in this collection.
pub projects: Vec<ProjectId>,
}
impl From<database::models::Collection> for Collection {
fn from(c: database::models::Collection) -> Self {
Self {
id: c.id.into(),
user: c.user_id.into(),
created: c.created,
title: c.title,
description: c.description,
updated: c.updated,
projects: c.projects.into_iter().map(|x| x.into()).collect(),
icon_url: c.icon_url,
color: c.color,
status: c.status,
}
}
}
/// A status decides the visibility of a collection in search, URLs, and the whole site itself.
/// Listed - collection is displayed on search, and accessible by URL (for if/when search is implemented for collections)
/// Unlisted - collection is not displayed on search, but accessible by URL
/// Rejected - collection is disabled
#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)]
#[serde(rename_all = "lowercase")]
pub enum CollectionStatus {
Listed,
Unlisted,
Rejected,
Unknown,
}
impl std::fmt::Display for CollectionStatus {
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(fmt, "{}", self.as_str())
}
}
impl CollectionStatus {
pub fn from_str(string: &str) -> CollectionStatus {
match string {
"listed" => CollectionStatus::Listed,
"unlisted" => CollectionStatus::Unlisted,
"rejected" => CollectionStatus::Rejected,
_ => CollectionStatus::Unknown,
}
}
pub fn as_str(&self) -> &'static str {
match self {
CollectionStatus::Listed => "listed",
CollectionStatus::Unlisted => "unlisted",
CollectionStatus::Rejected => "rejected",
CollectionStatus::Unknown => "unknown",
}
}
// Project pages + info cannot be viewed
pub fn is_hidden(&self) -> bool {
match self {
CollectionStatus::Rejected => true,
CollectionStatus::Listed => false,
CollectionStatus::Unlisted => false,
CollectionStatus::Unknown => false,
}
}
pub fn is_approved(&self) -> bool {
match self {
CollectionStatus::Listed => true,
CollectionStatus::Unlisted => true,
CollectionStatus::Rejected => false,
CollectionStatus::Unknown => false,
}
}
pub fn can_be_requested(&self) -> bool {
match self {
CollectionStatus::Listed => true,
CollectionStatus::Unlisted => true,
CollectionStatus::Rejected => false,
CollectionStatus::Unknown => false,
}
}
}

View File

@@ -1,5 +1,7 @@
use thiserror::Error;
pub use super::collections::CollectionId;
pub use super::images::ImageId;
pub use super::notifications::NotificationId;
pub use super::pats::PatId;
pub use super::projects::{ProjectId, VersionId};
@@ -109,6 +111,7 @@ macro_rules! base62_id_impl {
base62_id_impl!(ProjectId, ProjectId);
base62_id_impl!(UserId, UserId);
base62_id_impl!(VersionId, VersionId);
base62_id_impl!(CollectionId, CollectionId);
base62_id_impl!(TeamId, TeamId);
base62_id_impl!(ReportId, ReportId);
base62_id_impl!(NotificationId, NotificationId);
@@ -116,6 +119,7 @@ base62_id_impl!(ThreadId, ThreadId);
base62_id_impl!(ThreadMessageId, ThreadMessageId);
base62_id_impl!(SessionId, SessionId);
base62_id_impl!(PatId, PatId);
base62_id_impl!(ImageId, ImageId);
pub mod base62_impl {
use serde::de::{self, Deserializer, Visitor};

124
src/models/images.rs Normal file
View File

@@ -0,0 +1,124 @@
use super::{
ids::{Base62Id, ProjectId, ThreadMessageId, VersionId},
pats::Scopes,
reports::ReportId,
users::UserId,
};
use crate::database::models::image_item::Image as DBImage;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(from = "Base62Id")]
#[serde(into = "Base62Id")]
pub struct ImageId(pub u64);
#[derive(Serialize, Deserialize)]
pub struct Image {
pub id: ImageId,
pub url: String,
pub size: u64,
pub created: DateTime<Utc>,
pub owner_id: UserId,
// context it is associated with
#[serde(flatten)]
pub context: ImageContext,
}
impl From<DBImage> for Image {
fn from(x: DBImage) -> Self {
let mut context = ImageContext::from_str(&x.context, None);
match &mut context {
ImageContext::Project { project_id } => {
*project_id = x.project_id.map(|x| x.into());
}
ImageContext::Version { version_id } => {
*version_id = x.version_id.map(|x| x.into());
}
ImageContext::ThreadMessage { thread_message_id } => {
*thread_message_id = x.thread_message_id.map(|x| x.into());
}
ImageContext::Report { report_id } => {
*report_id = x.report_id.map(|x| x.into());
}
ImageContext::Unknown => {}
}
Image {
id: x.id.into(),
url: x.url,
size: x.size,
created: x.created,
owner_id: x.owner_id.into(),
context,
}
}
}
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Debug)]
#[serde(tag = "context")]
#[serde(rename_all = "snake_case")]
pub enum ImageContext {
Project {
project_id: Option<ProjectId>,
},
Version {
// version changelogs
version_id: Option<VersionId>,
},
ThreadMessage {
thread_message_id: Option<ThreadMessageId>,
},
Report {
report_id: Option<ReportId>,
},
Unknown,
}
impl ImageContext {
pub fn context_as_str(&self) -> &'static str {
match self {
ImageContext::Project { .. } => "project",
ImageContext::Version { .. } => "version",
ImageContext::ThreadMessage { .. } => "thread_message",
ImageContext::Report { .. } => "report",
ImageContext::Unknown => "unknown",
}
}
pub fn inner_id(&self) -> Option<u64> {
match self {
ImageContext::Project { project_id } => project_id.map(|x| x.0),
ImageContext::Version { version_id } => version_id.map(|x| x.0),
ImageContext::ThreadMessage { thread_message_id } => thread_message_id.map(|x| x.0),
ImageContext::Report { report_id } => report_id.map(|x| x.0),
ImageContext::Unknown => None,
}
}
pub fn relevant_scope(&self) -> Scopes {
match self {
ImageContext::Project { .. } => Scopes::PROJECT_WRITE,
ImageContext::Version { .. } => Scopes::VERSION_WRITE,
ImageContext::ThreadMessage { .. } => Scopes::THREAD_WRITE,
ImageContext::Report { .. } => Scopes::REPORT_WRITE,
ImageContext::Unknown => Scopes::NONE,
}
}
pub fn from_str(context: &str, id: Option<u64>) -> Self {
match context {
"project" => ImageContext::Project {
project_id: id.map(ProjectId),
},
"version" => ImageContext::Version {
version_id: id.map(VersionId),
},
"thread_message" => ImageContext::ThreadMessage {
thread_message_id: id.map(ThreadMessageId),
},
"report" => ImageContext::Report {
report_id: id.map(ReportId),
},
_ => ImageContext::Unknown,
}
}
}

View File

@@ -1,6 +1,8 @@
pub mod analytics;
pub mod collections;
pub mod error;
pub mod ids;
pub mod images;
pub mod notifications;
pub mod pack;
pub mod pats;

View File

@@ -85,8 +85,17 @@ bitflags::bitflags! {
// perform analytics action
const PERFORM_ANALYTICS = 1 << 30;
const ALL = 0b1111111111111111111111111111111;
const NOT_RESTRICTED = 0b00000011111111111111100111;
// create a collection
const COLLECTION_CREATE = 1 << 31;
// read a user's collections
const COLLECTION_READ = 1 << 32;
// write to a collection
const COLLECTION_WRITE = 1 << 33;
// delete a collection
const COLLECTION_DELETE = 1 << 34;
const ALL = 0b11111111111111111111111111111111111;
const NOT_RESTRICTED = 0b111100000011111111111111100111;
const NONE = 0b0;
}
}

View File

@@ -4,7 +4,7 @@ use crate::models::ids::{ProjectId, ThreadId, UserId, VersionId};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Debug)]
#[serde(from = "Base62Id")]
#[serde(into = "Base62Id")]
pub struct ReportId(pub u64);

View File

@@ -35,6 +35,7 @@ bitflags::bitflags! {
const DELETE_PROJECT = 1 << 7;
const VIEW_ANALYTICS = 1 << 8;
const VIEW_PAYOUTS = 1 << 9;
const ALL = 0b1111111111;
}
}

View File

@@ -1,4 +1,4 @@
use super::ids::Base62Id;
use super::ids::{Base62Id, ImageId};
use crate::models::ids::{ProjectId, ReportId};
use crate::models::projects::ProjectStatus;
use crate::models::users::{User, UserId};
@@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize};
#[serde(into = "Base62Id")]
pub struct ThreadId(pub u64);
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Debug)]
#[serde(from = "Base62Id")]
#[serde(into = "Base62Id")]
pub struct ThreadMessageId(pub u64);
@@ -42,6 +42,8 @@ pub enum MessageBody {
#[serde(default)]
private: bool,
replying_to: Option<ThreadMessageId>,
#[serde(default)]
associated_images: Vec<ImageId>,
},
StatusChange {
new_status: ProjectStatus,