-
Notifications
You must be signed in to change notification settings - Fork 69
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #225 from mrf345/development
Add tickets and tasks API endpoints, Resolves #184
- Loading branch information
Showing
20 changed files
with
515 additions
and
145 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
AUTH_HEADER_KEY = 'Authorization' | ||
LIMIT_PER_CHUNK = 30 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.'), | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.