Skip to content

Commit

Permalink
Merge pull request #225 from mrf345/development
Browse files Browse the repository at this point in the history
Add tickets and tasks API endpoints, Resolves #184
  • Loading branch information
mrf345 authored Sep 12, 2020
2 parents 94b18b1 + 0fb1980 commit d14ac68
Show file tree
Hide file tree
Showing 20 changed files with 515 additions and 145 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ python:
install:
- pip install -r requirements/test.txt

script: flake8 app/**/** tests/** && pytest -vv tests/* --cov=./app
script: flake8 app/**/**/** tests/**/**/** && pytest -vv tests/*/* --cov=./app
after_success:
- coveralls
28 changes: 0 additions & 28 deletions app/api/auth.py

This file was deleted.

1 change: 1 addition & 0 deletions app/api/constants.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
AUTH_HEADER_KEY = 'Authorization'
LIMIT_PER_CHUNK = 30
28 changes: 28 additions & 0 deletions app/api/endpoints/tasks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from http import HTTPStatus
from flask_restx import Resource
from flask import request

from app.api import api
from app.api.helpers import token_required
from app.api.serializers import TaskSerializer
from app.api.constants import LIMIT_PER_CHUNK
from app.database import Task


def setup_tasks_endpoint():
endpoint = api.namespace(name='tasks',
description='Endpoint to handle tasks CRUD operations.')

@endpoint.route('/')
class ListTasks(Resource):
@endpoint.marshal_list_with(TaskSerializer)
@endpoint.param('chunk', f'dividing tasks into chunks of {LIMIT_PER_CHUNK}, default is 1.')
@endpoint.doc(security='apiKey')
@token_required
def get(self):
''' Get list of tasks. '''
chunk = request.args.get('chunk', 1, type=int)

return Task.query.paginate(chunk,
per_page=LIMIT_PER_CHUNK,
error_out=False).items, HTTPStatus.OK
126 changes: 112 additions & 14 deletions app/api/endpoints/tickets.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,129 @@
from http import HTTPStatus
from flask_restx import Resource
from flask_restx import Resource, abort
from flask import request

from app.api import api
from app.api.auth import token_required
from app.api.serializers import HelloWorldSerializer
from app.api.helpers import token_required, get_or_reject
from app.api.serializers import TicketSerializer
from app.api.constants import LIMIT_PER_CHUNK
from app.database import Serial, Task, Office
from app.middleware import db


def setup_tickets_endpoint():
endpoint = api.namespace(name='tickets',
description='Endpoint to handle tickets CRUD operations.')

@endpoint.route('/')
class HelloWorld(Resource):
''' Just returns a hello world! '''

@endpoint.marshal_with(HelloWorldSerializer)
class ListDeleteAndCreateTickets(Resource):
@endpoint.marshal_list_with(TicketSerializer)
@endpoint.param('processed', 'get only processed tickets, by default False.')
@endpoint.param('chunk', f'dividing tickets into chunks of {LIMIT_PER_CHUNK}, default is 1.')
@endpoint.doc(security='apiKey')
@token_required
def get(self):
''' Get first HelloWorld. '''
return 'Hello world!', HTTPStatus.OK
''' Get list of tickets. '''
chunk = request.args.get('chunk', 1, type=int)
processed = request.args.get('processed', False, type=bool)
tickets = Serial.all_clean()

if processed:
tickets = tickets.filter_by(p=True)

return tickets.paginate(chunk,
per_page=LIMIT_PER_CHUNK,
error_out=False).items, HTTPStatus.OK

@endpoint.doc(security='apiKey')
@token_required
def delete(self):
''' Delete all tickets. '''
Serial.all_clean().delete()
db.session.commit()
return '', HTTPStatus.NO_CONTENT

@endpoint.marshal_with(HelloWorldSerializer)
@endpoint.expect(HelloWorldSerializer)
@endpoint.marshal_with(TicketSerializer)
@endpoint.expect(TicketSerializer)
@endpoint.doc(security='apiKey')
@token_required
def post(self):
''' Create HelloWorld. '''
return 'Hello world!', HTTPStatus.OK
''' Generate a new ticket. '''
registered = api.payload.get('n', False)
name_or_number = api.payload.get('name', None)
task = Task.get(api.payload.get('task_id', None))
office = Office.get(api.payload.get('office_id', None))

if not task:
abort(message='Task not found', code=HTTPStatus.NOT_FOUND)

if registered and not name_or_number:
abort(message='Name must be entered for registered tickets.',
code=HTTPStatus.NOT_FOUND)

ticket, exception = Serial.create_new_ticket(task,
office,
name_or_number)

if exception:
abort(message=str(exception))

return ticket, HTTPStatus.OK

@endpoint.route('/<int:ticket_id>')
class GetAndUpdateTicket(Resource):
@endpoint.marshal_with(TicketSerializer)
@endpoint.doc(security='apiKey')
@token_required
@get_or_reject(ticket_id=Serial, _message='Ticket not found')
def get(self, ticket):
''' Get a specific ticket. '''
return ticket, HTTPStatus.OK

@endpoint.marshal_with(TicketSerializer)
@endpoint.expect(TicketSerializer)
@endpoint.doc(security='apiKey')
@token_required
def put(self, ticket_id):
''' Update a specific ticket. '''
ticket = Serial.get(ticket_id)

if not ticket:
abort(message='Ticket not found', code=HTTPStatus.NOT_FOUND)

api.payload.pop('id', '')
ticket.query.update(api.payload)
db.session.commit()
return ticket, HTTPStatus.OK

@endpoint.doc(security='apiKey')
@token_required
@get_or_reject(ticket_id=Serial, _message='Ticket not found')
def delete(self, ticket):
''' Delete a specific ticket. '''
db.session.delete(ticket)
db.session.commit()
return '', HTTPStatus.NO_CONTENT

@endpoint.route('/pull')
class PullTicket(Resource):
@endpoint.marshal_with(TicketSerializer)
@endpoint.param('ticket_id', 'to pull a specific ticket with, by default None.')
@endpoint.param('office_id', 'to pull a specific ticket from, by default None.')
@endpoint.doc(security='apiKey')
@token_required
def get(self):
''' Pull a ticket from the waiting list. '''
ticket_id = request.args.get('ticket_id', None, type=int)
office_id = request.args.get('office_id', None, type=int)
ticket = Serial.get(ticket_id)

if ticket_id and not ticket:
abort(message='Ticket not found', code=HTTPStatus.NOT_FOUND)

next_ticket = ticket or Serial.get_next_ticket()

if not next_ticket:
abort(message='No tickets left to pull', code=HTTPStatus.NOT_FOUND)

# TODO: Add Ticket `Serials` CRUD endpoints here. And remove `HelloWorld`.
next_ticket.pull(office_id, self.auth_token and self.auth_token.id)
return next_ticket, HTTPStatus.OK
68 changes: 68 additions & 0 deletions app/api/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
from functools import wraps
from http import HTTPStatus
from flask import request, current_app
from flask_restx import abort

from app.database import AuthTokens
from app.api.constants import AUTH_HEADER_KEY


def token_required(function):
# FIXME: This a basic approach to verifying tokens, that's insecure especially
# without HTTPS, this needs to be replaced with a login based authentication
# and expiraing temporary tokens rather than constant ones, for better security.
@wraps(function)
def decorator(*args, **kwargs):
token = request.headers.get(AUTH_HEADER_KEY)
token_chunks = token.split(' ') if token else []

if len(token_chunks) > 1:
token = token_chunks[1]

auth_token = AuthTokens.get(token=token)

if not auth_token:
return abort(code=HTTPStatus.UNAUTHORIZED,
message='Authentication is required')

try:
setattr(args[0], 'auth_token', auth_token)
except Exception:
pass

return function(*args, **kwargs)

return decorator


def get_or_reject(**models):
def wrapper(function):
@wraps(function)
def decorator(*args, **kwargs):
with current_app.app_context():
new_kwargs = {}

for kwarg, model in models.items():
if not kwarg.startswith('_'):
record = model.get(kwargs.get(kwarg))
column_name = getattr(model, '__tablename__', ' ')[:-1]

if not record:
abort(message=models.get('_message', 'Model instance not found.'),
code=HTTPStatus.NOT_FOUND)

if column_name == 'serial':
column_name = 'ticket'

new_kwargs[column_name] = record

for kwarg, value in kwargs.items():
if kwarg not in new_kwargs and kwarg not in models:
new_kwargs[kwarg] = value

if len(kwargs.keys()) != len(new_kwargs.keys()):
raise AttributeError('Modules list mismatch arguments.')

return function(*args, **new_kwargs)
return decorator
return wrapper
32 changes: 24 additions & 8 deletions app/api/serializers.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,28 @@
from flask_restx import fields # noqa
from flask_restx import fields

from app.api import api # noqa
from app.api import api


# TODO: Add Serial model serializer here. And remove `HelloWorldSerializer`.
TicketSerializer = api.model('Ticket', {
'id': fields.Integer(required=False, description='ticket identification number.'),
'number': fields.String(required=False, description='ticket number.'),
'timestamp': fields.DateTime(required=False, description='date and time of ticket generation.'),
'date': fields.DateTime(required=False, description='date of ticket generation.'),
'name': fields.String(required=False, description='registered ticket stored value.'),
'n': fields.Boolean(required=False, description='ticket is registered.'),
'p': fields.Boolean(required=False, description='ticket is processed.'),
'pdt': fields.DateTime(required=False, description='ticket pulled date and time.'),
'pulledBy': fields.Integer(required=False, description='user id or token id that pulled ticket.'),
'on_hold': fields.Boolean(required=False, description='ticket is put on hold.'),
'status': fields.String(required=False, description='ticket processing status.'),
'office_id': fields.Integer(required=False, description='office ticket belongs to.'),
'task_id': fields.Integer(required=False, description='task ticket belongs to.'),
})

HelloWorldSerializer = api.model('Hellow', {
'id': fields.Integer(required=False, description='user identification number'),
'name': fields.String(required=True, description='user full name', max_length=100, min_length=3),
'role': fields.String(required=True, description='user role name', enum=['Admin', 'User', 'Operator']),
'address': fields.String(required=False, description='user full address', max_length=200)})

TaskSerializer = api.model('Task', {
'id': fields.Integer(required=False, description='task identification number.'),
'name': fields.String(required=False, description='task name.'),
'timestamp': fields.DateTime(required=False, description='date and time of task creation.'),
'hidden': fields.Boolean(required=False, description='task is is hidden in the touch screen.'),
})
9 changes: 8 additions & 1 deletion app/api/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,19 @@

from app.api import api
from app.api.endpoints.tickets import setup_tickets_endpoint
from app.api.endpoints.tasks import setup_tasks_endpoint


def setup_api():
blueprint = Blueprint('api', __name__)
blueprint = setup_api.__dict__.get('blueprint')

if blueprint:
return blueprint

blueprint = setup_api.__dict__['blueprint'] = Blueprint('api', __name__)

api.init_app(blueprint)
setup_tickets_endpoint()
setup_tasks_endpoint()

return blueprint
Loading

0 comments on commit d14ac68

Please sign in to comment.