1
0
mirror of https://github.com/actix/examples synced 2024-11-23 22:41:07 +01:00

Merge pull request #39 from munckymagik/actix-todo

actix-todo example
This commit is contained in:
Douman 2018-09-05 13:04:39 +03:00 committed by GitHub
commit 6b68cfc529
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 1617 additions and 0 deletions

View File

@ -37,6 +37,7 @@ script:
cd async_db && cargo check && cd ..
cd async_ex1 && cargo check && cd ..
cd actix_redis && cargo check && cd ..
cd actix_todo && cargo check && cd ..
cd basics && cargo check && cd ..
cd cookie-auth && cargo check && cd ..
cd cookie-auth-full && cargo check && cd ..

View File

@ -2,6 +2,7 @@
members = [
"./",
"actix_redis",
"actix_todo",
"async_db",
"async_ex1",
"basics",

1
actix_todo/.env Normal file
View File

@ -0,0 +1 @@
DATABASE_URL=postgres://localhost/actix_todo

20
actix_todo/Cargo.toml Normal file
View File

@ -0,0 +1,20 @@
[package]
authors = ["Dan Munckton <dangit@munckfish.net>"]
name = "actix-todo"
version = "0.1.0"
[dependencies]
actix = "0.7.3"
actix-web = "0.7.4"
dotenv = "0.13.0"
env_logger = "0.5.10"
futures = "0.1.22"
log = "0.4.3"
serde = "1.0.69"
serde_derive = "1.0.69"
serde_json = "1.0.22"
tera = "0.11.8"
[dependencies.diesel]
features = ["postgres", "r2d2"]
version = "1.3.2"

48
actix_todo/README.md Normal file
View File

@ -0,0 +1,48 @@
# actix-todo
A port of the [Rocket Todo example](https://github.com/SergioBenitez/Rocket/tree/master/examples/todo) into [actix-web](https://actix.rs/). Except this uses PostgreSQL instead of SQLite.
# Usage
## Prerequisites
* Rust >= 1.26
* PostgreSQL >= 9.5
## Change into the project sub-directory
All instructions assume you have changed into this folder:
```bash
cd examples/actix_todo
```
## Set up the database
Install the [diesel](http://diesel.rs) command-line tool including the `postgres` feature:
```bash
cargo install diesel_cli --no-default-features --features postgres
```
Check the contents of the `.env` file. If your database requires a password, update `DATABASE_URL` to be of the form:
```.env
DATABASE_URL=postgres://username:password@localhost/actix_todo
```
Then to create and set-up the database run:
```bash
diesel database setup
```
## Run the application
To run the application execute:
```bash
cargo run
```
Then to view it in your browser navigate to: [http://localhost:8088/](http://localhost:8088/)

5
actix_todo/diesel.toml Normal file
View File

@ -0,0 +1,5 @@
# For documentation on how to configure this file,
# see diesel.rs/guides/configuring-diesel-cli
[print_schema]
file = "src/schema.rs"

View File

View File

@ -0,0 +1,6 @@
-- This file was automatically created by Diesel to setup helper functions
-- and other internal bookkeeping. This file is safe to edit, any future
-- changes will be added to existing projects as new migrations.
DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass);
DROP FUNCTION IF EXISTS diesel_set_updated_at();

View File

@ -0,0 +1,36 @@
-- This file was automatically created by Diesel to setup helper functions
-- and other internal bookkeeping. This file is safe to edit, any future
-- changes will be added to existing projects as new migrations.
-- Sets up a trigger for the given table to automatically set a column called
-- `updated_at` whenever the row is modified (unless `updated_at` was included
-- in the modified columns)
--
-- # Example
--
-- ```sql
-- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW());
--
-- SELECT diesel_manage_updated_at('users');
-- ```
CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$
BEGIN
EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s
FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl);
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$
BEGIN
IF (
NEW IS DISTINCT FROM OLD AND
NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at
) THEN
NEW.updated_at := current_timestamp;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;

View File

@ -0,0 +1 @@
DROP TABLE tasks

View File

@ -0,0 +1,5 @@
CREATE TABLE tasks (
id SERIAL PRIMARY KEY,
description VARCHAR NOT NULL,
completed BOOLEAN NOT NULL DEFAULT 'f'
);

176
actix_todo/src/api.rs Normal file
View File

@ -0,0 +1,176 @@
use actix::prelude::Addr;
use actix_web::middleware::Response;
use actix_web::{
error, fs::NamedFile, http, AsyncResponder, Form, FutureResponse, HttpRequest,
HttpResponse, Path, Responder, Result,
};
use futures::{future, Future};
use tera::{Context, Tera};
use db::{AllTasks, CreateTask, DbExecutor, DeleteTask, ToggleTask};
use session::{self, FlashMessage};
pub struct AppState {
pub template: Tera,
pub db: Addr<DbExecutor>,
}
pub fn index(req: HttpRequest<AppState>) -> FutureResponse<HttpResponse> {
req.state()
.db
.send(AllTasks)
.from_err()
.and_then(move |res| match res {
Ok(tasks) => {
let mut context = Context::new();
context.add("tasks", &tasks);
//Session is set during operations on other endpoints
//that can redirect to index
if let Some(flash) = session::get_flash(&req)? {
context.add("msg", &(flash.kind, flash.message));
session::clear_flash(&req);
}
let rendered = req.state()
.template
.render("index.html.tera", &context)
.map_err(|e| {
error::ErrorInternalServerError(e.description().to_owned())
})?;
Ok(HttpResponse::Ok().body(rendered))
}
Err(e) => Err(e),
})
.responder()
}
#[derive(Deserialize)]
pub struct CreateForm {
description: String,
}
pub fn create(
(req, params): (HttpRequest<AppState>, Form<CreateForm>),
) -> FutureResponse<HttpResponse> {
if params.description.is_empty() {
future::lazy(move || {
session::set_flash(
&req,
FlashMessage::error("Description cannot be empty"),
)?;
Ok(redirect_to("/"))
}).responder()
} else {
req.state()
.db
.send(CreateTask {
description: params.description.clone(),
})
.from_err()
.and_then(move |res| match res {
Ok(_) => {
session::set_flash(
&req,
FlashMessage::success("Task successfully added"),
)?;
Ok(redirect_to("/"))
}
Err(e) => Err(e),
})
.responder()
}
}
#[derive(Deserialize)]
pub struct UpdateParams {
id: i32,
}
#[derive(Deserialize)]
pub struct UpdateForm {
_method: String,
}
pub fn update(
(req, params, form): (HttpRequest<AppState>, Path<UpdateParams>, Form<UpdateForm>),
) -> FutureResponse<HttpResponse> {
match form._method.as_ref() {
"put" => toggle(req, params),
"delete" => delete(req, params),
unsupported_method => {
let msg = format!("Unsupported HTTP method: {}", unsupported_method);
future::err(error::ErrorBadRequest(msg)).responder()
}
}
}
fn toggle(
req: HttpRequest<AppState>,
params: Path<UpdateParams>,
) -> FutureResponse<HttpResponse> {
req.state()
.db
.send(ToggleTask { id: params.id })
.from_err()
.and_then(move |res| match res {
Ok(_) => Ok(redirect_to("/")),
Err(e) => Err(e),
})
.responder()
}
fn delete(
req: HttpRequest<AppState>,
params: Path<UpdateParams>,
) -> FutureResponse<HttpResponse> {
req.state()
.db
.send(DeleteTask { id: params.id })
.from_err()
.and_then(move |res| match res {
Ok(_) => {
session::set_flash(&req, FlashMessage::success("Task was deleted."))?;
Ok(redirect_to("/"))
}
Err(e) => Err(e),
})
.responder()
}
fn redirect_to(location: &str) -> HttpResponse {
HttpResponse::Found()
.header(http::header::LOCATION, location)
.finish()
}
pub fn bad_request<S: 'static>(
req: &HttpRequest<S>,
resp: HttpResponse,
) -> Result<Response> {
let new_resp = NamedFile::open("static/errors/400.html")?
.set_status_code(resp.status())
.respond_to(req)?;
Ok(Response::Done(new_resp))
}
pub fn not_found<S: 'static>(
req: &HttpRequest<S>,
resp: HttpResponse,
) -> Result<Response> {
let new_resp = NamedFile::open("static/errors/404.html")?
.set_status_code(resp.status())
.respond_to(req)?;
Ok(Response::Done(new_resp))
}
pub fn internal_server_error<S: 'static>(
req: &HttpRequest<S>,
resp: HttpResponse,
) -> Result<Response> {
let new_resp = NamedFile::open("static/errors/500.html")?
.set_status_code(resp.status())
.respond_to(req)?;
Ok(Response::Done(new_resp))
}

100
actix_todo/src/db.rs Normal file
View File

@ -0,0 +1,100 @@
use std::ops::Deref;
use actix::prelude::{Actor, Handler, Message, SyncContext};
use actix_web::{error, Error};
use diesel::pg::PgConnection;
use diesel::r2d2::{ConnectionManager, Pool, PoolError, PooledConnection};
use model::{NewTask, Task};
type PgPool = Pool<ConnectionManager<PgConnection>>;
type PgPooledConnection = PooledConnection<ConnectionManager<PgConnection>>;
pub fn init_pool(database_url: &str) -> Result<PgPool, PoolError> {
let manager = ConnectionManager::<PgConnection>::new(database_url);
Pool::builder().build(manager)
}
pub struct DbExecutor(pub PgPool);
impl DbExecutor {
pub fn get_conn(&self) -> Result<PgPooledConnection, Error> {
self.0.get().map_err(|e| error::ErrorInternalServerError(e))
}
}
impl Actor for DbExecutor {
type Context = SyncContext<Self>;
}
pub struct AllTasks;
impl Message for AllTasks {
type Result = Result<Vec<Task>, Error>;
}
impl Handler<AllTasks> for DbExecutor {
type Result = Result<Vec<Task>, Error>;
fn handle(&mut self, _: AllTasks, _: &mut Self::Context) -> Self::Result {
Task::all(self.get_conn()?.deref())
.map_err(|_| error::ErrorInternalServerError("Error inserting task"))
}
}
pub struct CreateTask {
pub description: String,
}
impl Message for CreateTask {
type Result = Result<(), Error>;
}
impl Handler<CreateTask> for DbExecutor {
type Result = Result<(), Error>;
fn handle(&mut self, todo: CreateTask, _: &mut Self::Context) -> Self::Result {
let new_task = NewTask {
description: todo.description,
};
Task::insert(new_task, self.get_conn()?.deref())
.map(|_| ())
.map_err(|_| error::ErrorInternalServerError("Error inserting task"))
}
}
pub struct ToggleTask {
pub id: i32,
}
impl Message for ToggleTask {
type Result = Result<(), Error>;
}
impl Handler<ToggleTask> for DbExecutor {
type Result = Result<(), Error>;
fn handle(&mut self, task: ToggleTask, _: &mut Self::Context) -> Self::Result {
Task::toggle_with_id(task.id, self.get_conn()?.deref())
.map(|_| ())
.map_err(|_| error::ErrorInternalServerError("Error inserting task"))
}
}
pub struct DeleteTask {
pub id: i32,
}
impl Message for DeleteTask {
type Result = Result<(), Error>;
}
impl Handler<DeleteTask> for DbExecutor {
type Result = Result<(), Error>;
fn handle(&mut self, task: DeleteTask, _: &mut Self::Context) -> Self::Result {
Task::delete_with_id(task.id, self.get_conn()?.deref())
.map(|_| ())
.map_err(|_| error::ErrorInternalServerError("Error inserting task"))
}
}

87
actix_todo/src/main.rs Normal file
View File

@ -0,0 +1,87 @@
extern crate actix;
extern crate actix_web;
extern crate dotenv;
extern crate env_logger;
extern crate futures;
#[macro_use]
extern crate diesel;
#[macro_use]
extern crate log;
#[macro_use]
extern crate serde_derive;
#[macro_use]
extern crate tera;
use actix::prelude::SyncArbiter;
use actix_web::middleware::session::{CookieSessionBackend, SessionStorage};
use actix_web::middleware::{ErrorHandlers, Logger};
use actix_web::{dev::Resource, fs, http, server, App};
use dotenv::dotenv;
use std::env;
use tera::Tera;
mod api;
mod db;
mod model;
mod schema;
mod session;
static SESSION_SIGNING_KEY: &[u8] = &[0; 32];
const NUM_DB_THREADS: usize = 3;
fn main() {
dotenv().ok();
std::env::set_var("RUST_LOG", "actix_todo=debug,actix_web=info");
env_logger::init();
// Start the Actix system
let system = actix::System::new("todo-app");
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
let pool = db::init_pool(&database_url).expect("Failed to create pool");
let addr = SyncArbiter::start(NUM_DB_THREADS, move || db::DbExecutor(pool.clone()));
let app = move || {
debug!("Constructing the App");
let templates: Tera = compile_templates!("templates/**/*");
let session_store = SessionStorage::new(
CookieSessionBackend::signed(SESSION_SIGNING_KEY).secure(false),
);
let error_handlers = ErrorHandlers::new()
.handler(
http::StatusCode::INTERNAL_SERVER_ERROR,
api::internal_server_error,
)
.handler(http::StatusCode::BAD_REQUEST, api::bad_request)
.handler(http::StatusCode::NOT_FOUND, api::not_found);
let static_files = fs::StaticFiles::new("static/")
.expect("failed constructing static files handler");
let state = api::AppState {
template: templates,
db: addr.clone(),
};
App::with_state(state)
.middleware(Logger::default())
.middleware(session_store)
.middleware(error_handlers)
.route("/", http::Method::GET, api::index)
.route("/todo", http::Method::POST, api::create)
.resource("/todo/{id}", |r: &mut Resource<_>| {
r.post().with(api::update)
})
.handler("/static", static_files)
};
debug!("Starting server");
server::new(app).bind("localhost:8088").unwrap().start();
// Run actix system, this method actually starts all async processes
let _ = system.run();
}

46
actix_todo/src/model.rs Normal file
View File

@ -0,0 +1,46 @@
use diesel;
use diesel::pg::PgConnection;
use diesel::prelude::*;
use schema::{
tasks, tasks::dsl::{completed as task_completed, tasks as all_tasks},
};
#[derive(Debug, Insertable)]
#[table_name = "tasks"]
pub struct NewTask {
pub description: String,
}
#[derive(Debug, Queryable, Serialize)]
pub struct Task {
pub id: i32,
pub description: String,
pub completed: bool,
}
impl Task {
pub fn all(conn: &PgConnection) -> QueryResult<Vec<Task>> {
all_tasks.order(tasks::id.desc()).load::<Task>(conn)
}
pub fn insert(todo: NewTask, conn: &PgConnection) -> QueryResult<usize> {
diesel::insert_into(tasks::table)
.values(&todo)
.execute(conn)
}
pub fn toggle_with_id(id: i32, conn: &PgConnection) -> QueryResult<usize> {
let task = all_tasks.find(id).get_result::<Task>(conn)?;
let new_status = !task.completed;
let updated_task = diesel::update(all_tasks.find(id));
updated_task
.set(task_completed.eq(new_status))
.execute(conn)
}
pub fn delete_with_id(id: i32, conn: &PgConnection) -> QueryResult<usize> {
diesel::delete(all_tasks.find(id)).execute(conn)
}
}

7
actix_todo/src/schema.rs Normal file
View File

@ -0,0 +1,7 @@
table! {
tasks (id) {
id -> Int4,
description -> Varchar,
completed -> Bool,
}
}

39
actix_todo/src/session.rs Normal file
View File

@ -0,0 +1,39 @@
use actix_web::error::Result;
use actix_web::middleware::session::RequestSession;
use actix_web::HttpRequest;
const FLASH_KEY: &str = "flash";
pub fn set_flash<T>(request: &HttpRequest<T>, flash: FlashMessage) -> Result<()> {
request.session().set(FLASH_KEY, flash)
}
pub fn get_flash<T>(req: &HttpRequest<T>) -> Result<Option<FlashMessage>> {
req.session().get::<FlashMessage>(FLASH_KEY)
}
pub fn clear_flash<T>(req: &HttpRequest<T>) {
req.session().remove(FLASH_KEY);
}
#[derive(Deserialize, Serialize)]
pub struct FlashMessage {
pub kind: String,
pub message: String,
}
impl FlashMessage {
pub fn success(message: &str) -> Self {
Self {
kind: "success".to_owned(),
message: message.to_owned(),
}
}
pub fn error(message: &str) -> Self {
Self {
kind: "error".to_owned(),
message: message.to_owned(),
}
}
}

427
actix_todo/static/css/normalize.css vendored Normal file
View File

@ -0,0 +1,427 @@
/*! normalize.css v3.0.2 | MIT License | git.io/normalize */
/**
* 1. Set default font family to sans-serif.
* 2. Prevent iOS text size adjust after orientation change, without disabling
* user zoom.
*/
html {
font-family: sans-serif; /* 1 */
-ms-text-size-adjust: 100%; /* 2 */
-webkit-text-size-adjust: 100%; /* 2 */
}
/**
* Remove default margin.
*/
body {
margin: 0;
}
/* HTML5 display definitions
========================================================================== */
/**
* Correct `block` display not defined for any HTML5 element in IE 8/9.
* Correct `block` display not defined for `details` or `summary` in IE 10/11
* and Firefox.
* Correct `block` display not defined for `main` in IE 11.
*/
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
main,
menu,
nav,
section,
summary {
display: block;
}
/**
* 1. Correct `inline-block` display not defined in IE 8/9.
* 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
*/
audio,
canvas,
progress,
video {
display: inline-block; /* 1 */
vertical-align: baseline; /* 2 */
}
/**
* Prevent modern browsers from displaying `audio` without controls.
* Remove excess height in iOS 5 devices.
*/
audio:not([controls]) {
display: none;
height: 0;
}
/**
* Address `[hidden]` styling not present in IE 8/9/10.
* Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22.
*/
[hidden],
template {
display: none;
}
/* Links
========================================================================== */
/**
* Remove the gray background color from active links in IE 10.
*/
a {
background-color: transparent;
}
/**
* Improve readability when focused and also mouse hovered in all browsers.
*/
a:active,
a:hover {
outline: 0;
}
/* Text-level semantics
========================================================================== */
/**
* Address styling not present in IE 8/9/10/11, Safari, and Chrome.
*/
abbr[title] {
border-bottom: 1px dotted;
}
/**
* Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
*/
b,
strong {
font-weight: bold;
}
/**
* Address styling not present in Safari and Chrome.
*/
dfn {
font-style: italic;
}
/**
* Address variable `h1` font-size and margin within `section` and `article`
* contexts in Firefox 4+, Safari, and Chrome.
*/
h1 {
font-size: 2em;
margin: 0.67em 0;
}
/**
* Address styling not present in IE 8/9.
*/
mark {
background: #ff0;
color: #000;
}
/**
* Address inconsistent and variable font size in all browsers.
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` affecting `line-height` in all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sup {
top: -0.5em;
}
sub {
bottom: -0.25em;
}
/* Embedded content
========================================================================== */
/**
* Remove border when inside `a` element in IE 8/9/10.
*/
img {
border: 0;
}
/**
* Correct overflow not hidden in IE 9/10/11.
*/
svg:not(:root) {
overflow: hidden;
}
/* Grouping content
========================================================================== */
/**
* Address margin not present in IE 8/9 and Safari.
*/
figure {
margin: 1em 40px;
}
/**
* Address differences between Firefox and other browsers.
*/
hr {
-moz-box-sizing: content-box;
box-sizing: content-box;
height: 0;
}
/**
* Contain overflow in all browsers.
*/
pre {
overflow: auto;
}
/**
* Address odd `em`-unit font size rendering in all browsers.
*/
code,
kbd,
pre,
samp {
font-family: monospace, monospace;
font-size: 1em;
}
/* Forms
========================================================================== */
/**
* Known limitation: by default, Chrome and Safari on OS X allow very limited
* styling of `select`, unless a `border` property is set.
*/
/**
* 1. Correct color not being inherited.
* Known issue: affects color of disabled elements.
* 2. Correct font properties not being inherited.
* 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
*/
button,
input,
optgroup,
select,
textarea {
color: inherit; /* 1 */
font: inherit; /* 2 */
margin: 0; /* 3 */
}
/**
* Address `overflow` set to `hidden` in IE 8/9/10/11.
*/
button {
overflow: visible;
}
/**
* Address inconsistent `text-transform` inheritance for `button` and `select`.
* All other form control elements do not inherit `text-transform` values.
* Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
* Correct `select` style inheritance in Firefox.
*/
button,
select {
text-transform: none;
}
/**
* 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
* and `video` controls.
* 2. Correct inability to style clickable `input` types in iOS.
* 3. Improve usability and consistency of cursor style between image-type
* `input` and others.
*/
button,
html input[type="button"], /* 1 */
input[type="reset"],
input[type="submit"] {
-webkit-appearance: button; /* 2 */
cursor: pointer; /* 3 */
}
/**
* Re-set default cursor for disabled elements.
*/
button[disabled],
html input[disabled] {
cursor: default;
}
/**
* Remove inner padding and border in Firefox 4+.
*/
button::-moz-focus-inner,
input::-moz-focus-inner {
border: 0;
padding: 0;
}
/**
* Address Firefox 4+ setting `line-height` on `input` using `!important` in
* the UA stylesheet.
*/
input {
line-height: normal;
}
/**
* It's recommended that you don't attempt to style these elements.
* Firefox's implementation doesn't respect box-sizing, padding, or width.
*
* 1. Address box sizing set to `content-box` in IE 8/9/10.
* 2. Remove excess padding in IE 8/9/10.
*/
input[type="checkbox"],
input[type="radio"] {
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
}
/**
* Fix the cursor style for Chrome's increment/decrement buttons. For certain
* `font-size` values of the `input`, it causes the cursor style of the
* decrement button to change from `default` to `text`.
*/
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Address `appearance` set to `searchfield` in Safari and Chrome.
* 2. Address `box-sizing` set to `border-box` in Safari and Chrome
* (include `-moz` to future-proof).
*/
input[type="search"] {
-webkit-appearance: textfield; /* 1 */
-moz-box-sizing: content-box;
-webkit-box-sizing: content-box; /* 2 */
box-sizing: content-box;
}
/**
* Remove inner padding and search cancel button in Safari and Chrome on OS X.
* Safari (but not Chrome) clips the cancel button when the search input has
* padding (and `textfield` appearance).
*/
input[type="search"]::-webkit-search-cancel-button,
input[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* Define consistent border, margin, and padding.
*/
fieldset {
border: 1px solid #c0c0c0;
margin: 0 2px;
padding: 0.35em 0.625em 0.75em;
}
/**
* 1. Correct `color` not being inherited in IE 8/9/10/11.
* 2. Remove padding so people aren't caught out if they zero out fieldsets.
*/
legend {
border: 0; /* 1 */
padding: 0; /* 2 */
}
/**
* Remove default vertical scrollbar in IE 8/9/10/11.
*/
textarea {
overflow: auto;
}
/**
* Don't inherit the `font-weight` (applied by a rule above).
* NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
*/
optgroup {
font-weight: bold;
}
/* Tables
========================================================================== */
/**
* Remove most spacing between table cells.
*/
table {
border-collapse: collapse;
border-spacing: 0;
}
td,
th {
padding: 0;
}

421
actix_todo/static/css/skeleton.css vendored Normal file
View File

@ -0,0 +1,421 @@
/*
* Skeleton V2.0.4
* Copyright 2014, Dave Gamache
* www.getskeleton.com
* Free to use under the MIT license.
* http://www.opensource.org/licenses/mit-license.php
* 12/29/2014
*/
/* Table of contents
- Grid
- Base Styles
- Typography
- Links
- Buttons
- Forms
- Lists
- Code
- Tables
- Spacing
- Utilities
- Clearing
- Media Queries
*/
/* Grid
*/
.container {
position: relative;
width: 100%;
max-width: 960px;
margin: 0 auto;
padding: 0 20px;
box-sizing: border-box; }
.column,
.columns {
width: 100%;
float: left;
box-sizing: border-box; }
/* For devices larger than 400px */
@media (min-width: 400px) {
.container {
width: 85%;
padding: 0; }
}
/* For devices larger than 550px */
@media (min-width: 550px) {
.container {
width: 80%; }
.column,
.columns {
margin-left: 4%; }
.column:first-child,
.columns:first-child {
margin-left: 0; }
.one.column,
.one.columns { width: 4.66666666667%; }
.two.columns { width: 13.3333333333%; }
.three.columns { width: 22%; }
.four.columns { width: 30.6666666667%; }
.five.columns { width: 39.3333333333%; }
.six.columns { width: 48%; }
.seven.columns { width: 56.6666666667%; }
.eight.columns { width: 65.3333333333%; }
.nine.columns { width: 74.0%; }
.ten.columns { width: 82.6666666667%; }
.eleven.columns { width: 91.3333333333%; }
.twelve.columns { width: 100%; margin-left: 0; }
.one-third.column { width: 30.6666666667%; }
.two-thirds.column { width: 65.3333333333%; }
.one-half.column { width: 48%; }
/* Offsets */
.offset-by-one.column,
.offset-by-one.columns { margin-left: 8.66666666667%; }
.offset-by-two.column,
.offset-by-two.columns { margin-left: 17.3333333333%; }
.offset-by-three.column,
.offset-by-three.columns { margin-left: 26%; }
.offset-by-four.column,
.offset-by-four.columns { margin-left: 34.6666666667%; }
.offset-by-five.column,
.offset-by-five.columns { margin-left: 43.3333333333%; }
.offset-by-six.column,
.offset-by-six.columns { margin-left: 52%; }
.offset-by-seven.column,
.offset-by-seven.columns { margin-left: 60.6666666667%; }
.offset-by-eight.column,
.offset-by-eight.columns { margin-left: 69.3333333333%; }
.offset-by-nine.column,
.offset-by-nine.columns { margin-left: 78.0%; }
.offset-by-ten.column,
.offset-by-ten.columns { margin-left: 86.6666666667%; }
.offset-by-eleven.column,
.offset-by-eleven.columns { margin-left: 95.3333333333%; }
.offset-by-one-third.column,
.offset-by-one-third.columns { margin-left: 34.6666666667%; }
.offset-by-two-thirds.column,
.offset-by-two-thirds.columns { margin-left: 69.3333333333%; }
.offset-by-one-half.column,
.offset-by-one-half.columns { margin-left: 52%; }
}
/* Base Styles
*/
/* NOTE
html is set to 62.5% so that all the REM measurements throughout Skeleton
are based on 10px sizing. So basically 1.5rem = 15px :) */
html {
font-size: 62.5%; }
body {
font-size: 1.5em; /* currently ems cause chrome bug misinterpreting rems on body element */
line-height: 1.6;
font-weight: 400;
font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif;
color: #222; }
/* Typography
*/
h1, h2, h3, h4, h5, h6 {
margin-top: 0;
margin-bottom: 2rem;
font-weight: 300; }
h1 { font-size: 4.0rem; line-height: 1.2; letter-spacing: -.1rem;}
h2 { font-size: 3.6rem; line-height: 1.25; letter-spacing: -.1rem; }
h3 { font-size: 3.0rem; line-height: 1.3; letter-spacing: -.1rem; }
h4 { font-size: 2.4rem; line-height: 1.35; letter-spacing: -.08rem; }
h5 { font-size: 1.8rem; line-height: 1.5; letter-spacing: -.05rem; }
h6 { font-size: 1.5rem; line-height: 1.6; letter-spacing: 0; }
/* Larger than phablet */
@media (min-width: 550px) {
h1 { font-size: 5.0rem; }
h2 { font-size: 4.2rem; }
h3 { font-size: 3.6rem; }
h4 { font-size: 3.0rem; }
h5 { font-size: 2.4rem; }
h6 { font-size: 1.5rem; }
}
p {
margin-top: 0; }
/* Links
*/
a {
color: #1EAEDB; }
a:hover {
color: #0FA0CE; }
/* Buttons
*/
.button,
button,
input[type="submit"],
input[type="reset"],
input[type="button"] {
display: inline-block;
height: 38px;
padding: 0 30px;
color: #555;
text-align: center;
font-size: 11px;
font-weight: 600;
line-height: 38px;
letter-spacing: .1rem;
text-transform: uppercase;
text-decoration: none;
white-space: nowrap;
background-color: transparent;
border-radius: 4px;
border: 1px solid #bbb;
cursor: pointer;
box-sizing: border-box; }
.button:hover,
button:hover,
input[type="submit"]:hover,
input[type="reset"]:hover,
input[type="button"]:hover,
.button:focus,
button:focus,
input[type="submit"]:focus,
input[type="reset"]:focus,
input[type="button"]:focus {
color: #333;
border-color: #888;
outline: 0; }
.button.button-primary,
button.button-primary,
button.primary,
input[type="submit"].button-primary,
input[type="reset"].button-primary,
input[type="button"].button-primary {
color: #FFF;
background-color: #33C3F0;
border-color: #33C3F0; }
.button.button-primary:hover,
button.button-primary:hover,
button.primary:hover,
input[type="submit"].button-primary:hover,
input[type="reset"].button-primary:hover,
input[type="button"].button-primary:hover,
.button.button-primary:focus,
button.button-primary:focus,
button.primary:focus,
input[type="submit"].button-primary:focus,
input[type="reset"].button-primary:focus,
input[type="button"].button-primary:focus {
color: #FFF;
background-color: #1EAEDB;
border-color: #1EAEDB; }
/* Forms
*/
input[type="email"],
input[type="number"],
input[type="search"],
input[type="text"],
input[type="tel"],
input[type="url"],
input[type="password"],
textarea,
select {
height: 38px;
padding: 6px 10px; /* The 6px vertically centers text on FF, ignored by Webkit */
background-color: #fff;
border: 1px solid #D1D1D1;
border-radius: 4px;
box-shadow: none;
box-sizing: border-box; }
/* Removes awkward default styles on some inputs for iOS */
input[type="email"],
input[type="number"],
input[type="search"],
input[type="text"],
input[type="tel"],
input[type="url"],
input[type="password"],
textarea {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none; }
textarea {
min-height: 65px;
padding-top: 6px;
padding-bottom: 6px; }
input[type="email"]:focus,
input[type="number"]:focus,
input[type="search"]:focus,
input[type="text"]:focus,
input[type="tel"]:focus,
input[type="url"]:focus,
input[type="password"]:focus,
textarea:focus,
select:focus {
border: 1px solid #33C3F0;
outline: 0; }
label,
legend {
display: block;
margin-bottom: .5rem;
font-weight: 600; }
fieldset {
padding: 0;
border-width: 0; }
input[type="checkbox"],
input[type="radio"] {
display: inline; }
label > .label-body {
display: inline-block;
margin-left: .5rem;
font-weight: normal; }
/* Lists
*/
ul {
list-style: circle inside; }
ol {
list-style: decimal inside; }
ol, ul {
padding-left: 0;
margin-top: 0; }
ul ul,
ul ol,
ol ol,
ol ul {
margin: 1.5rem 0 1.5rem 3rem;
font-size: 90%; }
li {
margin-bottom: 1rem; }
/* Code
*/
code {
padding: .2rem .5rem;
margin: 0 .2rem;
font-size: 90%;
white-space: nowrap;
background: #F1F1F1;
border: 1px solid #E1E1E1;
border-radius: 4px; }
pre > code {
display: block;
padding: 1rem 1.5rem;
white-space: pre; }
/* Tables
*/
th,
td {
padding: 12px 15px;
text-align: left;
border-bottom: 1px solid #E1E1E1; }
th:first-child,
td:first-child {
padding-left: 0; }
th:last-child,
td:last-child {
padding-right: 0; }
/* Spacing
*/
button,
.button {
margin-bottom: 1rem; }
input,
textarea,
select,
fieldset {
margin-bottom: 1.5rem; }
pre,
blockquote,
dl,
figure,
table,
p,
ul,
ol,
form {
margin-bottom: 2.5rem; }
/* Utilities
*/
.u-full-width {
width: 100%;
box-sizing: border-box; }
.u-max-full-width {
max-width: 100%;
box-sizing: border-box; }
.u-pull-right {
float: right; }
.u-pull-left {
float: left; }
/* Misc
*/
hr {
margin-top: 3rem;
margin-bottom: 3.5rem;
border-width: 0;
border-top: 1px solid #E1E1E1; }
/* Clearing
*/
/* Self Clearing Goodness */
.container:after,
.row:after,
.u-cf {
content: "";
display: table;
clear: both; }
/* Media Queries
*/
/*
Note: The best way to structure the use of media queries is to create the queries
near the relevant code. For example, if you wanted to change the styles for buttons
on small devices, paste the mobile query code up in the buttons section and style it
there.
*/
/* Larger than mobile */
@media (min-width: 400px) {}
/* Larger than phablet (also point when grid becomes active) */
@media (min-width: 550px) {}
/* Larger than tablet */
@media (min-width: 750px) {}
/* Larger than desktop */
@media (min-width: 1000px) {}
/* Larger than Desktop HD */
@media (min-width: 1200px) {}

View File

@ -0,0 +1,58 @@
.field-error {
border: 1px solid #ff0000 !important;
}
.field-error-msg {
color: #ff0000;
display: block;
margin: -10px 0 10px 0;
}
.field-success {
border: 1px solid #5AB953 !important;
}
.field-success-msg {
color: #5AB953;
display: block;
margin: -10px 0 10px 0;
}
span.completed {
text-decoration: line-through;
}
form.inline {
display: inline;
}
form.link,
button.link {
display: inline;
color: #1EAEDB;
border: none;
outline: none;
background: none;
cursor: pointer;
padding: 0;
margin: 0 0 0 0;
height: inherit;
text-decoration: underline;
font-size: inherit;
text-transform: none;
font-weight: normal;
line-height: inherit;
letter-spacing: inherit;
}
form.link:hover, button.link:hover {
color: #0FA0CE;
}
button.small {
height: 20px;
padding: 0 10px;
font-size: 10px;
line-height: 20px;
margin: 0 2.5px;
}

View File

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>The server could not understand the request (400)</title>
<link href="//fonts.googleapis.com/css?family=Raleway:400,300,600" rel="stylesheet" type="text/css">
<link rel="stylesheet" href="/static/css/normalize.css">
<link rel="stylesheet" href="/static/css/skeleton.css">
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<div class="container">
<div class="row">
<h1>The server could not understand the request</h1>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>The page you were looking for doesn't exist (404)</title>
<link href="//fonts.googleapis.com/css?family=Raleway:400,300,600" rel="stylesheet" type="text/css">
<link rel="stylesheet" href="/static/css/normalize.css">
<link rel="stylesheet" href="/static/css/skeleton.css">
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<div class="container">
<div class="row">
<h1>The page you were looking for doesn't exist.</h1>
<p>You may have mistyped the address or the page may have moved.</p>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Ooops (500)</title>
<link href="//fonts.googleapis.com/css?family=Raleway:400,300,600" rel="stylesheet" type="text/css">
<link rel="stylesheet" href="/static/css/normalize.css">
<link rel="stylesheet" href="/static/css/skeleton.css">
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<div class="container">
<div class="row">
<h1>Ooops ...</h1>
<p>How embarrassing!</p>
<p>Looks like something weird happened while processing your request.</p>
<p>Please try again in a few moments.</p>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,65 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Actix Todo Example</title>
<link href="//fonts.googleapis.com/css?family=Raleway:400,300,600" rel="stylesheet" type="text/css">
<link rel="stylesheet" href="/static/css/normalize.css">
<link rel="stylesheet" href="/static/css/skeleton.css">
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<div class="container">
<p><!-- nothing to see here --></p>
<div class="row">
<h4>Actix Todo</h4>
<form action="/todo" method="post">
<div class="ten columns">
<input type="text" placeholder="enter a task description ..."
name="description" id="description" value="" autofocus
class="u-full-width {% if msg %}field-{{msg.0}}{% endif %}" />
{% if msg %}
<small class="field-{{msg.0}}-msg">
{{msg.1}}
</small>
{% endif %}
</div>
<div class="two columns">
<input type="submit" value="add task">
</div>
</form>
</div>
<div class="row">
<div class="twelve columns">
<ul>
{% for task in tasks %}
<li>
{% if task.completed %}
<span class="completed">{{task.description}}</span>
<form action="/todo/{{task.id}}" class="inline" method="post">
<input type="hidden" name="_method" value="put" />
<button type="submit" class="small">undo</button>
</form>
<form action="/todo/{{task.id}}" method="post" class="inline">
<input type="hidden" name="_method" value="delete" />
<button type="submit" class="primary small">delete</button>
</form>
{% else %}
<form action="/todo/{{task.id}}" class="link" method="post">
<input type="hidden" name="_method" value="put" />
<button type="submit" class="link">{{ task.description }}</button>
</form>
{% endif %}
</li>
{% endfor %}
</ul>
</div>
</div>
</div>
</body>
</html>