events, groups, entries endpoints added

master
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 100644
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' 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 = { CONFIG_FILES = {
'core': 'core.ini', 'core': 'core.ini',
'logging': 'logging.yaml' 'logging': 'logging.yaml'

View File

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

View File

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

View File

@ -1,6 +1,6 @@
from functools import wraps from functools import wraps
from aiohttp.web import json_response from aiohttp.web import json_response
from autogestionale.database import User, ProductCategory, Event from autogestionale.database import User, Group, Event, Entry
def needs(*needed): def needs(*needed):
@ -18,6 +18,7 @@ def needs(*needed):
return func(request) return func(request)
return wrapper return wrapper
return decorator return decorator
@ -35,9 +36,9 @@ def auth_required(func):
remote_token = headers['Authorization'] remote_token = headers['Authorization']
with db.get_session() as session: with db.get_session() as session:
token = session.query(AccessToken) \ token = session.query(AccessToken) \
.filter_by(token=remote_token) \ .filter_by(token=remote_token) \
.one_or_none() .one_or_none()
if not token: if not token:
return json_response({'err': 'unauthorized', return json_response({'err': 'unauthorized',
@ -62,9 +63,9 @@ async def token_create(request):
password = request_json['password'] password = request_json['password']
with db.get_session() as session: with db.get_session() as session:
user = session.query(User) \ user = session.query(User) \
.filter_by(username=username) \ .filter_by(username=username) \
.one_or_none() .one_or_none()
if not user or user.password != password: if not user or user.password != password:
return json_response({'err': 'invalid_credentials'}, return json_response({'err': 'invalid_credentials'},
@ -87,32 +88,57 @@ async def token_destroy(request):
remote_token = request.headers['Authorization'] remote_token = request.headers['Authorization']
with db.get_session() as session: with db.get_session() as session:
token = session.query(AccessToken) \ token = session.query(AccessToken) \
.filter_by(token=remote_token) \ .filter_by(token=remote_token) \
.one_or_none() .one_or_none()
token.is_active = False token.is_active = False
session.add(token) session.add(token)
return json_response({}, status=200) return json_response({}, status=200)
@auth_required # @auth_required
async def product_list(request): async def group_list(request):
db = request.app['db'] db = request.app['db']
with db.get_session() as session: with db.get_session() as session:
categories = session.query(ProductCategory).all() groups = session.query(Group).all()
return json_response({ return json_response({
'categories': [{ 'groups': [{
'uid': c.uid, 'uid': grp.uid,
'name': c.name, 'name': grp.name,
'products': [{ 'description': grp.description,
'uid': p.uid, 'created_at': grp.created_at.isoformat(),
'name': p.name, 'events': [{
'price': p.price 'uid': evt.uid,
} for p in c.products] 'name': evt.name,
} for c in categories] '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': [{ 'events': [{
'uid': evt.uid, 'uid': evt.uid,
'name': evt.name, 'name': evt.name,
'group_uid': evt.group_uid,
'balance': str(evt.get_balance()),
'entries': [{ 'entries': [{
'uid': entr.uid, 'uid': entr.uid,
'amount': entr.amount 'amount': str(entr.amount)
} for entr in evt.entries] } for entr in evt.entries]
} for evt in events] } 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): def setup_routes(app):
@ -6,6 +8,11 @@ def setup_routes(app):
app.router.add_route('GET', '/login', info) app.router.add_route('GET', '/login', info)
app.router.add_route('POST', '/login', info) app.router.add_route('POST', '/login', info)
app.router.add_route('GET', '/events', event_list) 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): async def info(request):

8
cli.py
View File

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

20
web.py
View File

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