Commit 2febdf14 authored by Taddeüs Kroes's avatar Taddeüs Kroes

Initial commit: framework and a number of pages

parents
*.swp
*.bak
composer.lock
vendor/
.cache/
www/js/
www/css/
config.local.json
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [QSA,L]
<?php
use Nette\Security as NS;
class DatabaseAuthenticator extends Nette\Object implements NS\IAuthenticator
{
private $database;
private $config = array(
'table' => 'user',
'username' => 'username',
'password_hash' => 'password'
);
function __construct(Nette\Database\Context $database, $config=null)
{
$this->database = $database;
is_array($config) && $this->config = array_merge($this->config, $config);
}
function authenticate(array $credentials)
{
list($username, $password) = $credentials;
$row = $this->database->table($this->config['table'])
->where($this->config['username'], $username)
->fetch();
if (!$row)
throw new NS\AuthenticationException('Invalid username or password.');
if (!NS\Passwords::verify($password, $row[$this->config['password_hash']]))
throw new NS\AuthenticationException('Invalid username or password.');
return new NS\Identity($row->id, $row->role);
}
}
<?php
namespace Slim\Latte;
class LatteView extends \Slim\View
{
/** @var array Default configuration, can be overwritten by constructor */
private $config = array(
'path' => '',
'cache' => '',
'extension' => '.latte',
'configure_engine' => null
);
/** @var Latte\Engine */
private $engine;
/**
* Create new Latte view.
*
* @param string $path Root template directory (without trailing slash)
*/
public function __construct($config=null) {
parent::__construct();
$config && $this->config = array_merge($this->config, $config);
$this->setTemplatesDirectory($this->config['path']);
$this->engine = new \Latte\Engine;
$this->engine->setTempDirectory($this->config['cache']);
if (is_callable($this->config['configure_engine']))
call_user_func($this->config['configure_engine'], $this->engine);
}
public function getTemplatePathname($file) {
//return parent::getTemplatePathname($file . $this->config['extension']);
return $this->config['path'] . '/' . $file . $this->config['extension'];
}
/**
* Render view.
*
* @param string $template Template path (relative from root dir, without
* extension)
* @param array $data Optional additional data to pass to the template.
* @return string The rendered template
*/
public function render($template, $data=null) {
$path = $this->getTemplatePathname($template);
$data = array_merge($this->data->all(), (array)$data);
return $this->engine->renderToString($path, $data);
}
}
CACHE_DIR := .cache
WWW_DIR := www
LIB_DIR := vendor
SCRIPTS := forms
STYLES := main
HTTPDUSER := www-data
SCRIPTS := $(patsubst %,$(WWW_DIR)/js/%.js,$(SCRIPTS))
STYLES := $(patsubst %,$(WWW_DIR)/css/%.css,$(STYLES))
.PHONY: all min clean cleaner
all: $(LIB_DIR)/autoload.php $(CACHE_DIR) $(SCRIPTS) $(STYLES)
$(LIB_DIR)/autoload.php: $(LIB_DIR) composer.json
composer update
$(LIB_DIR):
composer install
$(WWW_DIR)/js/%.js: coffee/%.coffee
@mkdir -p $(@D)
coffee --compile --output $(@D) $<
$(WWW_DIR)/css/%.css: sass/%.sass
@mkdir -p $(@D)
sass --cache-location $(CACHE_DIR)/sass $< $@
min: all
@for f in $(STYLES); do \
printf '%-20s : ' $$f 1>&2; \
mincss -v -o $$f $$f; \
done
$(CACHE_DIR):
mkdir $@
setfacl -R -m u:"$(HTTPDUSER)":rwX $@
clean:
rm -f $(SCRIPTS) $(STYLES)
cleaner: clean
rm -rf $(CACHE_DIR) $(LIB_DIR)
File added
window.Nette.addError = (elem, message) ->
elem.focus() if elem.focus
if message
elem.setCustomValidity(message)
elem.valid = 0
elem.checkValidity()
$ ->
$('input:first', document.forms[0]).focus() if document.forms.length
$('tr').on 'click', ->
href = $(this).data('href')
document.location = href if href?
{
"name": "archery",
"description": "A small mobile website to save personal archery scores",
"author": {
"name": "Taddeus Kroes"
},
"require": {
"php": ">= 5.3.7",
"slim/slim": "2.*",
"nette/database": "~2.2.0",
"nette/security": "~2.2.0",
"nette/forms": "~2.2.0",
"latte/latte": "~2.2.0",
"instante/bootstrap-3-renderer": "@dev"
}
}
{
"database": {
"lazy": true
},
"log.enable": true,
"debug": false
}
<?php
define('DATABASE_DSN', '');
define('DATABASE_USER', 'dev');
define('DATABASE_PASSWORD', 'dev');
define('DATABASE_LAZY', true);
"database": {
"lazy": true
},
"log.enable": true,
"debug": false
}
<?php
require 'vendor/autoload.php';
require 'LatteView.php';
require 'util.php';
require 'DatabaseAuthenticator.php';
/*
* Set locale based on browser language specification
* XXX: use for gettext
*/
//if ($locale = locale_accept_from_http($_SERVER['HTTP_ACCEPT_LANGUAGE']))
// setlocale(LC_ALL, $locale, "$locale.utf8");
/*
* Config
*/
$config = load_config('config.json');
if (($config_local = load_config('config.local.json', true)) !== null)
$config = array_merge_recursive($config, $config_local);
define('ROOT_URL', $config['root_url']);
$view = new Slim\Latte\LatteView(array(
'path' => __DIR__ . '/templates',
'cache' => __DIR__ . '/.cache/latte',
'configure_engine' => function ($engine) {
$engine->onCompile[] = function ($latte) {
Instante\Bootstrap3Renderer\Latte\FormMacros::install($latte->getCompiler());
};
}
));
$app = new Slim\Slim(array(
'view' => $view
));
/*
* Database setup
*/
$connection = new Nette\Database\Connection(
$config['database']['dsn'],
$config['database']['user'],
$config['database']['password'],
array(
'lazy' => $config['database']['lazy']
)
);
$db = new Nette\Database\Context($connection);
/*
* Security
*/
$factory = new Nette\Http\RequestFactory;
$request = $factory->createHttpRequest();
$response = new Nette\Http\Response;
$session = new Nette\Http\Session($request, $response);
$storage = new Nette\Http\UserStorage($session);
$authenticator = new DatabaseAuthenticator($db);
$user = new Nette\Security\User($storage, $authenticator);
$app->hook('slim.before.router', function () use ($app, $user) {
$url = $app->request()->getPathInfo();
$non_user_urls = array('/login', '/register', '/populate');
if (!in_array($url, $non_user_urls) && !$user->isLoggedIn())
$app->redirect(ROOT_URL . '/login');
});
/*
* Routes
*/
require 'routes/common.php';
require 'routes/register.php';
require 'routes/user.php';
require 'routes/match.php';
// XXX: debug code
require 'populate.php';
$view->replace(compact('app', 'config', 'user'));
$app->run();
<?php
$app->get('/populate', function () use ($app, $db) {
$db->table('user')->insert(array(
'id' => 1,
'username' => 'taddeus',
'password' => Nette\Security\Passwords::hash('test123')
));
$db->table('match')->insert(array(
array(
'id' => 1,
'name' => 'Test',
'user_id' => 1,
'distance' => 18,
'discipline' => 'recurve',
'arrows' => 3,
'turns' => 2,
'scores' => pack_scores(array(10, 7, 6, 0, 9, 10))
),
array(
'id' => 2,
'name' => 'Nog een test',
'user_id' => 1,
'distance' => 25,
'discipline' => 'compound',
'arrows' => 4,
'turns' => 3,
'scores' => pack_scores(array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2))
),
array(
'id' => 3,
'name' => 'Kopie van eerste test',
'user_id' => 1,
'distance' => 18,
'discipline' => 'recurve',
'arrows' => 3,
'turns' => 2,
'scores' => pack_scores(array(10, 7, 6, 0, 9, 10))
),
array(
'id' => 4,
'name' => 'Dit is een hele lange naam van meerdere regels',
'user_id' => 1,
'distance' => 18,
'discipline' => 'recurve',
'arrows' => 3,
'turns' => 2,
'scores' => pack_scores(array(10, 7, 6, 0, 9, 10))
)
));
$db->table('tag')->insert(array(
array('name' => 'foo', 'match_id' => 3),
array('name' => 'bar', 'match_id' => 3),
array('name' => 'foo', 'match_id' => 2),
));
});
<?php
use Nette\Forms\Form;
use Instante\Bootstrap3Renderer\BootstrapRenderer;
$app->get('/', function () use ($app) {
$app->redirect(ROOT_URL . '/match');
});
function login_form() {
$form = new Form;
$form->setRenderer(new BootstrapRenderer);
$form->setAction('login');
$form->addText('username', 'Username')
->setRequired();
$form->addPassword('password', 'Password')
->setRequired();
$form->addCheckbox('remember', 'Remember me');
$form->addSubmit('send', 'Login');
return $form;
}
$app->get('/login', function () use ($app, $user) {
if ($user->isLoggedIn())
$app->redirect(ROOT_URL);
$app->render('login', array('form' => login_form()));
});
$app->post('/login', function () use ($app, $user) {
$form = login_form();
$form->validate();
if (!$form->hasErrors()) {
$values = $form->getValues();
try {
$user->login($values->username, $values->password);
$app->redirect(ROOT_URL);
} catch (Nette\Security\AuthenticationException $e) {
$form->addError($e->getMessage());
}
}
$app->render('login', compact('form'));
});
$app->get('/logout', function () use ($app, $user) {
$user->logout();
$app->redirect(ROOT_URL);
});
<?php
function pack_scores($scores) {
$blob = '';
foreach ($scores as $score)
$blob .= pack('C', $score);
return $blob;
}
function unpack_scores($blob) {
return array_map(function($c) { return unpack('C', $c)[1]; }, str_split($blob));
}
function match_arrows($row) {
return sprintf('%d &times; %d', $row->turns, $row->arrows);
}
function match_score($row) {
return array_sum(unpack_scores($row->scores));
}
function match_avg_score($row) {
return match_score($row) / ($row->turns * $row->arrows);
}
function find_match($id) {
global $app, $db, $user;
$match = $db->table('match')->get($id);
if (!$match)
$app->halt(404, 'Match does not exist');
if ($match->user_id != $user->getId() && !$user->hasRole('admin'))
$app->halt(403, 'You are not allowed to view this match');
return $match;
}
function render_match_action($action) {
return function($id) use ($action) {
global $app;
$app->render("match/$action", array('match' => find_match($id)));
};
}
$app->get('/match', function () use ($app, $db, $user) {
$user_id = $user->getId();
$matches = $db->table('match')->where(compact('user_id'))->order('created_at', 'desc');
$app->render('match/list', compact('matches'));
});
$app->get('/match/:id', render_match_action('view'));
$app->get('/match/:id/edit', render_match_action('edit'));
$app->get('/match/:id/delete', render_match_action('delete'));
$app->delete('/match/:id', function ($id) use ($app, $db) {
$db->table('user')->delete();
//find_match($id)->delete();
});
<?php
use Nette\Forms\Form;
use Instante\Bootstrap3Renderer\BootstrapRenderer;
use Nette\Security\Passwords;
function validate_unique_user($field) {
global $db;
$username = $field->getValue();
return $db->table('user')->where(compact('username'))->count() == 0;
}
function registration_form() {
$form = new Form;
$form->setRenderer(new BootstrapRenderer);
$form->setAction('register');
$form->addText('username', 'Username')
->setRequired()
->addRule(Form::MIN_LENGTH, null, 3)
->addRule(Form::MAX_LENGTH, null, 100)
->addRule(Form::PATTERN, 'Username may not contain whitespace or special characters', '([a-zA-Z0-9-_])+')
->addRule('validate_unique_user', 'This username has already been taken');
$form->addPassword('password', 'Password')
->setRequired();
$form->addPassword('password_repeat', 'Confirm password')
->setRequired()
->addConditionOn($form['password'], Form::FILLED)
->addRule(Form::EQUAL, 'Passwords must match', $form['password']);
$form->addSubmit('send', 'Register');
return $form;
}
$app->get('/register', function () use ($app, $user) {
$form = registration_form();
$app->render('register', compact('form'));
});
$app->post('/register', function () use ($app, $user, $db) {
$form = registration_form();
$form->validate();
if (!$form->hasErrors()) {
$values = $form->getValues();
$db->table('user')->insert(array(
'username' => $values->username,
'password' => Passwords::hash($values->password)
));
$user->login($values->username, $values->password);
$app->redirect(ROOT_URL);
}
$app->render('register', compact('form'));
});
<?php
$app->get('/user', function () {
echo "create user form";
});
$app->post('/user', function () {
echo "insert user";
});
$app->get('/user/:id', function ($id) {
echo "view user $id";
});
$app->get('/user/:id/edit', function ($id) {
echo "edit user $id form";
});
$app->put('/user/:id', function ($id) {
echo "update user $id";
});
$app->delete('/user/:id', function ($id) {
echo "delete user $id";
});
$portrait-width: 640px
$xs-width: 768px
.visible-xs-pt
display: none
@media (max-width: $portrait-width - 1)
.hidden-xs-pt
display: none !important
.visible-xs-pt
display: block !important
.matches
td
white-space: nowrap
td.nowrap
white-space: normal
[data-href]
cursor: pointer
.page-header
margin: 0 0 10px
.back
font-size: 15px
float: right
margin-top: 15px
display: block
.match
margin-top: 5px
th:first-child
text-align: right
&, *
border-color: #c5c5c5 !important
.separator
border-right: 4px double
//@media (max-width: $xs-width - 1)
// .actions
// float: right
.tags span
margin-right: 4px
{var $menu = [
$user->isLoggedIn() ? ['match', 'Matches'],
$user->isLoggedIn() ? ['logout', 'Logout'] : ['login', 'Login'],
!$user->isLoggedIn() ? ['register', 'Register'],
]}
<!doctype html>
<html lang="en">
<head>
<title>Archery scorekeeper</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">
<base href="{$config['root_url']}/">
<link rel="stylesheet" href="css/main.css">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<nav class="navbar navbar-default navbar-static-top" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed"
data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand">Archery scorekeeper</a>
</div>
<div class="collapse navbar-collapse">
<ul class="nav navbar-nav">
<li n:foreach="$menu as $m" n:if="$m"
n:class="'/'.$m[0] == $app->request()->getPathInfo() ? active">
<a href="{$m[0]}">{$m[1]}</a>
</li>
</ul>
</div>
</div>
</nav>
<div class="container">
{include content}
</div>
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js"></script>
<script src="js/netteForms.js"></script>
<script src="js/forms.js"></script>
</body>
</html>
{extends "layout.latte"}
{block content}
<div class="row">
<div class="col-sm-8 col-md-6">
{form $form}
{form errors}
{form controls}
<div class="form-group">
<div class="form-actions col-sm-offset-2 col-sm-10">
<button type="submit" name="send" class="btn btn-primary">Login</button>
<a href="register" class="btn">Register</a>
</div>
</div>
{/form}
</div>
</div>
{extends '../layout.latte'}
{block content}
<h2 class="page-header">
Delete match
</h2>
<p>
Are you sure you want to delete the match "{$match->name}"? This action cannot be undone.
</p>
<form action="match/{$match->id}" method="post">
<input type="hidden" name="_METHOD" value="DELETE"/>
<button type="submit" class="btn btn-primary">
Yes, I'm sure
</button>
<a href="match/{$match->id}" class="btn btn-default">
Cancel
</a>
</form>
{/block}
{extends '../layout.latte'}
{block content}
<table class="table table-hover matches">
<thead>
<tr>
<th>Name</th>
<th>Date</th>
<th class="hidden-xs-pt">Distance</th>
<th class="hidden-xs-pt">Discipline</th>
<th class="hidden-xs-pt">Arrows</th>
<th>Score</th>
</tr>
</thead>
<tbody>
<tr n:foreach="$matches as $match" data-href="match/{$match->id}">
<td class="nowrap">{$match->name}</td>
<td>
<span class="hidden-xs-pt">{$match->created_at|date:'%a %e %b %Y'}</span>
<span class="visible-xs-pt">{$match->created_at|date:'%e %b'}</span>
</td>
<td class="hidden-xs-pt">{$match->distance}m</td>
<td class="hidden-xs-pt">{$match->discipline|capitalize}</td>
<td class="hidden-xs-pt">{match_arrows($match)|noescape}</td>
<td>{match_score($match)} (~{match_avg_score($match)|number:1})</td>
</tr>
<tr n:if="!$matches->count()">
<td colspan="5">You have not saved any matches yet.</td>
</tr>
</tbody>
</table>
{/block}
{extends '../layout.latte'}
{var $rows = array_chunk(unpack_scores($match->scores), $match->arrows)}
{var $total = 0}
{var $arrow_cols = array(3 => 4, 4 => 5, 5 => 5, 6 => 6, 7 => 6)}
{var $cols = isset($arrow_cols[$match->arrows]) ? $arrow_cols[$match->arrows] : 4}
{var $tags = $match->related('tag')}
{block content}
<h2 class="page-header">
{$match->name}
{*<a href="match" class="back">&laquo; Go back</a>*}
</h2>
<p>
{$match->distance}m {$match->discipline|capitalize} -
{$match->created_at|date:'%A %e %B %Y %H:%m'}
</p>
<p n:if="$tags->count()" class="tags">
<strong>Tags:</strong>
<a n:foreach="$tags as $tag" href="tag/{$tag->name}">
<span class="label label-primary">{$tag->name}</span>
</a>
</p>
<div class="row">
<div class="col-sm-{$cols + 2} col-md-{$cols + 1} col-lg-{$cols}">
<table class="table table-bordered table-condensed match">
<thead>
<tr>
<th class="separator"></th>
<th class="separator" colspan="{$match->arrows}">Points</th>
<th colspan="2">Subtotal</th>
</tr>
</thead>
<tbody>
<tr n:foreach="$rows as $i => $row">
<th class="separator">{$i + 1}</th>
<td n:foreach="$row as $j => $arrow"
n:class="$j == $match->arrows - 1 ? separator">{$arrow}</td>
<td>{$sum = array_sum($row)}</td>
<td>{$total = $total + $sum}</td>
</tr>
</tbody>
<tbody>
<tr>
<th colspan="{$match->arrows + 2}">Total:</th>
<td>{$total}</td>
</tr>
</tbody>
</table>
<div class="btn-group actions">
<a href="match" class="btn btn-default" title="Go back">
<span class="glyphicon glyphicon-chevron-left"></span>
</a>
<a href="match/{$match->id}/edit" class="btn btn-default" title="Edit">
<span class="glyphicon glyphicon-pencil"></span>
</a>
<a href="match/{$match->id}/delete" class="btn btn-danger" title="Delete">
<span class="glyphicon glyphicon-trash"></span>
</a>
</div>
</div>
</div>
{/block}
{extends "layout.latte"}
{block content}
<div class="row">
<div class="col-sm-8 col-md-6">
{$form}
</div>
</div>
<?php
function load_config($filename, $optional=false) {
if (!file_exists(__DIR__ . '/' . $filename)) {
if ($optional)
return null;
else
throw new RuntimeException("missing file $filename");
}
$config = json_decode(file_get_contents(__DIR__ . '/' . $filename), true);
if ($config === null)
throw new RuntimeException("file $filename is not valid JSON");
return $config;
}
#!/bin/sh
hash inotifywait || (echo "Install inotify-tools first"; exit 1)
make
while true; do
inotifywait --quiet --event attrib,modify sass/*.sass coffee/*.coffee
sleep 0.05s
make
done
../index.php
\ No newline at end of file
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment