feat: ability to hide tags

This commit is contained in:
Artemis 2025-04-02 12:46:29 +02:00
parent 1988abe369
commit 9580711d6b
19 changed files with 158 additions and 76 deletions

View file

@ -97,6 +97,11 @@
"ordinal": 18,
"name": "archived_at",
"type_info": "Timestamptz"
},
{
"ordinal": 19,
"name": "is_public",
"type_info": "Bool"
}
],
"parameters": {
@ -123,7 +128,8 @@
true,
true,
false,
true
true,
false
]
},
"hash": "164d77651f1f1b9ec7a28343db098305486e025bc4a5e71279a62da807ecea79"

View file

@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n\t\t\tupdate doll_profiles\n\t\t\tset microchip_id = $1,\n\t\t\tname = $2,\n\t\t\tpronoun_subject = $3,\n\t\t\tpronoun_object = $4,\n\t\t\tpronoun_possessive = $5,\n\t\t\thandler_name = $6,\n\t\t\thandler_link = $7,\n\t\t\tkind = $8,\n\t\t\tbreed = $9,\n\t\t\tbehaviour = $10,\n\t\t\tdescription = $11,\n\t\t\tchassis_type = $12,\n\t\t\tchassis_id = $13,\n\t\t\tchassis_color = $14,\n\t\t\tarchived_at = null,\n\t\t\tupdated_at = current_timestamp\n\t\t\twhere id = $15 and bound_to_id = $16\n\t\t",
"query": "\n\t\t\tupdate doll_profiles\n\t\t\tset microchip_id = $1,\n\t\t\tname = $2,\n\t\t\tpronoun_subject = $3,\n\t\t\tpronoun_object = $4,\n\t\t\tpronoun_possessive = $5,\n\t\t\thandler_name = $6,\n\t\t\thandler_link = $7,\n\t\t\tkind = $8,\n\t\t\tbreed = $9,\n\t\t\tbehaviour = $10,\n\t\t\tdescription = $11,\n\t\t\tchassis_type = $12,\n\t\t\tchassis_id = $13,\n\t\t\tchassis_color = $14,\n\t\t\tis_public = $15,\n\t\t\tarchived_at = null,\n\t\t\tupdated_at = current_timestamp\n\t\t\twhere id = $16 and bound_to_id = $17\n\t\t",
"describe": {
"columns": [],
"parameters": {
@ -19,11 +19,12 @@
"Varchar",
"Varchar",
"Varchar",
"Bool",
"Int4",
"Uuid"
]
},
"nullable": []
},
"hash": "5e6bd41ff3105cb2c41c82e0bb4ea973e358c702857c05158b32c8bc81c8942e"
"hash": "3152a8cb3d5be0a8be3b6e75754fa1d0fdbf2fc64b7086792d776318c1cec454"
}

View file

@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n\t\t\t\tselect * from doll_profiles where (id = $1 or microchip_id = $2) and archived_at is null\n\t\t\t",
"query": "select * from doll_profiles where (id = $1 or microchip_id = $2)",
"describe": {
"columns": [
{
@ -97,6 +97,11 @@
"ordinal": 18,
"name": "archived_at",
"type_info": "Timestamptz"
},
{
"ordinal": 19,
"name": "is_public",
"type_info": "Bool"
}
],
"parameters": {
@ -124,8 +129,9 @@
true,
true,
false,
true
true,
false
]
},
"hash": "03800471afd396ebcabb3dfd9fb7523989c0d0af621aeea8c7480dede8ceef1b"
"hash": "32ee0b9c347300695e7aff02c940eb851984a761e4ce64239645088588d505da"
}

View file

@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "select id from doll_profiles where id = $1 or microchip_id = $2",
"query": "select id from doll_profiles where id = $1",
"describe": {
"columns": [
{
@ -11,13 +11,12 @@
],
"parameters": {
"Left": [
"Int4",
"Text"
"Int4"
]
},
"nullable": [
false
]
},
"hash": "44834b8a95718d0ae8ffc96e93469c6e0b4e6ca1160f7ada141f9515a6921ec9"
"hash": "70736384c06084a3967b2d008f4c843d239a6a36ae02b1e2fd306498a04746af"
}

View file

@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n\t\t\tinsert into doll_profiles\n\t\t\t(id, microchip_id, name, pronoun_subject, pronoun_object, pronoun_possessive, handler_name, handler_link, kind, breed, behaviour, description, chassis_type, chassis_id, chassis_color, bound_to_id)\n\t\t\tvalues ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)\n\t\t",
"query": "\n\t\t\tinsert into doll_profiles\n\t\t\t(id, microchip_id, name, pronoun_subject, pronoun_object, pronoun_possessive, handler_name, handler_link, kind, breed, behaviour, description, chassis_type, chassis_id, chassis_color, bound_to_id, is_public)\n\t\t\tvalues ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)\n\t\t",
"describe": {
"columns": [],
"parameters": {
@ -20,10 +20,11 @@
"Varchar",
"Varchar",
"Varchar",
"Uuid"
"Uuid",
"Bool"
]
},
"nullable": []
},
"hash": "a3d6e4ddfa10505e777ccb7a184e17a57c3620cd56ae3858f33db020c6be42f2"
"hash": "7d5fef137e77f2ed7a13958a4bd345eccbded1a4d838e7953dd906b484ff9a91"
}

View file

@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n\t\t\tupdate doll_profiles\n\t\t\tset microchip_id = null,\n\t\t\tname = '',\n\t\t\tpronoun_subject = '',\n\t\t\tpronoun_object = '',\n\t\t\tpronoun_possessive = '',\n\t\t\thandler_name = '',\n\t\t\thandler_link = null,\n\t\t\tkind = null,\n\t\t\tbreed = null,\n\t\t\tbehaviour = null,\n\t\t\tdescription = null,\n\t\t\tchassis_type = null,\n\t\t\tchassis_id = null,\n\t\t\tchassis_color = null,\n\t\t\tupdated_at = current_timestamp,\n\t\t\tarchived_at = current_timestamp\n\t\t\twhere id = $1 and bound_to_id = $2\n\t\t",
"query": "\n\t\t\tupdate doll_profiles\n\t\t\tset microchip_id = null,\n\t\t\tname = '',\n\t\t\tpronoun_subject = '',\n\t\t\tpronoun_object = '',\n\t\t\tpronoun_possessive = '',\n\t\t\thandler_name = '',\n\t\t\thandler_link = null,\n\t\t\tkind = null,\n\t\t\tbreed = null,\n\t\t\tbehaviour = null,\n\t\t\tdescription = null,\n\t\t\tchassis_type = null,\n\t\t\tchassis_id = null,\n\t\t\tchassis_color = null,\n\t\t\tis_public = false,\n\t\t\tupdated_at = current_timestamp,\n\t\t\tarchived_at = current_timestamp\n\t\t\twhere id = $1 and bound_to_id = $2\n\t\t",
"describe": {
"columns": [],
"parameters": {
@ -11,5 +11,5 @@
},
"nullable": []
},
"hash": "fd4b05dbaea47ab4a0da33ba4ba4f05eec757e511c686690346e4c3cc1fb3a28"
"hash": "baec493b4eb0998f3e5e0be8b8bb4740630a222b3bef2d7d32ac61928734d30d"
}

View file

@ -97,6 +97,11 @@
"ordinal": 18,
"name": "archived_at",
"type_info": "Timestamptz"
},
{
"ordinal": 19,
"name": "is_public",
"type_info": "Bool"
}
],
"parameters": {
@ -123,7 +128,8 @@
true,
true,
false,
true
true,
false
]
},
"hash": "e3234918965fd36a56fc48fc78956b53c8a84f08e0424d63e930f9dcfc449175"

View file

@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n\t\t\t\tselect * from doll_profiles where (id = $1 or microchip_id = $2)\n\t\t\t",
"query": "select * from doll_profiles where (id = $1 or microchip_id = $2) and archived_at is null and is_public is true",
"describe": {
"columns": [
{
@ -97,6 +97,11 @@
"ordinal": 18,
"name": "archived_at",
"type_info": "Timestamptz"
},
{
"ordinal": 19,
"name": "is_public",
"type_info": "Bool"
}
],
"parameters": {
@ -124,8 +129,9 @@
true,
true,
false,
true
true,
false
]
},
"hash": "b032b09996d538f01c221d2d2b09563c1a0a9164416b3f5c08de54c2f5b19fa4"
"hash": "f46606ed7e634ac9ef528348868b4f05eca9df12fda01f9042939aca4689d3f1"
}

6
TODO.md Normal file
View file

@ -0,0 +1,6 @@
1. change the tag access status filter into a struct rust-side
2. go over all access routes currently in place and double-check / update them
3. add a way to discern public and private tags inside the tag list (account/index)
4. add a way to toggle that (account/index)
5. tag create / edit form: add a "save in private" btn that saves the tag but in private mode
6. audit: private/public switch

View file

@ -281,8 +281,14 @@ p.note {
font-size: .9em;
}
div.submit {
margin: 2em 0;
display: flex;
justify-content: space-around;
align-items: center;
}
button.submit {
margin: 2em auto;
font-size: 1.2em;
display: block;
}
@ -451,6 +457,6 @@ pre.recovery-key {
@media screen and (min-width: 800px) {
.dual-fields>*:last-child {
flex: 2;
flex: 1;
}
}

View file

@ -0,0 +1,2 @@
alter table doll_profiles
add column is_public boolean not null default true;

View file

@ -3,6 +3,7 @@ use uuid::{uuid, Uuid};
use super::schema::{CreateDollProfile, DbHook, TrxHook};
/// Lists all the unarchived tags this account has
pub async fn list(db: &mut DbHook, from: &Uuid) -> sqlx::Result<Vec<DollProfile>> {
sqlx::query_as!(
DollProfile,
@ -13,6 +14,7 @@ pub async fn list(db: &mut DbHook, from: &Uuid) -> sqlx::Result<Vec<DollProfile>
.await
}
/// Lists the IDs of archived tags that are bound to this account
pub async fn list_archived(db: &mut DbHook, from: &Uuid) -> sqlx::Result<Vec<i32>> {
sqlx::query_scalar!(
"select id from doll_profiles where bound_to_id = $1 and archived_at is not null",
@ -22,6 +24,7 @@ pub async fn list_archived(db: &mut DbHook, from: &Uuid) -> sqlx::Result<Vec<i32
.await
}
/// Lists all the user's tags
pub async fn list_all(db: &mut DbHook, from: &Uuid) -> sqlx::Result<Vec<DollProfile>> {
sqlx::query_as!(
DollProfile,
@ -32,37 +35,38 @@ pub async fn list_all(db: &mut DbHook, from: &Uuid) -> sqlx::Result<Vec<DollProf
.await
}
/// Tries to get the requested tag based on the provided ID or microchip ID.
/// Will only include public non-archived tags.
pub async fn get_public(
db: &mut DbHook,
ident: i32,
microchip_id: &str,
) -> sqlx::Result<Option<DollProfile>> {
sqlx::query_as!(
DollProfile,
r#"select * from doll_profiles where (id = $1 or microchip_id = $2) and archived_at is null and is_public is true"#,
ident,
microchip_id
).fetch_optional(&mut **db)
.await
}
pub async fn get(
db: &mut DbHook,
ident: i32,
microchip_id: &str,
include_archived: bool,
) -> sqlx::Result<Option<DollProfile>> {
if include_archived {
sqlx::query_as!(
DollProfile,
r#"
select * from doll_profiles where (id = $1 or microchip_id = $2)
"#,
ident,
microchip_id
)
.fetch_optional(&mut **db)
.await
} else {
sqlx::query_as!(
DollProfile,
r#"
select * from doll_profiles where (id = $1 or microchip_id = $2) and archived_at is null
"#,
ident,
microchip_id
)
.fetch_optional(&mut **db)
.await
}
sqlx::query_as!(
DollProfile,
r#"select * from doll_profiles where (id = $1 or microchip_id = $2)"#,
ident,
microchip_id
)
.fetch_optional(&mut **db)
.await
}
/// Checks if some of the provided IDs are already used, returning the list of IDs that are used
pub async fn check_ids(db: &mut DbHook, idents: &Vec<i32>) -> sqlx::Result<Vec<i32>> {
sqlx::query_scalar!(
"select id from doll_profiles where id in (select * from unnest($1::int[]))",
@ -72,23 +76,23 @@ pub async fn check_ids(db: &mut DbHook, idents: &Vec<i32>) -> sqlx::Result<Vec<i
.await
}
pub async fn id_exists(db: &mut DbHook, ident: i32, microchip_id: &str) -> sqlx::Result<bool> {
Ok(sqlx::query!(
"select id from doll_profiles where id = $1 or microchip_id = $2",
ident,
microchip_id
/// Checks if a tag exists with this ID (in global, collision check method)
pub async fn id_exists(db: &mut DbHook, ident: i32) -> sqlx::Result<bool> {
Ok(
sqlx::query!("select id from doll_profiles where id = $1", ident)
.fetch_optional(&mut **db)
.await?
.is_some(),
)
.fetch_optional(&mut **db)
.await?
.is_some())
}
/// Creates a new tag using the form data from [`CreateDollProfile`]
pub async fn create(db: &mut DbHook, doll: CreateDollProfile<'_>) -> sqlx::Result<()> {
sqlx::query!(
r#"
insert into doll_profiles
(id, microchip_id, name, pronoun_subject, pronoun_object, pronoun_possessive, handler_name, handler_link, kind, breed, behaviour, description, chassis_type, chassis_id, chassis_color, bound_to_id)
values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)
(id, microchip_id, name, pronoun_subject, pronoun_object, pronoun_possessive, handler_name, handler_link, kind, breed, behaviour, description, chassis_type, chassis_id, chassis_color, bound_to_id, is_public)
values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)
"#,
doll.id,
doll.microchip_id,
@ -106,11 +110,13 @@ pub async fn create(db: &mut DbHook, doll: CreateDollProfile<'_>) -> sqlx::Resul
doll.chassis_id,
doll.chassis_color,
doll.bound_to_id,
doll.is_public,
).execute(&mut **db).await?;
Ok(())
}
/// Edits the given tag with the create/edit form,
/// editing a doll_profile will also unarchive it
pub async fn edit(
db: &mut DbHook,
@ -134,9 +140,10 @@ pub async fn edit(
chassis_type = $12,
chassis_id = $13,
chassis_color = $14,
is_public = $15,
archived_at = null,
updated_at = current_timestamp
where id = $15 and bound_to_id = $16
where id = $16 and bound_to_id = $17
"#,
doll.microchip_id,
doll.name,
@ -152,6 +159,7 @@ pub async fn edit(
doll.chassis_type,
doll.chassis_id,
doll.chassis_color,
doll.is_public,
doll.id,
bound_account_id
)
@ -161,7 +169,7 @@ pub async fn edit(
Ok(())
}
/// deleting a doll profile only wipes the data associated to it but retains two bits of info:
/// deleting (or archiving) a doll profile only wipes the data associated to it but retains two bits of info:
/// - the tag's ID
/// - the account which created this tag
///
@ -187,6 +195,7 @@ pub async fn delete(trx: &mut TrxHook<'_>, id: i32, bound_account_id: &Uuid) ->
chassis_type = null,
chassis_id = null,
chassis_color = null,
is_public = false,
updated_at = current_timestamp,
archived_at = current_timestamp
where id = $1 and bound_to_id = $2

View file

@ -23,6 +23,7 @@ pub struct DollProfile {
pub created_at: chrono::DateTime<Utc>,
pub updated_at: Option<chrono::DateTime<Utc>>,
pub archived_at: Option<chrono::DateTime<Utc>>,
pub is_public: bool,
pub bound_to_id: Uuid,
@ -58,6 +59,7 @@ pub struct CreateDollProfile<'a> {
pub id: i32,
pub microchip_id: Option<&'a str>,
pub bound_to_id: &'a Uuid,
pub is_public: bool,
pub name: &'a str,
pub pronoun_subject: &'a str,

View file

@ -85,10 +85,10 @@ pub async fn ask_delete(
user: User,
meta: CommonTemplateState,
) -> PageResult {
let db_tag = doll::get(&mut *db, id, "", false).await?;
let db_tag = doll::get(&mut *db, id, "").await?;
if let Some(tag) = db_tag {
if tag.bound_to_id != user.id {
if tag.bound_to_id != user.id || tag.archived_at.is_some() {
Ok(Redirect::to(uri!("/account", index)).into())
} else {
Ok(Template::render(

View file

@ -115,7 +115,6 @@ pub async fn handle_in_page_forms(
id_public_to_db(values.tag_id)
.expect("is form-validated so should always succeed"),
"",
true,
)
.await?;
if target_tag.is_none() {

View file

@ -41,6 +41,7 @@ impl From<DollProfile> for FakeContext {
"microchip_id",
vec![tag.microchip_id.unwrap_or(String::from(""))],
),
("is_public", vec![tag.is_public.to_string()]),
("name", vec![tag.name]),
("pronoun_subject", vec![tag.pronoun_subject]),
("pronoun_object", vec![tag.pronoun_object]),
@ -87,7 +88,7 @@ pub async fn show_edit_tag(
Some(v) => v,
None => return Ok(Redirect::to(uri!("/account", account::common::index)).into()),
};
let tag = match doll::get(&mut *db, normalized_id, "", true).await? {
let tag = match doll::get(&mut *db, normalized_id, "").await? {
Some(v) => {
if v.bound_to_id != user.id {
return Ok(Redirect::to(uri!("/account", account::common::index)).into());
@ -102,7 +103,7 @@ pub async fn show_edit_tag(
"register_tag",
context! {
mode: "edit",
id,
id: normalized_id,
previous: FakeContext::from(tag),
meta,
},
@ -117,6 +118,8 @@ pub struct TagForm<'a> {
#[field(validate=len(..32))]
pub microchip_id: &'a str,
pub is_public: bool,
#[field(validate=len(1..=256))]
pub name: &'a str,
#[field(validate=len(1..=32))]
@ -205,7 +208,7 @@ pub async fn handle_register(
let normalized_microchip_id = tag.microchip_id.to_lowercase();
let microchip_id = normalize_opt(&normalized_microchip_id);
if doll::id_exists(&mut *db, id, microchip_id.unwrap_or("")).await? {
if doll::id_exists(&mut *db, id).await? {
// TODO: that's weird... what was i expecting to do here?
return Ok(Redirect::found(uri!("/account", show_register)).into());
}
@ -229,6 +232,7 @@ pub async fn handle_register(
chassis_id: normalize_opt(tag.chassis_id),
chassis_color: normalize_opt(tag.chassis_color),
bound_to_id: &user.id,
is_public: tag.is_public,
},
)
.await;
@ -250,7 +254,11 @@ pub async fn handle_register(
tag.ident
);
Ok(Redirect::to(uri!(public::show_profile(Some(tag.ident), microchip_id))).into())
if tag.is_public {
Ok(Redirect::to(uri!(public::show_profile(Some(tag.ident), microchip_id))).into())
} else {
Ok(Redirect::to(uri!(account::common::index)).into())
}
}
#[post("/edit_tag/<id>", data = "<tag>")]
@ -316,9 +324,14 @@ pub async fn handle_edit_tag(
chassis_id: normalize_opt(tag.chassis_id),
chassis_color: normalize_opt(tag.chassis_color),
bound_to_id: &user.id,
is_public: tag.is_public,
},
)
.await?;
Ok(Redirect::to(uri!(public::show_profile(Some(tag.ident), microchip_id))).into())
if tag.is_public {
Ok(Redirect::to(uri!(public::show_profile(Some(tag.ident), microchip_id))).into())
} else {
Ok(Redirect::to(uri!(account::common::index)).into())
}
}

View file

@ -47,7 +47,7 @@ pub async fn show_profile(
let internal_id = ident.and_then(|v| id_public_to_db(v)).unwrap_or(0);
let microchip_id = microchip_id.unwrap_or("");
let profile = match doll::get(&mut *db, internal_id, microchip_id, false).await? {
let profile = match doll::get_public(&mut *db, internal_id, microchip_id).await? {
Some(p) => p,
None => return Ok(Redirect::to(uri!(index(Some(true), ident, Some(microchip_id)))).into()),
};

View file

@ -26,6 +26,8 @@
<input type="checkbox" name="anti_bot" style="display: none">
<button type="submit" class="submit">Log in</button>
<div class="submit">
<button type="submit" class="submit">Log in</button>
</div>
</form>
{% endblock main %}

View file

@ -1,10 +1,11 @@
{% extends "base" %}
{% import "macros/form" as form %}
{% block title %}{% if mode == "register" %}Register a new tag{% else %}Edit {{id}}{% endif %} - {% endblock title
{% block title %}{% if mode == "register" %}Register a new tag{% else %}Edit {{id|pretty_id}}{% endif %} - {% endblock
title
%}
{% block main %}
<h2>{% if mode == "register" %}Register a new tag{% else %}Edit {{id}}{% endif %}</h2>
<h2>{% if mode == "register" %}Register a new tag{% else %}Edit {{id|pretty_id}}{% endif %}</h2>
<aside>
<h3>A foreword</h3>
@ -205,13 +206,30 @@
</div>
</section>
<button class="submit" type="submit">
{% if mode == "register" %}
Register this tag!
{% else %}
Save your changes
{% endif %}
</button>
<div class="submit">
<div>
{% set tag_vis = previous.values | get(key="is_public", default=[]) %}
{% set is_public = tag_vis | length > 0 and tag_vis | first == "true" %}
<label for="is_public">Who can see the tag?</label>
<select name="is_public" id="is_public">
<option value="true">Everyone, it's public</option>
<option value="false" {% if is_public==false %}selected{% endif %}>No one, keep it private for now
</option>
</select>
{{form::error(ctx=previous, name="is_public")}}
</div>
<div>
<button class="submit" type="submit">
{% if mode == "register" %}
Register this tag!
{% else %}
Save your changes
{% endif %}
</button>
</div>
</div>
</form>
</section>