~lthms/ogmios

8bd16df76869ebfd1aa822f6214ac3cd35caeb5b — Thomas Letan 1 year, 8 months ago 895a1df
feature: Implement a complete validation process for characters
M src/main.rs => src/main.rs +5 -0
@@ 133,6 133,11 @@ fn run() -> Result <(), Error> {
            routes::document::render_document,
            routes::account::account_index,
            routes::characters::get_current_sheet,
            routes::characters::get_future_sheet,
            routes::characters::validate_future_sheet,
            routes::characters::new_future_sheet,
            routes::characters::edit_future_sheet,
            routes::characters::edit_future_sheet_post,
            static_files,
        ])
        .manage(conn)

M src/models/character.rs => src/models/character.rs +1 -1
@@ 60,7 60,7 @@ impl Character {
                .chain_err(|| "Could not create a new character")
                .map(Character::from)?;

            Sheet::try_create_first_sheet(
            Sheet::try_create_future_sheet(
                character.id,
                sheet,
                &conn

M src/models/document.rs => src/models/document.rs +40 -3
@@ 7,7 7,7 @@ use ::db::PgConn;

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

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


@@ 37,7 37,7 @@ struct DocumentDb {
  rendered: String,
}

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

impl From<i32> for DocumentId {


@@ 84,7 84,7 @@ impl Document {
    ) -> Result<String, Error> {
        match lang {
            Lang::Galatian => {
                galatian::compile(raw.as_str(), &English, &Html)
                galatian::compile(raw.as_str(), &French, &Html)
                    .map_err(|_| {
                        "could not compile galatian document for some reason".into()
                    })


@@ 111,6 111,23 @@ impl Document {
            .and_then(Document::try_from)
    }

    pub fn edit(
        id: DocumentId,
        raw: String,
        conn: &PgConn,
    ) -> Result<(), Error> {
        // get the first document
        let doc = Document::get(id, conn)?;
        let rendered = Document::render(&raw, &doc.language)?;

        diesel::update(documents::table.find(id.into(): i32))
            .set(documents::rendered.eq(rendered))
            .execute(conn.get())
            .chain_err(|| "still no")?;

        Ok(())
    }

    pub fn get(
        id: DocumentId,
        conn: &PgConn,


@@ 121,4 138,24 @@ impl Document {
            .chain_err(|| "Hem?")
            .and_then(Document::try_from)
    }

    pub fn duplicate(
        id: DocumentId,
        conn: &PgConn
    ) -> Result<DocumentId, Error> {
        let doc = documents::table
            .filter(documents::id.eq(id.into(): i32))
            .get_result::<DocumentDb>(conn.get())
            .chain_err(|| "Fuh")?;

        diesel::insert_into(documents::table)
            .values(&NewDocument {
                language: doc.language,
                raw: doc.raw,
                rendered: doc.rendered,
            })
            .get_result::<DocumentDb>(conn.get())
            .chain_err(|| "Foo")
            .map(|x| DocumentId::from(x.id))
    }
}

M src/models/sheet.rs => src/models/sheet.rs +44 -2
@@ 162,7 162,7 @@ impl Sheet {
        Ok(res)
    }

    pub fn try_create_first_sheet(
    pub fn try_create_future_sheet(
        character_id:  CharacterId,
        content:       Content,
        conn:          &PgConn


@@ 224,6 224,49 @@ impl Sheet {
            .map(|mut res|  res.pop())
    }

    pub fn edit_future_sheet(
        character:    CharacterId,
        name:         String,
        history:      String,
        avatar_small: Option<ImageId>,
        avatar_large: Option<ImageId>,
        conn:         &PgConn,
    ) -> Result<(), Error> {
        let sheet = Sheet::get_future_sheet(character, conn)?
            .ok_or("no future sheet to edit".into(): Error)?;

        conn.transaction(|| {
            // edit the history
            let history_id = sheet.content.history;
            Document::edit(history_id, history, conn)?;

            let id: i32 = sheet.id.into();

            // edit the rest of the sheet
            // FIXME: only one request should be required
            diesel::update(sheets::table.find(id))
                .set(sheets::name.eq(name))
                .execute(conn.get())
                .chain_err(|| "nop")?;

            if let Some(_) = avatar_small {
                diesel::update(sheets::table.find(id))
                    .set(sheets::avatar_small.eq(avatar_small.map(|x| x.into(): i32)))
                    .execute(conn.get())
                    .chain_err(|| "nop")?;
            }

            if let Some(_) = avatar_large {
                diesel::update(sheets::table.find(id))
                    .set(sheets::avatar_large.eq(avatar_large.map(|x| x.into(): i32)))
                    .execute(conn.get())
                    .chain_err(|| "nop")?;
            }

            Ok(())
        })
    }

    pub fn get_future_sheet(
        character:  CharacterId,
        conn:       &PgConn,


@@ 314,7 357,6 @@ impl SheetExtended {
            avatar_small: sh.content.avatar_small,
            avatar_large: sh.content.avatar_large,
            history: history

        })
    }
}

M src/routes/characters.rs => src/routes/characters.rs +157 -1
@@ 1,17 1,28 @@
use rocket::response::{Flash, Redirect};
use rocket::request::{Form, FlashMessage};
use rocket_contrib::Template;

use ::db::PgConn;
use ::errors::Error;
use ::models::image::Image;
use ::models::user::User;
use ::models::sheet::{Sheet, SheetExtended};
use ::models::sheet::{Sheet, SheetExtended, Content};
use ::models::document::Document;
use ::models::id::CharacterId;
use ::routes::document::RenderRequest;

#[get("/character/<id>/current")]
pub fn get_current_sheet(
    user: Option<User>,
    conn: PgConn,
    id: i32,
    msg: Option<FlashMessage>,
) -> Result<Template, Error> {
    let msg = msg.map(|msg| json!({
        "name": msg.name(),
        "msg":  msg.msg()
    }));

    let id = CharacterId::from(id);

    let sheet = match Sheet::get_current_sheet(id, &conn)? {


@@ 41,9 52,154 @@ pub fn get_current_sheet(
    Ok(Template::render(
        "character_current",
        json!({
            "msg": msg,
            "sheet": sheet,
            "hash": env!("GIT_HASH"),
            "user": user,
        })
    ))
}

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

    let sheet = match Sheet::get_future_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_future",
        json!({
            "sheet": sheet,
            "hash": env!("GIT_HASH"),
            "user": user,
        })
    ))
}

#[post("/character/<id>/future/validate")]
pub fn validate_future_sheet(
    _user: User,
    conn: PgConn,
    id: i32,
) -> Result<Flash<Redirect>, Error> {
    Sheet::validate_future_sheet(CharacterId::from(id), &conn)?;

    Ok(Flash::success(
        Redirect::to(&format!("/character/{}/current", id)),
        "Sheet has been validated"
    ))
}

#[post("/character/<id>/future/new")]
pub fn new_future_sheet(
    user: User,
    conn: PgConn,
    id: i32,
) -> Result<Flash<Redirect>, Error> {
    let id = CharacterId::from(id);
    let sheet = Sheet::get_current_sheet(id.into(), &conn)?
        .ok_or("Character does not have a validated method".into(): Error)?;

    conn.transaction(|| {
        let content = Content {
            name: sheet.content.name,
            avatar_small: sheet.content.avatar_small,
            avatar_large: sheet.content.avatar_large,
            history: Document::duplicate(sheet.content.history, &conn)?
        };

        Sheet::try_create_future_sheet(id, content, &conn)
    })?;

    Ok(Flash::success(
        Redirect::to(&format!("/character/{}/future", id.into(): i32)),
        "Sheet has been validated"
    ))
}

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

    let sheets = Sheet::get_current_sheets_of(user.id, &conn)?;
    let sheet = Sheet::get_future_sheet(id.into(), &conn)?
        .ok_or("Character does not have a sheet to edit".into(): Error)
        .and_then(|x| SheetExtended::extend_from(x, &conn))?;

    Ok(
        Template::render(
            "character_edit",
            json!({
                "target": format!("/character/{}/future/edit", id.into(): i32),
                "hash": env!("GIT_HASH"),
                "previous": sheet,
                "user": json!({
                    "default": user.default_character,
                    "characters": sheets,
                    "nickname": user.nickname
                }),
            })
        )
    )
}

#[post("/character/<id>/future/edit", data="<submission>")]
pub fn edit_future_sheet_post(
    user: User,
    submission: Form<RenderRequest>,
    conn: PgConn,
    id: i32,
) -> Result<Flash<Redirect>, Error> {
    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);

        Sheet::edit_future_sheet(
            CharacterId::from(id),
            submission.name,
            submission.history,
            small_id,
            large_id,
            &conn,
        )
    })?;

    Ok(Flash::success(
        Redirect::to(&format!("/character/{}/future", id)),
        "Sheet has been updated"
    ))
}

M src/routes/document.rs => src/routes/document.rs +7 -6
@@ 24,18 24,19 @@ pub fn submit_document(
            "characters": sheets,
            "nickname": user.nickname
        }),
        "target": "/document",
        "hash": env!("GIT_HASH")
    })))
}

#[derive(FromForm)]
pub struct RenderRequest {
    name: String,
    avatar_large: String,
    avatar_small: String,
    appearance: String,
    personality: String,
    history: String,
    pub name: String,
    pub avatar_large: String,
    pub avatar_small: String,
    pub appearance: String,
    pub personality: String,
    pub history: String,
}

#[post("/document", data="<submission>")]

M templates/account_index.html.tera => templates/account_index.html.tera +3 -1
@@ 21,7 21,9 @@
    <li>
      <img class="ui avatar image"
           src="/img/{{ character.content.avatar_small }}" />
      {{ character.content.name }}
      <a href="/character/{{ character.character_id }}/future">
        {{ character.content.name }}
      </a>
    </li>
    {% endfor %}
  </ul>

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

{% block content %}
<section class="ui text container">
  {% if sheet %}
  <img class="ui image right floated" src="/img/{{ sheet.avatar_large }}" />

  <h1 class="ui header">
    {{ sheet.name }}
  </h1>

  {% block characternav %}
  {% endblock characternav %}

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

  {{ sheet.history.rendered | safe }}
  {% else %}
  {% block characternotfound %}
  {% endblock characternotfound %}
  {% endif %}
</section>
{% endblock content %}

M templates/character_current.html.tera => templates/character_current.html.tera +23 -17
@@ 1,21 1,27 @@
{% extends "base" %}
{% extends "character" %}

{% block content %}
<section class="ui text container">
  {% if sheet %}
  <h1 class="ui header">
    {{ sheet.name }}
  </h1>
{% block characternav %}
{% if future %}
<a href="/character/{{ sheet.character_id }}/future">
  <button class="ui button">
    See next version
  </button>
</a>
{% else %}

  <img src="/img/{{ sheet.avatar_large }}">
{% if user %}
<form method="post" action="/character/{{ sheet.character_id }}/future/new">
  <button class="ui button positive submit">
    Start an update
  </button>
</form>
{% endif %}

  <h2 class="ui header">History</h2>
{% endif %}
{% endblock characternav %}

  {{ sheet.history.rendered | safe }}
  {% else %}
  <p>
    This character, assuming it exists, has not been validated yet.
  </p>
  {% endif %}
</section>
{% endblock content %}
{% block characternotfound %}
<p>
  There is nothing to see here.
</p>
{% endblock characternotfound %}

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

{% block content %}
<section class="ui text container">
  <h1 class="ui header">
    Edit character sheet
  </h1>

  {% include "partials/character_sheet" %}
</section>
{% endblock content %}

{% block script %}
<script>
  var $avatar_large;
  var $avatar_small;
  var $avatars = false;

  function readImage(input) {
      if (input.files && input.files[0]) {
          var reader = new FileReader();

          reader.onload = function (e) {
              if (!$avatars) {
                  $avatars = true;
                  $avatar_large.toggle();
                  $avatar_small.toggle();
              }

              $avatar_large.croppie('bind', {
                  url: e.target.result
              });

              $avatar_small.croppie('bind', {
                  url: e.target.result
              });
          };

          reader.readAsDataURL(input.files[0]);
      }
  }

  $('#avatar_upload').on('change', function () {
      readImage(this);
  });

  $avatar_large = $('#avatar_cropper_large').croppie({
      viewport: {
          width: 180,
          height: 400,
          type: 'square'
      },
      boundary: {
          width: 216,
          height: 480
      },
      enableExif: true
  });
  $avatar_large.toggle();

  $avatar_small = $('#avatar_cropper_small').croppie({
      viewport: {
          width: 150,
          height: 150,
          type: 'square'
      },
      boundary: {
          width: 200,
          height: 200
      },
      enableExif: true
  });
  $avatar_small.toggle();

  $('#character_submit').on('click', function () {
      if ($avatars) {
          $avatar_small.croppie('result', {
              type: 'base64',
              size: 'viewport',
              format: 'png'
          }).then(function (e) {
              $('#character_create input[name=avatar_small]').val(e);

              $avatar_large.croppie('result', {
                  type: 'base64',
                  size: 'viewport',
                  format: 'png'
              }).then(function (e) {
                  $('#character_create input[name=avatar_large]').val(e);

                  $('#character_create').submit();
              });
          });
      } else {
          $('#character_create').submit();
      }
  });
</script>
{% endblock script %}

A templates/character_future.html.tera => templates/character_future.html.tera +31 -0
@@ 0,0 1,31 @@
{% extends "character" %}

{% block characternav %}
{% if current %}
<a href="/character/{{ sheet.character_id }}/current">
  <button class="ui button">
    See validated version
  </button>
</a>
{% endif %}

{% if user %}
<form method="post" action="/character/{{ sheet.character_id }}/future/validate">
  <button class="ui button positive submit">
    Validate
  </button>
</form>
{% endif %}

<a href="/character/{{ sheet.character_id }}/future/edit">
  <button class="ui button icon">
    <i class="icon edit"></i>
  </button>
</a>
{% endblock characternav %}

{% block characternotfound %}
<p>
  There is nothing to see here.
</p>
{% endblock characternotfound %}

M templates/document.html.tera => templates/document.html.tera +1 -50
@@ 6,56 6,7 @@
    Create a new character
  </h1>

  <form action="/document" method="post" class="ui form" id="character_create">
    <h2 class="ui dividing header">Summary</h2>

    <div class="field">
      <label>Full Name</label>
      <input type="text" name="name" placeholder="Full Name" />
    </div>

    <h2 class="ui dividing header">Descriptions</h2>

    <div class="field">
      <input id="avatar_upload" value="Choose a file" accept="image/*" type="file">
    </div>

    <div class="inline fields">
      <div class="field">
        <input type="hidden" name="avatar_large" />

        <div id="avatar_cropper_large">
        </div>
      </div>

      <div class="field">
        <input type="hidden" name="avatar_small" />

        <div id="avatar_cropper_small">
        </div>
      </div>
    </div>

    <div class="field">
      <label>Appearance</label>
      <textarea name="appearance" placeholder="Describe your character's appearance."></textarea>
    </div>

    <div class="field">
      <label>Personality</label>
      <textarea name="personality" placeholder="Describe your character's personality."></textarea>
    </div>

    <h2 class="ui dividing header">Story</h2>

    <div class="field">
      <label>History</label>
      <textarea name="history" rows="18" placeholder="Tell your character's story."></textarea>
    </div>
    <div class="field">
      <div id="character_submit" class="ui button submit">Create</div>
    </div>
  </form>
  {% include "partials/character_sheet" %}
</section>
{% endblock content %}


A templates/partials/character_sheet.html.tera => templates/partials/character_sheet.html.tera +57 -0
@@ 0,0 1,57 @@
  <form action="{{ target }}" method="post" class="ui form" id="character_create">
    <h2 class="ui dividing header">Summary</h2>

    <div class="field">
      <label>Full Name</label>
      <input type="text" name="name" placeholder="Full Name"
             {% if previous %}
             value="{{ previous.name }}"
             {% endif %}/>
    </div>

    <h2 class="ui dividing header">Descriptions</h2>

    <div class="field">
      <input id="avatar_upload" value="Choose a file" accept="image/*" type="file">
    </div>

    <div class="inline fields">
      <div class="field">
        <input type="hidden" name="avatar_large" />

        <div id="avatar_cropper_large">
        </div>
      </div>

      <div class="field">
        <input type="hidden" name="avatar_small" />

        <div id="avatar_cropper_small">
        </div>
      </div>
    </div>

    <div class="field">
      <label>Appearance</label>
      <textarea name="appearance" placeholder="Describe your character's appearance."></textarea>
    </div>

    <div class="field">
      <label>Personality</label>
      <textarea name="personality" placeholder="Describe your character's personality."></textarea>
    </div>

    <h2 class="ui dividing header">Story</h2>

    <div class="field">
      <label>History</label>
      <textarea
        name="history"
        rows="18"
        placeholder="Tell your character's story."
        >{% if previous %}{{ previous.history.raw | safe }}{% endif %}</textarea>
    </div>
    <div class="field">
      <div id="character_submit" class="ui button submit">Create</div>
    </div>
  </form>