events, groups, entries endpoints added
This commit is contained in:
parent
8e0dad4abc
commit
36d77361c4
196
README.md
Normal file
196
README.md
Normal 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
|
104
autogestionale.postman_collection.json
Normal file
104
autogestionale.postman_collection.json
Normal 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": []
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
@ -96,23 +97,48 @@ async def token_destroy(request):
|
|||
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(),
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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):
|
||||
|
|
6
cli.py
6
cli.py
|
@ -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,7 +116,7 @@ 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)\
|
||||
events = events.filter(Event.ends_at >= starts_at) \
|
||||
.filter(Event.starts_at <= ends_at)
|
||||
|
||||
return events.all()
|
||||
|
|
20
web.py
20
web.py
|
@ -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'))
|
||||
|
|
Loading…
Reference in New Issue
Block a user