~lthms/ogmios

895a1df3fd8fdecf50b211863b6206cce446dfb7 — Thomas Letan 1 year, 8 months ago 3d2039f
feature: Introduce character sheet and validation process

The present commit implements many changes related to character
information. Now, the main idea to keep in mind is that a given
character can have different versions of its information. The current
implementation lacks the actual validation.
A migrations/2018-09-18-204037_alter_characters_table/down.sql => migrations/2018-09-18-204037_alter_characters_table/down.sql +1 -0
@@ 0,0 1,1 @@
(fail)

A migrations/2018-09-18-204037_alter_characters_table/up.sql => migrations/2018-09-18-204037_alter_characters_table/up.sql +14 -0
@@ 0,0 1,14 @@
ALTER TABLE characters
DROP name;

ALTER TABLE characters
DROP history;

ALTER TABLE characters
DROP main;

ALTER TABLE characters
DROP avatar_small;

ALTER TABLE characters
DROP avatar_large;

A migrations/2018-09-18-204038_character_summary_history/down.sql => migrations/2018-09-18-204038_character_summary_history/down.sql +1 -0
@@ 0,0 1,1 @@
DROP TABLE sheets;

A migrations/2018-09-18-204038_character_summary_history/up.sql => migrations/2018-09-18-204038_character_summary_history/up.sql +56 -0
@@ 0,0 1,56 @@
CREATE TABLE sheets (
  id             SERIAL PRIMARY KEY,
  character_id   INTEGER NOT NULL REFERENCES characters,
  created        TIMESTAMP NOT NULL DEFAULT now(),
  validated      TIMESTAMP,
  current        BOOLEAN,
  future         BOOLEAN,
  invalidated    TIMESTAMP,

  name          VARCHAR(80) NOT NULL,
  avatar_small  INTEGER REFERENCES images,
  avatar_large  INTEGER REFERENCES images,

  history       INTEGER NOT NULL REFERENCES documents,

  CONSTRAINT validation_process CHECK(
    (invalidated >= validated) AND (validated >= created)
  ),

  CONSTRAINT invalidated_has_been_validated CHECK(
    /* invalidated is not null -> validated is not null */
    (invalidated IS NULL) OR (validated IS NOT NULL)
  ),

  /* there is only one “current” sheet per character (we rely on the fact that
     NULL is ignored for uniqueness) */
  CONSTRAINT current_or_null   CHECK(current),
  CONSTRAINT one_sheet_current UNIQUE (character_id, current),

  CONSTRAINT current_is_validated CHECK(
    /* current is not null -> validated is not null */
    (current IS NULL) OR (validated IS NOT NULL)
  ),
  CONSTRAINT invalidated_is_not_current CHECK(
    /* invalidated is not null -> current is null */
    (invalidated IS NULL) OR (current IS NULL)
  ),

  CONSTRAINT future_or_null    CHECK(future),
  CONSTRAINT one_sheet_future  UNIQUE (character_id, future),

  CONSTRAINT future_is_not_validated CHECK(
    /* future is not null -> validated is null */
    (future IS NULL) OR (validated IS NULL)
  ),

  CONSTRAINT current_has_small_avatar CHECK(
    /* current is not null -> avatar_small is not null */
    (current IS NULL) OR (avatar_small IS NOT NULL)
  ),

  CONSTRAINT current_has_large_avatar CHECK(
    /* current is not null -> avatar_small is not null */
    (current IS NULL) OR (avatar_large IS NOT NULL)
  )
);

M src/main.rs => src/main.rs +5 -3
@@ 43,7 43,7 @@ use db::{PgPool, PgConn, establish_connection};

use ::errors::{Error, ResultExt};
use ::models::user::User;
use ::models::character::Character;
use ::models::sheet::Sheet;
use ::models::image::{ImageId, Image};

#[get("/", rank=1)]


@@ 64,14 64,14 @@ fn index_auth(
        .get_result::<i64>(conn.get())
        .chain_err(|| "Could not query users table")?;

    let characters = Character::find_by_owner(user.id, &conn)?;
    let sheets = Sheet::get_current_sheets_of(user.id, &conn)?;

    Ok(Template::render("index", json!({
        "msg": msg,
        "nb": nb,
        "user": json!({
            "default": user.default_character,
            "characters": characters,
            "characters": sheets,
            "nickname": user.nickname
        }),
        "hash": env!("GIT_HASH")


@@ 131,6 131,8 @@ fn run() -> Result <(), Error> {
            routes::session::select_character_auth,
            routes::document::submit_document,
            routes::document::render_document,
            routes::account::account_index,
            routes::characters::get_current_sheet,
            static_files,
        ])
        .manage(conn)

M src/models/character.rs => src/models/character.rs +12 -50
@@ 1,52 1,32 @@
use diesel;
use diesel::prelude::*;

use galatian;
use galatian::typography::French;
use galatian::html::Html;

use ::db::PgConn;
use ::errors::{Error, ResultExt};
use ::models::id::{CharacterId, UserId};
use ::models::entity::{EntityId, Entity, Role};
use ::models::image::ImageId;
use ::models::document::{DocumentId, Document, Lang};
use ::models::sheet::{Sheet, Content};
use ::schema::characters;

#[derive(Queryable)]
struct CharacterDb {
    id: i32,
    entity_id: i32,
    name: String,
    history: i32,
    owner: Option<i32>,
    main: bool,
    avatar_small: Option<i32>,
    avatar_large: Option<i32>,
}

#[derive(Insertable)]
#[table_name="characters"]
pub struct NewCharacter {
    entity_id: i32,
    name: String,
    history: i32,
    owner: Option<i32>,
    main: bool,
    avatar_small: Option<i32>,
    avatar_large: Option<i32>,
}

#[derive(Clone, Serialize, Deserialize)]
pub struct Character {
    pub id: CharacterId,
    pub entity_id: EntityId,
    pub history_id: DocumentId,
    pub name: String,
    pub owner: Option<UserId>,
    pub main: bool,
    pub avatar_small: ImageId,
    pub avatar_large: ImageId,
}

impl From<CharacterDb> for Character {


@@ 54,57 34,39 @@ impl From<CharacterDb> for Character {
        Character {
            id: CharacterId::from(e.id),
            entity_id: EntityId::from(e.entity_id),
            history_id: DocumentId::from(e.history),
            name: e.name,
            owner: e.owner.map(UserId::from),
            main: e.main,
            avatar_small: ImageId::from(e.avatar_small.unwrap_or(1)),
            avatar_large: ImageId::from(e.avatar_large.unwrap_or(2)),
        }
    }
}

impl Character {
    pub fn create(
        name: String,
        owner: Option<UserId>,
        history: String,
        main: bool,
        avatar_small: Option<ImageId>,
        avatar_large: Option<ImageId>,
        sheet: Content,
        conn: &PgConn
    ) -> Result<Character, Error> {
        let rendered = galatian::compile(history.as_str(), &French, &Html)
            .map_err::<Error, _>(|_| {
                "Could not parse galatian document for some reason".into()
            })?;

        conn.transaction(|| {
            let entity = Entity::create(Role::Character, conn)
                .chain_err(|| {
                    "Could not create a new entity for a new character"
                })?;

            let doc = Document::create(
                history,
                rendered.into_string(),
                Lang::Galatian,
                conn
            )?;

            diesel::insert_into(characters::table)
            let character = diesel::insert_into(characters::table)
                .values(&NewCharacter {
                    entity_id: entity,
                    history: doc.id.into(),
                    name: name,
                    owner: owner.map(|x| x.into()),
                    main: main,
                    avatar_small: avatar_small.map(|x| x.into()),
                    avatar_large: avatar_large.map(|x| x.into()),
                })
                .get_result::<CharacterDb>(conn.get())
                .chain_err(|| "Could not create a new character")
                .map(Character::from)
                .map(Character::from)?;

            Sheet::try_create_first_sheet(
                character.id,
                sheet,
                &conn
            )?;

            Ok(character)
        })
    }


M src/models/document.rs => src/models/document.rs +34 -1
@@ 5,6 5,11 @@ use ::errors::{ResultExt, Error};
use ::schema::documents;
use ::db::PgConn;

use galatian;
use galatian::html::Html;
use galatian::typography::English;

#[derive(Clone, Serialize, Deserialize)]
pub enum Lang {
    Galatian
}


@@ 55,6 60,7 @@ struct NewDocument {
    rendered: String,
}

#[derive(Clone, Serialize, Deserialize)]
pub struct Document {
  pub id: DocumentId,
  pub language: Lang,


@@ 72,12 78,28 @@ impl Document {
        })
    }

    fn render(
        raw: &String,
        lang: &Lang,
    ) -> Result<String, Error> {
        match lang {
            Lang::Galatian => {
                galatian::compile(raw.as_str(), &English, &Html)
                    .map_err(|_| {
                        "could not compile galatian document for some reason".into()
                    })
                    .map(|r| r.into_string())
            }
        }
    }

    pub fn create(
        raw: String,
        rendered: String,
        lang: Lang,
        conn: &PgConn
    ) -> Result<Document, Error> {
        let rendered = Document::render(&raw, &lang)?;

        diesel::insert_into(documents::table)
            .values(&NewDocument {
                language: lang.to_i16(),


@@ 88,4 110,15 @@ impl Document {
            .chain_err(|| "Could not create a new entity")
            .and_then(Document::try_from)
    }

    pub fn get(
        id: DocumentId,
        conn: &PgConn,
    ) -> Result<Document, Error> {
        documents::table
            .filter(documents::id.eq(id.into(): i32))
            .get_result::<DocumentDb>(conn.get())
            .chain_err(|| "Hem?")
            .and_then(Document::try_from)
    }
}

M src/models/id.rs => src/models/id.rs +15 -0
@@ 1,4 1,19 @@
#[derive(Clone, Copy, Serialize, Deserialize)]
pub struct Id<A>(i32, ::std::marker::PhantomData<A>);

impl<A> From<i32> for Id<A> {
    fn from(id: i32) -> Id<A> {
        Id(id, ::std::marker::PhantomData)
    }
}

impl<A> Into<i32> for Id<A> {
    fn into(self) -> i32 {
        self.0
    }
}

#[derive(Clone, Copy, Serialize, Deserialize)]
pub struct UserId(i32);

impl From<i32> for UserId {

M src/models/mod.rs => src/models/mod.rs +1 -0
@@ 3,4 3,5 @@ pub mod image;
pub mod user;
pub mod entity;
pub mod document;
pub mod sheet;
pub mod character;

A src/models/sheet.rs => src/models/sheet.rs +320 -0
@@ 0,0 1,320 @@
use std::time::SystemTime;

use diesel;
use diesel::prelude::*;

use ::errors::{ResultExt, Error};
use ::schema::*;
use ::db::PgConn;
use ::models::id::{Id, CharacterId, UserId};
use ::models::image::ImageId;
use ::models::document::{DocumentId, Document};

#[derive(Queryable)]
struct SheetDb {
    id:            i32,
    character_id:  i32,
    created:       SystemTime,
    validated:     Option<SystemTime>,
    current:       Option<bool>,
    future:        Option<bool>,
    invalidated:   Option<SystemTime>,
    name:          String,
    avatar_small:  Option<i32>,
    avatar_large:  Option<i32>,
    history:       i32,
}

#[derive(Clone, Serialize, Deserialize)]
pub enum Validation {
    Old(SystemTime),
    Current(SystemTime),
    Next,
    Invalidated(SystemTime, SystemTime),
}

impl Validation {
    pub fn decide(
        current: bool,
        future: bool,
        validated: Option<SystemTime>,
        invalidated: Option<SystemTime>,
    ) -> Option<Validation> {
        match (current, future, validated, invalidated) {
            (false, true, None, None) => Some(Validation::Next),
            (true, false, Some(val), None) => Some(Validation::Current(val)),
            (false, false, Some(val), None) => Some(Validation::Old(val)),
            (false, false, Some(val), Some(inval)) => Some(Validation::Invalidated(val, inval)),
            _ => None,
        }
    }
}

#[derive(Clone, Serialize, Deserialize)]
pub struct Content {
    pub name:          String,
    pub avatar_small:  Option<ImageId>,
    pub avatar_large:  Option<ImageId>,
    pub history:       DocumentId,
}

#[derive(Clone, Serialize, Deserialize)]
pub struct Sheet {
    pub id:            Id<Sheet>,
    pub character_id:  CharacterId,
    pub created:       SystemTime,
    pub validation:    Validation,
    pub content:       Content,
}

impl Sheet {
    fn try_from(origin: SheetDb) -> Result<Sheet, Error> {
        Ok(Sheet {
            id: Id::from(origin.id),
            character_id: CharacterId::from(origin.character_id),
            created: origin.created,
            validation: Validation::decide(
                origin.current.unwrap_or(false),
                origin.future.unwrap_or(false),
                origin.validated,
                origin.invalidated,
            ).ok_or("Invalid db scheme".into(): Error)?,
            content: Content {
                name: origin.name,
                avatar_small: origin.avatar_small.map(|x| x.into()),
                avatar_large: origin.avatar_large.map(|x| x.into()),
                history: origin.history.into(),
            },
        })
    }
}

#[derive(Insertable)]
#[table_name="sheets"]
struct NewSheet {
    pub character_id:  i32,
    pub created:       SystemTime,
    pub name:          String,
    pub avatar_small:  Option<i32>,
    pub avatar_large:  Option<i32>,
    pub history:       i32,
    pub future:        Option<bool>,
}

impl Sheet {
    pub fn get_future_sheets_of(
        user: UserId,
        conn: &PgConn,
    ) -> Result<Vec<Sheet>, Error> {
        let mut sheets = characters::table
            .filter(characters::owner.eq(Some(user.into(): i32)))
            .inner_join(sheets::table)
            .select(sheets::all_columns)
            .filter(sheets::future.eq(Some(true)))
            .get_results::<SheetDb>(conn.get())
            .chain_err(|| "")?;

        let res = sheets
            .drain(..)
            .fold(
                Ok(Vec::new()),
                |res: Result<Vec<Sheet>, Error>, x| {
                    match res {
                        Ok(mut res) => {
                            res.push(Sheet::try_from(x)?);
                            Ok(res)
                        },
                        err => err,
                    }
                }
            )?;

        Ok(res)
    }

    pub fn get_current_sheets_of(
        user: UserId,
        conn: &PgConn,
    ) -> Result<Vec<Sheet>, Error> {
        let mut sheets = characters::table
            .filter(characters::owner.eq(Some(user.into(): i32)))
            .inner_join(sheets::table)
            .select(sheets::all_columns)
            .filter(sheets::current.eq(Some(true)))
            .get_results::<SheetDb>(conn.get())
            .chain_err(|| "")?;

        let res = sheets
            .drain(..)
            .fold(
                Ok(Vec::new()),
                |res: Result<Vec<Sheet>, Error>, x| {
                    match res {
                        Ok(mut res) => {
                            res.push(Sheet::try_from(x)?);
                            Ok(res)
                        },
                        err => err,
                    }
                }
            )?;

        Ok(res)
    }

    pub fn try_create_first_sheet(
        character_id:  CharacterId,
        content:       Content,
        conn:          &PgConn
    ) -> Result<Sheet, Error> {
        conn.transaction(|| {
            diesel::insert_into(sheets::table)
                .values(&NewSheet {
                    character_id: character_id.into(),
                    created: SystemTime::now(),
                    name:    content.name,
                    avatar_small: content.avatar_small.map(|x| x.into()),
                    avatar_large: content.avatar_large.map(|x| x.into()),
                    history: content.history.into(),
                    future:  Some(true),
                })
                .get_result::<SheetDb>(conn.get())
                .chain_err(|| "Hum")
                .and_then(Sheet::try_from)
        })
    }

    fn get_current_sheet_db(
        character:  i32,
        conn:       &PgConn,
    ) -> Result<Option<SheetDb>, Error> {
        sheets::table
            .filter(sheets::character_id.eq(character)
                    .and(sheets::current.eq(Some(true))))
            .limit(1)
            .get_results::<SheetDb>(conn.get())
            .chain_err(|| "")
            .map(|mut res|  res.pop())
    }

    pub fn get_current_sheet(
        character:  CharacterId,
        conn:       &PgConn,
    ) -> Result<Option<Sheet>, Error> {
        Sheet::get_current_sheet_db(character.into(): i32, &conn)
            .and_then(|res| {
                if let Some(res) = res {
                    Ok(Some(Sheet::try_from(res)?))
                } else {
                    Ok(None)
                }
            })
    }

    fn get_future_sheet_db(
        character:  i32,
        conn:       &PgConn,
    ) -> Result<Option<SheetDb>, Error> {
        sheets::table
            .filter(sheets::character_id.eq(character)
                    .and(sheets::future.eq(Some(true))))
            .limit(1)
            .get_results::<SheetDb>(conn.get())
            .chain_err(|| "")
            .map(|mut res|  res.pop())
    }

    pub fn get_future_sheet(
        character:  CharacterId,
        conn:       &PgConn,
    ) -> Result<Option<Sheet>, Error> {
        Sheet::get_future_sheet_db(character.into(): i32, &conn)
            .and_then(|res| {
                if let Some(res) = res {
                    Ok(Some(Sheet::try_from(res)?))
                } else {
                    Ok(None)
                }
            })
    }

    pub fn validate_future_sheet(
        character:  CharacterId,
        conn:       &PgConn,
    ) -> Result<bool, Error> {
        let current = Sheet::get_current_sheet_db(character.into(), conn)?;
        let future = Sheet::get_future_sheet_db(character.into(), conn)?;

        match (current, future) {
            (Some(current), Some(future)) => {
                conn.transaction(|| {
                    /* step one, unmark the current one */
                    diesel::update(sheets::table.find(current.id.into(): i32))
                        .set(sheets::current.eq(None: Option<bool>))
                        .execute(conn.get())
                        .chain_err(|| "could not unmark the current sheet")?;

                    /* step two, mark the future one as current */
                    diesel::update(sheets::table.find(future.id.into(): i32))
                        .set((
                            sheets::future.eq(None: Option<bool>),
                            sheets::current.eq(Some(true)),
                            sheets::validated.eq(Some(SystemTime::now()))
                        ))
                        .execute(conn.get())
                        .chain_err(|| "could not mark the future sheet as current")?;

                    Ok(true)
                })
            },
            (None, Some(future)) => {
                /* mark the future one as current */
                diesel::update(sheets::table.find(future.id.into(): i32))
                    .set((
                        sheets::future.eq(None: Option<bool>),
                        sheets::current.eq(Some(true)),
                        sheets::validated.eq(Some(SystemTime::now()))
                    ))
                    .execute(conn.get())
                    .chain_err(|| "..")?;

                Ok(true)
            },
            _ => {
                // nothing to do
                Ok(false)
            }
        }
    }
}

#[derive(Clone, Serialize, Deserialize)]
pub struct SheetExtended {
    pub character_id:  CharacterId,
    pub created:       SystemTime,
    pub validation:    Validation,
    pub name:          String,
    pub avatar_small:  Option<ImageId>,
    pub avatar_large:  Option<ImageId>,
    pub history:       Document,
}

impl SheetExtended {
    pub fn extend_from(
        sh: Sheet,
        conn: &PgConn
    ) -> Result<SheetExtended, Error> {
        let history = Document::get(sh.content.history, conn)?;

        Ok(SheetExtended {
            character_id: sh.character_id,
            created: sh.created,
            validation: sh.validation,
            name: sh.content.name,
            avatar_small: sh.content.avatar_small,
            avatar_large: sh.content.avatar_large,
            history: history

        })
    }
}

A src/routes/account/mod.rs => src/routes/account/mod.rs +29 -0
@@ 0,0 1,29 @@
use rocket_contrib::Template;

use ::db::PgConn;
use ::errors::Error;
use ::models::user::User;
use ::models::sheet::Sheet;

#[get("/account")]
pub fn account_index(
    user: User,
    conn: PgConn,
) -> Result<Template, Error> {
    let currents = Sheet::get_current_sheets_of(user.id, &conn)?;
    let futures = Sheet::get_future_sheets_of(user.id, &conn)?;

    Ok(Template::render(
        "account_index",
        json!({
            "hash": env!("GIT_HASH"),
            "currents": currents,
            "futures": futures,
            "user": json!({
                "default": user.default_character,
                "characters": currents,
                "nickname": user.nickname
            }),
        })
    ))
}

A src/routes/characters.rs => src/routes/characters.rs +49 -0
@@ 0,0 1,49 @@
use rocket_contrib::Template;

use ::db::PgConn;
use ::errors::Error;
use ::models::user::User;
use ::models::sheet::{Sheet, SheetExtended};
use ::models::id::CharacterId;

#[get("/character/<id>/current")]
pub fn get_current_sheet(
    user: Option<User>,
    conn: PgConn,
    id: i32,
) -> Result<Template, Error> {
    let id = CharacterId::from(id);

    let sheet = match Sheet::get_current_sheet(id, &conn)? {
        Some(sheet) => {
            Some(SheetExtended::extend_from(sheet, &conn)?)
        },
        None => {
            None
        },
    };

    let user = match user {
        Some(user) => {
            let sheets = Sheet::get_current_sheets_of(user.id, &conn)?;

            Some(json!({
                "default": user.default_character,
                "characters": sheets,
                "nickname": user.nickname
            }))
        }
        _ => {
            None
        }
    };

    Ok(Template::render(
        "character_current",
        json!({
            "sheet": sheet,
            "hash": env!("GIT_HASH"),
            "user": user,
        })
    ))
}

M src/routes/document.rs => src/routes/document.rs +30 -23
@@ 5,8 5,10 @@ use ::db::PgConn;
use ::models::image::Image;
use ::models::user::User;
use ::models::character::Character;

use errors::Error;
use ::models::sheet::Sheet;
use ::models::document::{Lang, Document};
use ::models::sheet::Content;
use ::errors::Error;

#[get("/document")]
pub fn submit_document(


@@ 14,12 16,12 @@ pub fn submit_document(
    user: User,
) -> Result<Template, Error> {

    let characters = Character::find_by_owner(user.id, &conn)?;
    let sheets = Sheet::get_current_sheets_of(user.id, &conn)?;

    Ok(Template::render("document", json!({
        "user": json!({
            "default": user.default_character,
            "characters": characters,
            "characters": sheets,
            "nickname": user.nickname
        }),
        "hash": env!("GIT_HASH")


@@ 42,28 44,33 @@ pub fn render_document<'a>(
    submission: Form<RenderRequest>,
    conn: PgConn,
) -> Result<Template, Error> {
    let submission = submission.get();
    conn.transaction(|| {
        let submission = submission.into_inner();

    let small_id = Image::create_data_url(&submission.avatar_small, &conn)?
        .map(|x| x.id);
    let large_id = Image::create_data_url(&submission.avatar_large, &conn)?
        .map(|x| x.id);
        let small_id = Image::create_data_url(&submission.avatar_small, &conn)?
            .map(|x| x.id);
        let large_id = Image::create_data_url(&submission.avatar_large, &conn)?
            .map(|x| x.id);

    Character::create(
        submission.name.clone(),
        Some(user.id),
        submission.history.clone(),
        true,
        small_id,
        large_id,
        &conn,
    )?;
        let history_id = Document::create(
            submission.history,
            Lang::Galatian,
            &conn
        )?.id;

    let characters = Character::find_by_owner(user.id, &conn)?;
        Character::create(
            Some(user.id),
            Content {
                name: submission.name,
                avatar_small: small_id,
                avatar_large: large_id,
                history: history_id,
            },
            &conn,
        )
    })?;

    if user.default_character.is_none() && characters.len() > 0 {
        user.set_default_character(&conn, characters[0].id)?;
    }
    let sheets = Sheet::get_current_sheets_of(user.id, &conn)?;

    Ok(
        Template::render(


@@ 72,7 79,7 @@ pub fn render_document<'a>(
            "hash": env!("GIT_HASH"),
            "user": json!({
                "default": user.default_character,
                "characters": characters,
                "characters": sheets,
                "nickname": user.nickname
            }),
        }))

M src/routes/mod.rs => src/routes/mod.rs +2 -0
@@ 1,2 1,4 @@
pub mod session;
pub mod document;
pub mod account;
pub mod characters;

M src/schema.rs => src/schema.rs +19 -6
@@ 2,12 2,7 @@ table! {
    characters (id) {
        id -> Int4,
        entity_id -> Int4,
        name -> Varchar,
        history -> Int4,
        owner -> Nullable<Int4>,
        main -> Bool,
        avatar_small -> Nullable<Int4>,
        avatar_large -> Nullable<Int4>,
    }
}



@@ 61,6 56,22 @@ table! {
}

table! {
    sheets (id) {
        id -> Int4,
        character_id -> Int4,
        created -> Timestamp,
        validated -> Nullable<Timestamp>,
        current -> Nullable<Bool>,
        future -> Nullable<Bool>,
        invalidated -> Nullable<Timestamp>,
        name -> Varchar,
        avatar_small -> Nullable<Int4>,
        avatar_large -> Nullable<Int4>,
        history -> Int4,
    }
}

table! {
    users (id) {
        id -> Int4,
        nickname -> Varchar,


@@ 68,11 79,12 @@ table! {
    }
}

joinable!(characters -> documents (history));
joinable!(characters -> entities (entity_id));
joinable!(event_participants -> entities (entity_id));
joinable!(event_participants -> events (event_id));
joinable!(events -> documents (description));
joinable!(sheets -> characters (character_id));
joinable!(sheets -> documents (history));

allow_tables_to_appear_in_same_query!(
    characters,


@@ 82,5 94,6 @@ allow_tables_to_appear_in_same_query!(
    events,
    images,
    relationships,
    sheets,
    users,
);

A templates/account_index.html.tera => templates/account_index.html.tera +48 -0
@@ 0,0 1,48 @@
{% extends "base" %}

{% block content %}
<section class="ui text container">
  <h1 class="ui header">
    {{ user.nickname }}
  </h1>

  <p>
    Here is the list of your characters.
    <a href="/document">Want to create a new one?</a>
  </p>

  {% if futures %}
  <h2 class="ui header">
    To Be Validated
  </h2>

  <ul>
    {% for character in futures %}
    <li>
      <img class="ui avatar image"
           src="/img/{{ character.content.avatar_small }}" />
      {{ character.content.name }}
    </li>
    {% endfor %}
  </ul>
  {% endif %}

  {% if currents %}
  <h2 class="ui header">
    Active Characters
  </h2>

  <ul>
    {% for character in currents %}
    <li>
      <img class="ui avatar image"
           src="/img/{{ character.content.avatar_small }}" />
      <a href="/character/{{ character.character_id }}/current">
        {{ character.content.name }}
      </a>
    </li>
    {% endfor %}
  </ul>
  {% endif %}
</section>
{% endblock content %}

M templates/base.html.tera => templates/base.html.tera +5 -5
@@ 5,9 5,9 @@
    <meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no" />
    <meta name="referrer" content="no-referrer">
    <title>ogmios</title>
    <link rel="stylesheet" type="text/css" href="static/semantic/semantic.min.css" />
    <link rel="stylesheet" type="text/css" href="static/croppie.css" />
    <link rel="stylesheet" type="text/css" href="static/main.css" />
    <link rel="stylesheet" type="text/css" href="/static/semantic/semantic.min.css" />
    <link rel="stylesheet" type="text/css" href="/static/croppie.css" />
    <link rel="stylesheet" type="text/css" href="/static/main.css" />
  </head>
  <body>
  <header class="ui text fluid">


@@ 39,8 39,8 @@
    integrity="sha256-hVVnYaiADRTO2PzUGmuLJr8BLUSjGIZsDYGmIJLv2b8="
    crossorigin="anonymous">
  </script>
  <script src="static/croppie.min.js"></script>
  <script src="static/semantic/semantic.min.js"></script>
  <script src="/static/croppie.min.js"></script>
  <script src="/static/semantic/semantic.min.js"></script>
  <script>
    $('#characterSelection')
    .dropdown({

A templates/character_current.html.tera => templates/character_current.html.tera +21 -0
@@ 0,0 1,21 @@
{% extends "base" %}

{% block content %}
<section class="ui text container">
  {% if sheet %}
  <h1 class="ui header">
    {{ sheet.name }}
  </h1>

  <img src="/img/{{ sheet.avatar_large }}">

  <h2 class="ui header">History</h2>

  {{ sheet.history.rendered | safe }}
  {% else %}
  <p>
    This character, assuming it exists, has not been validated yet.
  </p>
  {% endif %}
</section>
{% endblock content %}

M templates/partials/navbar-auth.html.tera => templates/partials/navbar-auth.html.tera +5 -5
@@ 14,16 14,16 @@
  <div class="default text">Manage your characters</div>
  <div class="menu">
    {% for character in user.characters %}
    <div class="item {% if user.default == character.id %} selected {% endif %}"
         data-value={{ character.id }}>
    <div class="item {% if user.default == character.character_id %} selected {% endif %}"
         data-value={{ character.character_id }}>
      <img class="ui avatar image"
           src="/img/{{ character.avatar_small }}" />
      {{ character.name }}
           src="/img/{{ character.content.avatar_small }}" />
      {{ character.content.name }}
    </div>
    {% endfor %}
    <div class="divider"></div>
    <div class="item" data-value="manage">
      <a href="/manage-characters">
      <a href="/account">
        Manage your characters
      </a>
    </div>