events, groups, entries endpoints added

This commit is contained in:
Notisset 2017-10-02 00:47:22 +02:00
parent 8e0dad4abc
commit 36d77361c4
9 changed files with 480 additions and 50 deletions

196
README.md Normal file
View File

@ -0,0 +1,196 @@
# Autogestionale (work in progress)
Autogestionale e' parte della suite **autogestionale+scassa** e si occupa di tenere il bilancio generale delle serate e dei gruppi che le organizzano.
Dal momento che il progetto parte da una copia di [macao-pos](https://git.unit.macaomilano.org/crudo/macao-pos/) di seguito si trova una copia quasi invariata del suo readme.
## Installation
The only requirements are Python 3 and virtualenv as you're going to install all
the modules through pip.
If you're using MySQL or PostgreSQL create now a new user and database. You can
also use SQLite for testing purposes.
```
cd /var/www
git clone ...
cd autogestionale
virtualenv -p python3 env
source env/bin/activate
pip install -r requirements.txt
```
Now you need to configure this software. Inside `doc/` you'll find some
examples. The default path for configurations in `conf/` but you can also use
`~/.config/autogestionale`, `/usr/local/etc/autogestionale` and `/etc/autogestionale` that will be checked in
this order.
For a testing environment you can just do:
```
mkdir conf
cp docs/config_core/core_debug_sqlite.ini conf/core.ini
cp docs/config_logging/logging_debug.yaml conf/logging.yaml
```
and you're ready to go.
For a production environment:
```
cd /var/www/autogestionale
mkdir conf
cp docs/config_core/core_production_mysql.ini conf/core.ini
cp docs/config_logging/logging_production.yaml conf/logging.yaml
```
and then edit the conf/core.ini file to adjust the database params. Don't forget
to change the SECRET_KEY value (`openssl rand -hex 32` will help you).
If you want to change the log file path open your `conf/logging.yaml` and
change the `filename` field of the `file` entry inside the `handlers` category.
### Building the database
You also need to add some entries to the database.
First of all add a new user. Get inside the virtualenv and then just do:
```
python3 cli.py user add username password
```
Add some categories with:
```
python3 cli.py category add "Birra"
python3 cli.py category add "Super"
python3 cli.py category add "Altro"
```
Add some products with:
```
python3 cli.py product add -c 1 "Birra media" 3
python3 cli.py product add -c 1 "Birra grande" 4
python3 cli.py product add -c 2 "Vino" 4
python3 cli.py product add -c 2 "Cocktail" 5
python3 cli.py product add -c 2 "Amaro" 2
python3 cli.py product add -c 3 "Acqua" 0.5
```
And finally add and event you can play with:
```
python3 cli.py event add "My party" "2017-03-19 22:00" "2017-03-22 07:00"
```
## Running
You can run this software within the virtualenv with:
```
python3 web.py
```
## REST API
### Authentication
* **URL**: `/api/token`
* **Method**: `POST`
* **Success response**:
* **Code**: 200
* **Content**:
```
{
"token": "3ea90c63-4b92-465e-bee8-018a4c569252",
"created_at": "2017-09-25T18:50:38.620881",
"expires_at": "2017-09-25T18:50:38.620881"
}
```
* **Error response**:
* ***Malformed request***
* **Code**: 400
* **Content**:
```
{
"err": "malformed_request",
"msg": "Missing username and/or password keys."
}
```
* ***Invalid credentials***
* **Code**: 400
* **Content**:
```
{
"err": "invalid_credentials"
}
```
* **Sample call**:
```
curl -X POST \
-H "Accept: application/json" \
-d '{"username": "gino", "password": "paoli"}' \
"http://127.0.0.1:8009/api/token"
```
### Logout
* **URL**: `/api/token`
* **Method**: `DELETE`
* **Success response**:
* **Code**: 200
* **Content**:
```
{}
```
* **Error response**:
* ***Malformed request***
* **Code**: 400
* **Content**:
```
{
"err": "malformed_request",
"msg": "Missing Authorization header."
}
```
* ***Unauthorizred***
* **Code**: 401
* **Content**:
```
{
"err": "unauthorized",
"msg": "The token is not valid."
}
```
* ***Forbidden***
* **Code**: 403
* **Content**:
```
{
"err": "forbidden",
"msg": "The token has expired."
}
```
* **Sample call**:
```
curl -X DELETE \
-H "Authorization: 3ea90c63-4b92-465e-bee8-018a4c569252" \
"http://127.0.0.1:8009/api/token"
```
## Contributing
Before pushing any commit make sure flake8 doesn't complain running:
```
flake8 web.py cli.py autogestionale/*.py
```
1. Clone this repository
2. Create a new branch
3. Make your patch. Split it in different commits if it's huge
4. Fork this repo
5. Push your branch to your fork
6. Issue a pull request

View File

@ -0,0 +1,104 @@
{
"variables": [],
"info": {
"name": "autogestionale",
"_postman_id": "d070bf6b-87c1-b2f1-430c-eba72e87c043",
"description": "",
"schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json"
},
"item": [
{
"name": "localhost:8009/groups",
"request": {
"url": "localhost:8009/groups",
"method": "GET",
"header": [],
"body": {},
"description": ""
},
"response": []
},
{
"name": "localhost:8009/groups",
"request": {
"url": "localhost:8009/groups",
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json",
"description": ""
}
],
"body": {
"mode": "raw",
"raw": "{\"name\":\"unit\",\"description\":\"facciamo tante cose\"}"
},
"description": ""
},
"response": []
},
{
"name": "localhost:8009/events",
"request": {
"url": "localhost:8009/events",
"method": "GET",
"header": [],
"body": {},
"description": ""
},
"response": []
},
{
"name": "localhost:8009/events",
"request": {
"url": "localhost:8009/events",
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json",
"description": ""
}
],
"body": {
"mode": "raw",
"raw": "{\"group_uid\":1,\"name\":\"unit\",\"description\":\"facciamo tante cose\"}"
},
"description": ""
},
"response": []
},
{
"name": "localhost:8009/entries",
"request": {
"url": "localhost:8009/entries",
"method": "GET",
"header": [],
"body": {},
"description": ""
},
"response": []
},
{
"name": "localhost:8009/entries",
"request": {
"url": "localhost:8009/entries",
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json",
"description": ""
}
],
"body": {
"mode": "raw",
"raw": "{\"event_uid\":1,\"description\":\"birra\",\"amount\":-254.99}"
},
"description": ""
},
"response": []
}
]
}

View File

@ -5,7 +5,12 @@ import yaml
APP_NAME = 'autogestionale'
CONFIG_PATHS = ['conf/', '~/.config/autogestionale', '/usr/local/etc/autogestionale', '/etc/autogestionale']
CONFIG_PATHS = [
'conf/',
'~/.config/autogestionale',
'/usr/local/etc/autogestionale',
'/etc/autogestionale'
]
CONFIG_FILES = {
'core': 'core.ini',
'logging': 'logging.yaml'

View File

@ -4,7 +4,7 @@ from contextlib import contextmanager
import sqlalchemy as sa
import sqlalchemy.ext.declarative
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy import Table, Column, ForeignKey
from sqlalchemy import Column, ForeignKey
from sqlalchemy import Integer, String, Boolean, DateTime, Numeric
from sqlalchemy.orm import relationship
@ -102,15 +102,14 @@ class User(Base):
class Entry(Base):
__tablename__ = 'events'
__tablename__ = 'entry'
uid = Column(Integer, primary_key=True)
amount = Column(Numeric(precision=3))
description = Column(String, nullable=False)
starts_at = Column(DateTime, nullable=False, default=datetime.now)
ends_at = Column(DateTime)
# entry_category_uid = Column(Integer, ForeignKey('entry_category.uid'),
# nullable=False)
created_at = Column(DateTime, nullable=False, default=datetime.now)
event_uid = Column(Integer, ForeignKey('event.uid'),
nullable=False)
event_uid = Column(Integer, ForeignKey('event.uid'), nullable=False)
class Event(Base):
@ -125,6 +124,12 @@ class Event(Base):
entries = relationship('Entry', lazy='joined')
def get_balance(self):
_ret = 0
for ent in self.entries:
_ret += ent.amount
return _ret
class Group(Base):
__tablename__ = 'group'
@ -135,6 +140,18 @@ class Group(Base):
events = relationship('Event', lazy='joined')
# def to_json(self):
# return {
# 'uid': self.uid,
# 'name': self.name,
# 'description': self.description,
# 'created_at': self.created_at.isoformat(),
# 'events': [{
# 'uid': evt.uid,
# 'name': evt.name
# } for evt in self.events]
# }
class ProductCategory(Base):
__tablename__ = 'product_categories'

View File

@ -1,13 +1,11 @@
import logging
import logging.config
from autogestionale.config import APP_NAME
root = logging.getLogger('autogestionale')
root = logging.getLogger(APP_NAME)
def init_logging(config):
def setup_logging(config):
logging.config.dictConfig(config)

View File

@ -1,6 +1,6 @@
from functools import wraps
from aiohttp.web import json_response
from autogestionale.database import User, ProductCategory, Event
from autogestionale.database import User, Group, Event, Entry
def needs(*needed):
@ -18,6 +18,7 @@ def needs(*needed):
return func(request)
return wrapper
return decorator
@ -35,9 +36,9 @@ def auth_required(func):
remote_token = headers['Authorization']
with db.get_session() as session:
token = session.query(AccessToken) \
.filter_by(token=remote_token) \
.one_or_none()
token = session.query(AccessToken) \
.filter_by(token=remote_token) \
.one_or_none()
if not token:
return json_response({'err': 'unauthorized',
@ -62,9 +63,9 @@ async def token_create(request):
password = request_json['password']
with db.get_session() as session:
user = session.query(User) \
.filter_by(username=username) \
.one_or_none()
user = session.query(User) \
.filter_by(username=username) \
.one_or_none()
if not user or user.password != password:
return json_response({'err': 'invalid_credentials'},
@ -87,32 +88,57 @@ async def token_destroy(request):
remote_token = request.headers['Authorization']
with db.get_session() as session:
token = session.query(AccessToken) \
.filter_by(token=remote_token) \
.one_or_none()
token = session.query(AccessToken) \
.filter_by(token=remote_token) \
.one_or_none()
token.is_active = False
session.add(token)
return json_response({}, status=200)
@auth_required
async def product_list(request):
# @auth_required
async def group_list(request):
db = request.app['db']
with db.get_session() as session:
categories = session.query(ProductCategory).all()
groups = session.query(Group).all()
return json_response({
'categories': [{
'uid': c.uid,
'name': c.name,
'products': [{
'uid': p.uid,
'name': p.name,
'price': p.price
} for p in c.products]
} for c in categories]
'groups': [{
'uid': grp.uid,
'name': grp.name,
'description': grp.description,
'created_at': grp.created_at.isoformat(),
'events': [{
'uid': evt.uid,
'name': evt.name,
'starts_at': evt.starts_at.isoformat(),
'ends_at': evt.ends_at.isoformat()
if evt.ends_at is not None else None,
} for evt in grp.events]
} for grp in groups]
})
async def group_create(request):
db = request.app['db']
request_json = await request.json()
name = request_json['name']
description = request_json['description']
with db.get_session() as session:
grp = Group(name=name, description=description)
session.add(grp)
return json_response({
'group': {
'uid': grp.uid,
'name': grp.name,
'description': grp.description,
'created_at': grp.created_at.isoformat(),
}
})
@ -126,9 +152,86 @@ async def event_list(request):
'events': [{
'uid': evt.uid,
'name': evt.name,
'group_uid': evt.group_uid,
'balance': str(evt.get_balance()),
'entries': [{
'uid': entr.uid,
'amount': entr.amount
'amount': str(entr.amount)
} for entr in evt.entries]
} for evt in events]
})
async def event_create(request):
db = request.app['db']
request_json = await request.json()
group_uid = request_json['group_uid']
name = request_json['name']
# description = request_json['description']
starts_at = request_json['starts_at'] \
if 'starts_at' in request_json else None
ends_at = request_json['ends_at'] \
if 'ends_at' in request_json else None
with db.get_session() as session:
evt = Event(
group_uid=group_uid,
name=name,
starts_at=starts_at,
ends_at=ends_at
)
session.add(evt)
return json_response({
'event': {
'uid': evt.uid,
'group_uid': evt.group_uid,
'name': evt.name,
'created_at': evt.created_at.isoformat(),
}
})
async def entry_list(request):
db = request.app['db']
with db.get_session() as session:
entrys = session.query(Entry).all()
return json_response({
'entries': [{
'uid': ent.uid,
'event_uid': ent.event_uid,
'amount': str(ent.amount),
'description': ent.description,
'created_at': ent.created_at.isoformat(),
} for ent in entrys]
})
async def entry_create(request):
db = request.app['db']
request_json = await request.json()
amount = request_json['amount']
description = request_json['description']
event_uid = request_json['event_uid']
with db.get_session() as session:
ent = Entry(
amount=amount,
description=description,
event_uid=event_uid
)
session.add(ent)
return json_response({
'entry': {
'uid': ent.uid,
'amount': ent.amount,
'description': ent.description,
'event_uid': ent.event_uid,
'created_at': ent.created_at.isoformat(),
}
})

View File

@ -1,4 +1,6 @@
from autogestionale.rest import event_list
from aiohttp.web import json_response
from autogestionale.rest import event_list, event_create, group_list, \
group_create, entry_list, entry_create
def setup_routes(app):
@ -6,6 +8,11 @@ def setup_routes(app):
app.router.add_route('GET', '/login', info)
app.router.add_route('POST', '/login', info)
app.router.add_route('GET', '/events', event_list)
app.router.add_route('POST', '/events', event_create)
app.router.add_route('GET', '/groups', group_list)
app.router.add_route('POST', '/groups', group_create)
app.router.add_route('GET', '/entries', entry_list)
app.router.add_route('POST', '/entries', entry_create)
async def info(request):

8
cli.py
View File

@ -5,8 +5,8 @@ from datetime import datetime
from autogestionale.config import Config
from autogestionale.logging import init_logging, get_logger
from autogestionale.database import Database, User, Event, ProductCategory, Product,\
Transaction
from autogestionale.database import Database, User, Event, ProductCategory,\
Product, Transaction
config = Config()
conf_db = config.core['DATABASE']
@ -116,8 +116,8 @@ def get_overlapping_events(session, starts_at, ends_at):
if ends_at is None:
events = events.filter(Event.starts_at <= starts_at)
else:
events = events.filter(Event.ends_at >= starts_at)\
.filter(Event.starts_at <= ends_at)
events = events.filter(Event.ends_at >= starts_at) \
.filter(Event.starts_at <= ends_at)
return events.all()

20
web.py
View File

@ -1,18 +1,21 @@
#! /usr/bin/env python3
from autogestionale.config import Config
from autogestionale.logging import init_logging, get_logger
from autogestionale.database import Database
from autogestionale.routes import setup_routes
#!/usr/bin/env python3
import asyncio
from aiohttp import web
from autogestionale.config import Config
from autogestionale.logging import setup_logging, get_logger
from autogestionale.database import Database
from autogestionale.routes import setup_routes
log = get_logger('web')
def setup_app(loop, config):
app = web.Application(loop=loop)
app['config'] = config
app['db'] = Database(**config.core['DATABASE'])
setup_routes(app)
return app
@ -20,14 +23,11 @@ def setup_app(loop, config):
if __name__ == '__main__':
config = Config()
init_logging(config.logging)
conf_db = config.core['DATABASE']
db = Database(**conf_db)
setup_logging(config.logging)
loop = asyncio.get_event_loop()
app = setup_app(loop, config)
app['db'] = db
web.run_app(app,
host=config.core.get('GENERAL', 'Address'),
port=config.core.getint('GENERAL', 'Port'))