diff --git a/app/__init__.py b/app/__init__.py index 953448a..b369981 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -7,36 +7,33 @@ from app.auth import auth from app.signer import Signer from config import get_config +from flask_graphql import GraphQLView +from flask_cors import CORS env = os.getenv('ENVIRONMENT', 'local') config = get_config(env) -db = SQLAlchemy(engine_options={ - 'isolation_level': 'REPEATABLE_READ' -}) +db = SQLAlchemy(engine_options={'isolation_level': 'REPEATABLE_READ'}) +db.Model.query = db.session.query_property() def create_app(): app = Flask(__name__) + CORS(app) app.config.from_object(config) - - # use g. or app_context db.init_app(app) Migrate(app, db) - app.db = db - from app.models import sql_alchemy - from app.repositories.dashboards.sql_alchemy import SqlAlchemyDashboardsRepository - from app.repositories.tickets.sql_alchemy import SqlAlchemyTicketsRepository - from app.repositories.users.sql_alchemy import SqlAlchemyUsersRepository - - app.tickets_repository = SqlAlchemyTicketsRepository(app.db) - app.dashboard_repository = SqlAlchemyDashboardsRepository(app.db) - app.users_repository = SqlAlchemyUsersRepository(app.db) app.pass_encoder = partial(hashlib.blake2b, key=b'secret_key') app.signer = Signer() - app.before_request(auth) + # app.before_request(auth) - from app.urls import URLS - for endpoint in URLS: - app.route(endpoint.url, **endpoint.optional_params)(endpoint.view) + from app.schema import schema + app.add_url_rule( + '/graphql', + view_func=GraphQLView.as_view( + 'graphql', + schema=schema, + graphiql=True + ) + ) return app diff --git a/app/repositories/__init__.py b/app/dashboards/__init__.py similarity index 100% rename from app/repositories/__init__.py rename to app/dashboards/__init__.py diff --git a/app/dashboards/connections.py b/app/dashboards/connections.py new file mode 100644 index 0000000..e4b5623 --- /dev/null +++ b/app/dashboards/connections.py @@ -0,0 +1,8 @@ +from graphene import relay +from .nodes import DashboardNode + + +class DashboardConnection(relay.Connection): + """A Dashboard Connection""" + class Meta: + node = DashboardNode diff --git a/app/dashboards/nodes.py b/app/dashboards/nodes.py new file mode 100644 index 0000000..b106f1e --- /dev/null +++ b/app/dashboards/nodes.py @@ -0,0 +1,10 @@ +from graphene import relay +from app.models import Dashboard +from graphene_sqlalchemy import SQLAlchemyObjectType + + +class DashboardNode(SQLAlchemyObjectType): + """A Dashboard information""" + class Meta: + model = Dashboard + interfaces = (relay.Node, ) diff --git a/app/db.py b/app/db.py deleted file mode 100644 index f888d68..0000000 --- a/app/db.py +++ /dev/null @@ -1,6 +0,0 @@ -class ISOLATION_LEVEL: - READ_COMMITTED = 'READ COMMITTED' - READ_UNCOMMITTED = 'READ UNCOMMITTED' - REPEATABLE_READ = 'REPEATABLE READ' - SERIALIZABLE = 'SERIALIZABLE' - AUTOCOMMIT = 'AUTOCOMMIT' diff --git a/app/models/sql_alchemy.py b/app/models.py similarity index 60% rename from app/models/sql_alchemy.py rename to app/models.py index 87ae8f7..dd97c89 100644 --- a/app/models/sql_alchemy.py +++ b/app/models.py @@ -1,5 +1,5 @@ -from datetime import datetime from app import db +from datetime import datetime class User(db.Model): @@ -10,49 +10,20 @@ class User(db.Model): email = db.Column(db.String(150), nullable=False, unique=True) password = db.Column(db.String, nullable=False) - __table_args__ = ( - db.CheckConstraint('name <> "" and email <> "" and password <> ""', name='valid_user'), + created_dashboards = db.relationship('Dashboard', backref='creator') + created_tickets = db.relationship( + lambda: Ticket, foreign_keys=lambda: Ticket.creator_id, back_populates='creator' ) - - def __repr__(self): - return f'' - - -class DashboardToUser(db.Model): - __tablename__ = 'DashboardToUser' - - id = db.Column(db.Integer, primary_key=True) - dashboard_id = db.Column(db.Integer, db.ForeignKey('Dashboards.id'), nullable=False) - user_id = db.Column(db.Integer, db.ForeignKey('Users.id'), nullable=False) - role = db.Column( - db.Enum('admin', 'write', 'read', name='userRole'), - nullable=False + assigned_tickets = db.relationship( + lambda: Ticket, foreign_keys=lambda: Ticket.assignee_id, back_populates='assignee' ) - user = db.relationship('User', backref='authorized_dashboards') - dashboard = db.relationship('Dashboard', backref='authorized_users') - __table_args__ = ( - db.UniqueConstraint('dashboard_id', 'user_id'), + db.CheckConstraint('name <> "" and email <> "" and password <> ""', name='valid_user'), ) def __repr__(self): - return f'' - - -class Dashboard(db.Model): - __tablename__ = 'Dashboards' - - id = db.Column(db.Integer, primary_key=True) - name = db.Column(db.String(100), nullable=False) - description = db.Column(db.String) - date_of_creation = db.Column(db.DateTime(), nullable=False, default=datetime.utcnow) - creator_id = db.Column(db.Integer, db.ForeignKey('Users.id')) - - creator = db.relationship('User', backref='created_dashboards') - - def __repr__(self): - return f'' + return f'' class Ticket(db.Model): @@ -64,7 +35,7 @@ class Ticket(db.Model): date_of_creation = db.Column(db.DateTime(), nullable=False, default=datetime.utcnow) creator_id = db.Column(db.Integer, db.ForeignKey('Users.id'), nullable=False) dashboard_id = db.Column(db.Integer, db.ForeignKey('Dashboards.id'), nullable=False) - assignee = db.Column(db.Integer, db.ForeignKey('Users.id')) + assignee_id = db.Column(db.Integer, db.ForeignKey('Users.id')) status = db.Column( db.Enum( 'open', 'inProgress', 'review', 'test', 'closed', name='ticketProgressStatus' @@ -72,8 +43,8 @@ class Ticket(db.Model): default='open' ) - creator = db.relationship('User', backref='created_tickets', foreign_keys=[creator_id]) - dashboard = db.relationship('Dashboard', backref='tickets', foreign_keys=[dashboard_id]) + creator = db.relationship(User, foreign_keys=creator_id, back_populates='created_tickets') + assignee = db.relationship(User, foreign_keys=assignee_id, back_populates='assigned_tickets') __table_args__ = ( db.CheckConstraint("title <> ''", name='valid_ticket'), @@ -81,3 +52,18 @@ class Ticket(db.Model): def __repr__(self): return f'' + + +class Dashboard(db.Model): + __tablename__ = 'Dashboards' + + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(100), nullable=False) + description = db.Column(db.String) + date_of_creation = db.Column(db.DateTime(), nullable=False, default=datetime.utcnow) + creator_id = db.Column(db.Integer, db.ForeignKey('Users.id')) + + tickets = db.relationship('Ticket', backref='dashboard') + + def __repr__(self): + return f'' diff --git a/app/models/__init__.py b/app/models/__init__.py deleted file mode 100644 index ba62c8b..0000000 --- a/app/models/__init__.py +++ /dev/null @@ -1,27 +0,0 @@ -from dataclasses import dataclass - - -class BaseModel: - def to_dict(self): - return {k: v for k, v in vars(self).items()} - - -@dataclass -class TicketModel(BaseModel): - title: str = None - creator_id: int = None - dashboard_id: int = None - id: int = None - date_of_creation: str = None - description: str = None - assignee: int = None - status: str = None - - -@dataclass -class DashboardModel(BaseModel): - name: str = None - creator_id: int = None - id: int = None - description: int = None - date_of_creation: str = None diff --git a/app/queries.py b/app/queries.py deleted file mode 100644 index fc3ea72..0000000 --- a/app/queries.py +++ /dev/null @@ -1,44 +0,0 @@ -class SQLQuery: - ALL_TICKETS = 'SELECT * FROM Tickets' - TICKETS_BY_ASSIGNEE = 'SELECT * FROM Tickets WHERE assignee=%s' - TICKETS_BY_CREATOR = 'SELECT * FROM Tickets WHERE creator_id=%s' - GET_USER_NAME_BY_ID = 'SELECT name FROM Users WHERE id=%s' - GET_USER_BY_EMAIL = 'SELECT * FROM Users WHERE email=%s' - GET_USER = 'SELECT * FROM Users WHERE email=%s and password=%s' - HAS_USER = 'SELECT * FROM Users WHERE email=%s' - INSERT_USER = 'INSERT INTO Users(name, email, "password") VALUES (%s, %s, %s)' - DELETE_USER = 'DELETE FROM Users WHERE email=%s' - INSERT_TICKET = 'INSERT INTO ' \ - 'Tickets(title, description, date_of_creation, creator_id, dashboard_id) ' \ - 'VALUES (:title, :description, CURRENT_TIMESTAMP, :creator_id, :dashboard_id)' - DELETE_TICKET = 'DELETE FROM Tickets WHERE id=%s' - UPDATE_TICKET = 'UPDATE Tickets SET ' \ - 'title = :title, ' \ - 'description = :description, ' \ - 'date_of_creation = :date_of_creation, ' \ - 'creator_id = :creator_id, ' \ - 'dashboard_id = :dashboard_id, ' \ - 'assignee = :assignee, ' \ - 'status = :status ' \ - 'WHERE id = :id' - GET_TICKETS = 'SELECT * FROM Tickets WHERE ' \ - 'id=:id OR ' \ - 'title=:title OR ' \ - 'creator_id=:creator_id OR ' \ - 'dashboard_id=:dashboard_id OR ' \ - 'assignee=:assignee OR ' \ - 'status=:status' - CREATE_DASHBOARD = 'INSERT INTO ' \ - 'Dashboards(name, description, date_of_creation, creator_id) ' \ - 'VALUES (:name, :description, CURRENT_TIMESTAMP, :creator_id)' - DELETE_DASHBOARD = 'DELETE FROM Dashboards WHERE id=%s' - UPDATE_DASHBOARD = 'UPDATE Dashboards SET ' \ - 'name = :name, ' \ - 'description = :description, ' \ - 'date_of_creation = :date_of_creation, ' \ - 'creator_id = :creator_id ' \ - 'WHERE id = :id' - GET_DASHBOARDS = 'SELECT * FROM Dashboards WHERE ' \ - 'id=:id OR ' \ - 'name=:name OR ' \ - 'creator_id=:creator_id' diff --git a/app/repositories/dashboards/raw_sql.py b/app/repositories/dashboards/raw_sql.py deleted file mode 100644 index cf48c4c..0000000 --- a/app/repositories/dashboards/raw_sql.py +++ /dev/null @@ -1,54 +0,0 @@ -from functools import wraps - -from sqlalchemy import text -from sqlalchemy.exc import SQLAlchemyError -from app.models import DashboardModel -from app.queries import SQLQuery -from logging import getLogger -from app.repositories.exceptions import DataBaseError - - -logger = getLogger(__name__) - - -def exceptions_wrapper(func): - @wraps(func) - def wrapper(*args, **kwargs): - try: - return func(*args, **kwargs) - except SQLAlchemyError as e: - logger.exception(e) - raise DataBaseError() - return wrapper - - -class RawSqlDashboardsRepository: - - def __init__(self, db): - self.db = db - - @exceptions_wrapper - def add(self, model: DashboardModel) -> DashboardModel: - with self.db.begin() as conn: - conn.execute(text(SQLQuery.CREATE_DASHBOARD), model.to_dict()) - return model - - @exceptions_wrapper - def delete(self, model: DashboardModel) -> DashboardModel: - with self.db.begin() as conn: - conn.execute(text(SQLQuery.DELETE_DASHBOARD), model.id) - return model - - @exceptions_wrapper - def update(self, model: DashboardModel) -> DashboardModel: - with self.db.begin() as conn: - conn.execute(text(SQLQuery.UPDATE_DASHBOARD), model.to_dict()) - return model - - @exceptions_wrapper - def query(self, **args) -> [DashboardModel]: - with self.db.begin() as conn: - return [DashboardModel(**row) - for row in conn.execute( - text(SQLQuery.GET_DASHBOARDS), DashboardModel(**args).to_dict() - )] diff --git a/app/repositories/dashboards/sql_alchemy.py b/app/repositories/dashboards/sql_alchemy.py deleted file mode 100644 index 7d8e412..0000000 --- a/app/repositories/dashboards/sql_alchemy.py +++ /dev/null @@ -1,48 +0,0 @@ -from functools import wraps -from flask_sqlalchemy import SQLAlchemy -from sqlalchemy.exc import SQLAlchemyError -from app.models.sql_alchemy import Dashboard -from logging import getLogger -from app.repositories.exceptions import DataBaseError - - -logger = getLogger(__name__) - - -def exceptions_wrapper(func): - @wraps(func) - def wrapper(*args, **kwargs): - try: - return func(*args, **kwargs) - except SQLAlchemyError as e: - logger.exception(e) - raise DataBaseError() - return wrapper - - -class SqlAlchemyDashboardsRepository: - - def __init__(self, db: SQLAlchemy): - self.db = db - - @exceptions_wrapper - def add(self, model: Dashboard) -> Dashboard: - self.db.session.add(model) - self.db.session.commit() - return model - - @exceptions_wrapper - def delete(self, model: Dashboard) -> Dashboard: - self.db.session.delete(model) - self.db.session.commit() - return model - - @exceptions_wrapper - def update(self, model: Dashboard) -> Dashboard: - Dashboard.query.filter_by(id=model.id).update(dict(model)) - self.db.session.commit() - return model - - @exceptions_wrapper - def query(self, **args) -> [Dashboard]: - return list(Dashboard.query.filter_by(**args)) diff --git a/app/repositories/exceptions.py b/app/repositories/exceptions.py deleted file mode 100644 index b9f2340..0000000 --- a/app/repositories/exceptions.py +++ /dev/null @@ -1,3 +0,0 @@ -class DataBaseError(Exception): - def __init__(self): - super().__init__('Unable to connect to Database') diff --git a/app/repositories/tickets/raw_sql.py b/app/repositories/tickets/raw_sql.py deleted file mode 100644 index 117afdb..0000000 --- a/app/repositories/tickets/raw_sql.py +++ /dev/null @@ -1,54 +0,0 @@ -from functools import wraps - -from sqlalchemy import text -from sqlalchemy.exc import SQLAlchemyError -from app.models import TicketModel -from app.queries import SQLQuery -from logging import getLogger -from app.repositories.exceptions import DataBaseError - - -logger = getLogger(__name__) - - -def exceptions_wrapper(func): - @wraps(func) - def wrapper(*args, **kwargs): - try: - return func(*args, **kwargs) - except SQLAlchemyError as e: - logger.exception(e) - raise DataBaseError() - return wrapper - - -class RawSqlTicketsRepository: - - def __init__(self, db): - self.db = db - - @exceptions_wrapper - def add(self, model: TicketModel) -> TicketModel: - with self.db.begin() as conn: - conn.execute(text(SQLQuery.INSERT_TICKET), model.to_dict()) - return model - - @exceptions_wrapper - def delete(self, model: TicketModel) -> TicketModel: - with self.db.begin() as conn: - conn.execute(text(SQLQuery.DELETE_TICKET), model.id) - return model - - @exceptions_wrapper - def update(self, model: TicketModel) -> TicketModel: - with self.db.begin() as conn: - conn.execute(text(SQLQuery.UPDATE_TICKET), model.to_dict()) - return model - - @exceptions_wrapper - def query(self, **args) -> [TicketModel]: - with self.db.begin() as conn: - return [TicketModel(**row) - for row in conn.execute( - text(SQLQuery.GET_TICKETS), TicketModel(**args).to_dict() - )] diff --git a/app/repositories/tickets/sql_alchemy.py b/app/repositories/tickets/sql_alchemy.py deleted file mode 100644 index 3364ded..0000000 --- a/app/repositories/tickets/sql_alchemy.py +++ /dev/null @@ -1,48 +0,0 @@ -from functools import wraps -from flask_sqlalchemy import SQLAlchemy -from sqlalchemy.exc import SQLAlchemyError -from app.models.sql_alchemy import Ticket -from logging import getLogger -from app.repositories.exceptions import DataBaseError - - -logger = getLogger(__name__) - - -def exceptions_wrapper(func): - @wraps(func) - def wrapper(*args, **kwargs): - try: - return func(*args, **kwargs) - except SQLAlchemyError as e: - logger.exception(e) - raise DataBaseError() - return wrapper - - -class SqlAlchemyTicketsRepository: - - def __init__(self, db: SQLAlchemy): - self.db = db - - @exceptions_wrapper - def add(self, model: Ticket) -> Ticket: - self.db.session.add(model) - self.db.session.commit() - return model - - @exceptions_wrapper - def delete(self, model: Ticket) -> Ticket: - self.db.session.delete(model) - self.db.session.commit() - return model - - @exceptions_wrapper - def update(self, model: Ticket) -> Ticket: - Ticket.query.filter_by(id=model.id).update(dict(model)) - self.db.session.commit() - return model - - @exceptions_wrapper - def query(self, **args) -> [Ticket]: - return list(Ticket.query.filter_by(**args)) diff --git a/app/repositories/users/sql_alchemy.py b/app/repositories/users/sql_alchemy.py deleted file mode 100644 index f204ee2..0000000 --- a/app/repositories/users/sql_alchemy.py +++ /dev/null @@ -1,48 +0,0 @@ -from functools import wraps -from flask_sqlalchemy import SQLAlchemy -from sqlalchemy.exc import SQLAlchemyError -from app.models.sql_alchemy import User -from logging import getLogger -from app.repositories.exceptions import DataBaseError - - -logger = getLogger(__name__) - - -def exceptions_wrapper(func): - @wraps(func) - def wrapper(*args, **kwargs): - try: - return func(*args, **kwargs) - except SQLAlchemyError as e: - logger.exception(e) - raise DataBaseError() - return wrapper - - -class SqlAlchemyUsersRepository: - - def __init__(self, db: SQLAlchemy): - self.db = db - - @exceptions_wrapper - def add(self, model: User) -> User: - self.db.session.add(model) - self.db.session.commit() - return model - - @exceptions_wrapper - def delete(self, model: User) -> User: - self.db.session.delete(model) - self.db.session.commit() - return model - - @exceptions_wrapper - def update(self, model: User) -> User: - User.query.filter_by(id=model.id).update(dict(model)) - self.db.session.commit() - return model - - @exceptions_wrapper - def query(self, **args) -> [User]: - return list(User.query.filter_by(**args)) diff --git a/app/schema.py b/app/schema.py new file mode 100644 index 0000000..48abc2a --- /dev/null +++ b/app/schema.py @@ -0,0 +1,15 @@ +from graphene import ObjectType, Schema, relay +from app.users.connections import UserConnection +from app.dashboards.connections import DashboardConnection +from app.tickets.connections import TicketConnection +from graphene_sqlalchemy import SQLAlchemyConnectionField + + +class Query(ObjectType): + node = relay.Node.Field() + users = SQLAlchemyConnectionField(UserConnection) + dashboards = SQLAlchemyConnectionField(DashboardConnection) + tickets = SQLAlchemyConnectionField(TicketConnection) + + +schema = Schema(query=Query) diff --git a/app/status_codes.py b/app/status_codes.py deleted file mode 100644 index 6702c05..0000000 --- a/app/status_codes.py +++ /dev/null @@ -1,2 +0,0 @@ -HTTP_CONFLICT_CODE = 409 -BAD_REQUEST = 400 diff --git a/app/templates/dashboard.html b/app/templates/dashboard.html deleted file mode 100644 index 35d4a6c..0000000 --- a/app/templates/dashboard.html +++ /dev/null @@ -1,23 +0,0 @@ -{% extends 'users.html' %} - -{% block content %} -

Board

- - - - - - - - - - - - - - - - - -
Board nameDescriptionCreatedId of creatorSee tickets
{{ board["name"] }}{{ board["description"] }}{{ board["date_of_creation"] }}{{ board["creator_id"] }}open
-{% endblock %} \ No newline at end of file diff --git a/app/templates/dashboards.html b/app/templates/dashboards.html deleted file mode 100644 index 9df7030..0000000 --- a/app/templates/dashboards.html +++ /dev/null @@ -1,41 +0,0 @@ -{% extends 'users.html' %} - -{% block content %} -

Boards

- {% if boards %} - - - - - - - - - {% for board in boards %} - - - - - - - {% endfor %} - -
Board nameCreatedId of creatorOpen
{{ board["name"] }}{{ board["date_of_creation"] }}{{ board["creator_id"] }}open
- {% endif %} -
-
-

Create new Dashboard

-
- - - - - -
-
-{% endblock %} \ No newline at end of file diff --git a/app/templates/index.html b/app/templates/index.html deleted file mode 100644 index 0ac3248..0000000 --- a/app/templates/index.html +++ /dev/null @@ -1,32 +0,0 @@ - - - - - Task tracker - - -

Hi there!

- -
- {% block content %}{% endblock %} -
- - - \ No newline at end of file diff --git a/app/templates/login.html b/app/templates/login.html deleted file mode 100644 index 47beb44..0000000 --- a/app/templates/login.html +++ /dev/null @@ -1,15 +0,0 @@ -{% extends 'index.html' %} - -{% block header %} -

Log In

-{% endblock %} - -{% block content %} -
- - - - - -
-{% endblock %} \ No newline at end of file diff --git a/app/templates/register.html b/app/templates/register.html deleted file mode 100644 index 76b6467..0000000 --- a/app/templates/register.html +++ /dev/null @@ -1,17 +0,0 @@ -{% extends 'index.html' %} - -{% block header %} -

Register

-{% endblock %} - -{% block content %} -
- - - - - - - -
-{% endblock %} \ No newline at end of file diff --git a/app/templates/ticket.html b/app/templates/ticket.html deleted file mode 100644 index a4c6a95..0000000 --- a/app/templates/ticket.html +++ /dev/null @@ -1,25 +0,0 @@ -{% extends 'users.html' %} - -{% block content %} -

Ticket

- - - - - - - - - - - - - - - - - - - -
titledescriptioncreatedcreator idassigneestatus
{{ ticket["title"] }}{{ ticket["description"] }}{{ ticket["date_of_creation"] }}{{ ticket["creator_id"] }}{{ ticket["assignee"] }}{{ ticket["status"] }}
-{% endblock %} \ No newline at end of file diff --git a/app/templates/tickets.html b/app/templates/tickets.html deleted file mode 100644 index 8cd1bae..0000000 --- a/app/templates/tickets.html +++ /dev/null @@ -1,41 +0,0 @@ -{% extends 'users.html' %} - -{% block content %} -

Tickets

- {% if tickets %} - - - - - - - - - {% for ticket in tickets %} - - - - - - - {% endfor %} - -
titleassigneestatusopen details
{{ ticket["title"] }}{{ ticket["assignee"] }}{{ ticket["status"] }}open
- {% endif %} -
-
-

Create new ticket

-
- - - - - -
-
-{% endblock %} \ No newline at end of file diff --git a/app/templates/users.html b/app/templates/users.html deleted file mode 100644 index 331621f..0000000 --- a/app/templates/users.html +++ /dev/null @@ -1,13 +0,0 @@ -{% extends 'index.html' %} - -{% block content %} -

User : {{ user_name }}

- -
-{% endblock %} diff --git a/app/repositories/dashboards/__init__.py b/app/tickets/__init__.py similarity index 100% rename from app/repositories/dashboards/__init__.py rename to app/tickets/__init__.py diff --git a/app/tickets/connections.py b/app/tickets/connections.py new file mode 100644 index 0000000..6d0c3b7 --- /dev/null +++ b/app/tickets/connections.py @@ -0,0 +1,8 @@ +from graphene import relay +from .nodes import TicketNode + + +class TicketConnection(relay.Connection): + """A Ticket Connection""" + class Meta: + node = TicketNode diff --git a/app/tickets/nodes.py b/app/tickets/nodes.py new file mode 100644 index 0000000..936c1fc --- /dev/null +++ b/app/tickets/nodes.py @@ -0,0 +1,10 @@ +from graphene import relay +from app.models import Ticket +from graphene_sqlalchemy import SQLAlchemyObjectType + + +class TicketNode(SQLAlchemyObjectType): + """A Ticket information""" + class Meta: + model = Ticket + interfaces = (relay.Node, ) diff --git a/app/urls.py b/app/urls.py deleted file mode 100644 index ab4d86a..0000000 --- a/app/urls.py +++ /dev/null @@ -1,29 +0,0 @@ -from app.views.board import board -from app.views.boards import boards -from app.views.ticket import ticket -from app.views.tickets import tickets -from app.views.users import users -from app.views.logout import logout -from app.views.index import index -from app.views.register import register -from app.views.login import login - - -class URL: - def __init__(self, url, view, **params): - self.url = url - self.view = view - self.optional_params = params - - -URLS = ( - URL('/', index), - URL('/register', register, methods=['POST', 'GET']), - URL('/login', login, methods=['POST', 'GET']), - URL('/logout', logout, methods=['GET']), - URL('/users/', users, methods=['GET']), - URL('/boards', boards, methods=['GET', 'POST']), - URL('/boards/', board, methods=['GET', 'PUT', 'DELETE']), - URL('/boards//tickets', tickets, methods=['GET', 'POST']), - URL('/boards//tickets/', ticket, methods=['GET', 'PUT', 'DELETE']), -) diff --git a/app/repositories/tickets/__init__.py b/app/users/__init__.py similarity index 100% rename from app/repositories/tickets/__init__.py rename to app/users/__init__.py diff --git a/app/users/connections.py b/app/users/connections.py new file mode 100644 index 0000000..ac66400 --- /dev/null +++ b/app/users/connections.py @@ -0,0 +1,8 @@ +from graphene import relay +from .nodes import UserNode + + +class UserConnection(relay.Connection): + """A User Connection""" + class Meta: + node = UserNode diff --git a/app/users/nodes.py b/app/users/nodes.py new file mode 100644 index 0000000..737a139 --- /dev/null +++ b/app/users/nodes.py @@ -0,0 +1,10 @@ +from graphene import relay +from app.models import User +from graphene_sqlalchemy import SQLAlchemyObjectType + + +class UserNode(SQLAlchemyObjectType): + """A User information""" + class Meta: + model = User + interfaces = (relay.Node, ) diff --git a/app/views/__init__.py b/app/views/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/views/board.py b/app/views/board.py deleted file mode 100644 index 7924084..0000000 --- a/app/views/board.py +++ /dev/null @@ -1,9 +0,0 @@ -from flask import current_app, render_template, request - - -def board(board_id): - if request.method == 'GET': - return render_template( - 'dashboard.html', - board=current_app.dashboard_repository.query(id=board_id)[0] - ) diff --git a/app/views/boards.py b/app/views/boards.py deleted file mode 100644 index 60eb974..0000000 --- a/app/views/boards.py +++ /dev/null @@ -1,14 +0,0 @@ -from flask import current_app, render_template, request, url_for, redirect -from app.models.sql_alchemy import Dashboard - - -def boards(): - if request.method == 'GET': - return render_template( - 'dashboards.html', - boards=current_app.dashboard_repository.query(creator_id=request.user.id) - ) - if request.method == 'POST': - model = Dashboard(**{'creator_id': request.user.id, **request.form}) - current_app.dashboard_repository.add(model) - return redirect(url_for('boards', user_id=request.user.id)) diff --git a/app/views/index.py b/app/views/index.py deleted file mode 100644 index fa80726..0000000 --- a/app/views/index.py +++ /dev/null @@ -1,7 +0,0 @@ -from flask import render_template, request - - -def index(): - return render_template( - 'index.html', email=request.cookies.get('email') - ) diff --git a/app/views/login.py b/app/views/login.py deleted file mode 100644 index 89df95a..0000000 --- a/app/views/login.py +++ /dev/null @@ -1,15 +0,0 @@ -from flask import request, current_app, make_response, redirect, url_for, Response, render_template -from app.status_codes import BAD_REQUEST - - -def login(): - if request.method == 'POST': - email = request.form.get('email') - password = current_app.pass_encoder(request.form.get('password', '').encode()).hexdigest() - user_info = current_app.users_repository.query(email=email, password=password)[0] - if user_info: - resp = make_response(redirect(url_for('users', user_id=user_info.id))) - resp.set_cookie('email', current_app.signer.sign(email)) - return resp - return Response('Unable to login. Wrong credentials provided.', status=BAD_REQUEST) - return render_template('login.html') diff --git a/app/views/logout.py b/app/views/logout.py deleted file mode 100644 index b456059..0000000 --- a/app/views/logout.py +++ /dev/null @@ -1,7 +0,0 @@ -from flask import make_response, url_for, redirect - - -def logout(): - resp = make_response(redirect(url_for('login'))) - resp.set_cookie('email', '') - return resp diff --git a/app/views/register.py b/app/views/register.py deleted file mode 100644 index a256e8e..0000000 --- a/app/views/register.py +++ /dev/null @@ -1,21 +0,0 @@ -from flask import request, current_app, Response, render_template, make_response, redirect, url_for -from app.models.sql_alchemy import User -from app.status_codes import HTTP_CONFLICT_CODE - - -def register(): - if request.method == 'POST': - name = request.form.get('username') - email = request.form.get('email') - if current_app.users_repository.query(email=email): - return Response('User already exist', status=HTTP_CONFLICT_CODE) - password = current_app.pass_encoder( - request.form.get('password', '').encode() - ).hexdigest() - - try: - current_app.users_repository.add(User(name=name, email=email, password=password)) - except Exception as e: - return Response('Unable to register the user', status=500) - return make_response(redirect(url_for('login'))) - return render_template('register.html', email=request.cookies.get('email')) diff --git a/app/views/ticket.py b/app/views/ticket.py deleted file mode 100644 index 8e232de..0000000 --- a/app/views/ticket.py +++ /dev/null @@ -1,9 +0,0 @@ -from flask import current_app, render_template, request - - -def ticket(board_id, ticket_id): - if request.method == 'GET': - return render_template( - 'ticket.html', - ticket=current_app.tickets_repository.query(id=ticket_id)[0] - ) diff --git a/app/views/tickets.py b/app/views/tickets.py deleted file mode 100644 index 41e8e43..0000000 --- a/app/views/tickets.py +++ /dev/null @@ -1,21 +0,0 @@ -from flask import current_app, render_template, request, redirect, url_for -from app.models.sql_alchemy import Ticket - - -def tickets(board_id): - if request.method == 'GET': - return render_template( - 'tickets.html', - board_id=board_id, - tickets=current_app.tickets_repository.query(dashboard_id=board_id) - ) - elif request.method == 'POST': - model = Ticket( - **{ - 'creator_id': request.user.id, - 'dashboard_id': board_id, - **request.form - } - ) - current_app.tickets_repository.add(model) - return redirect(url_for('tickets', board_id=board_id)) diff --git a/app/views/users.py b/app/views/users.py deleted file mode 100644 index 40a6abf..0000000 --- a/app/views/users.py +++ /dev/null @@ -1,6 +0,0 @@ -from flask import current_app, render_template - - -def users(user_id): - user_name = current_app.users_repository.query(id=user_id)[0].name - return render_template('users.html', user_name=user_name, user_id=user_id) diff --git a/init_db_scripts/1init.sql b/init_db_scripts/1init.sql deleted file mode 100644 index a1ee67b..0000000 --- a/init_db_scripts/1init.sql +++ /dev/null @@ -1,49 +0,0 @@ -CREATE TYPE ticketProgressStatus AS ENUM ( - 'open', 'in progress', 'review', 'test', 'closed' -); - -CREATE TYPE userRole AS ENUM ( - 'admin', 'write', 'read' -); - -CREATE TABLE Users ( - id serial PRIMARY KEY, - name varchar(100) not null, - email varchar(150) not null, - password varchar not null, - UNIQUE (email), - CONSTRAINT valid_user CHECK ( - name <> '' and email <> '' and password <> '' - ) -); - -CREATE TABLE Dashboards ( - id serial PRIMARY KEY, - name varchar(100) not null, - description character varying, - date_of_creation timestamp not null, - creator_id integer REFERENCES Users not null -); - -CREATE TABLE Tickets ( - id serial PRIMARY KEY, - title varchar(300) not null, - description character varying, - date_of_creation timestamp not null, - creator_id integer REFERENCES Users not null, - dashboard_id integer REFERENCES Dashboards not null, - assignee integer REFERENCES Users, - status ticketProgressStatus default 'open', - CONSTRAINT valid_ticket CHECK ( - title <> '' - ), - UNIQUE (dashboard_id, creator_id) -); - -CREATE TABLE DashboardToUser ( - id serial PRIMARY KEY, - dashboard_id integer REFERENCES Dashboards not null, - user_id integer REFERENCES Users not null, - role userRole not null, - UNIQUE (dashboard_id, user_id) -); diff --git a/migrations/versions/08da4446aef6_create_dashboard_table.py b/migrations/versions/08da4446aef6_create_dashboard_table.py index e6fb4bc..c4e8831 100644 --- a/migrations/versions/08da4446aef6_create_dashboard_table.py +++ b/migrations/versions/08da4446aef6_create_dashboard_table.py @@ -1,4 +1,4 @@ -"""Create Dashboard table +"""Create DashboardNode table Revision ID: 08da4446aef6 Revises: e423983e4fff diff --git a/migrations/versions/a22b9033a38d_.py b/migrations/versions/a22b9033a38d_.py new file mode 100644 index 0000000..ef1776f --- /dev/null +++ b/migrations/versions/a22b9033a38d_.py @@ -0,0 +1,45 @@ +"""empty message + +Revision ID: a22b9033a38d +Revises: f005ae29b85e +Create Date: 2020-01-19 19:54:38.184938 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = 'a22b9033a38d' +down_revision = 'f005ae29b85e' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('DashboardToUser') + op.add_column('Tickets', sa.Column('assignee_id', sa.Integer(), nullable=True)) + op.drop_constraint('Tickets_assignee_fkey', 'Tickets', type_='foreignkey') + op.create_foreign_key(None, 'Tickets', 'Users', ['assignee_id'], ['id']) + op.drop_column('Tickets', 'assignee') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('Tickets', sa.Column('assignee', sa.INTEGER(), autoincrement=False, nullable=True)) + op.drop_constraint(None, 'Tickets', type_='foreignkey') + op.create_foreign_key('Tickets_assignee_fkey', 'Tickets', 'Users', ['assignee'], ['id']) + op.drop_column('Tickets', 'assignee_id') + op.create_table('DashboardToUser', + sa.Column('id', sa.INTEGER(), server_default=sa.text('nextval(\'"DashboardToUser_id_seq"\'::regclass)'), autoincrement=True, nullable=False), + sa.Column('dashboard_id', sa.INTEGER(), autoincrement=False, nullable=False), + sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=False), + sa.Column('role', postgresql.ENUM('admin', 'write', 'read', name='userRole'), autoincrement=False, nullable=False), + sa.ForeignKeyConstraint(['dashboard_id'], ['Dashboards.id'], name='DashboardToUser_dashboard_id_fkey'), + sa.ForeignKeyConstraint(['user_id'], ['Users.id'], name='DashboardToUser_user_id_fkey'), + sa.PrimaryKeyConstraint('id', name='DashboardToUser_pkey'), + sa.UniqueConstraint('dashboard_id', 'user_id', name='DashboardToUser_dashboard_id_user_id_key') + ) + # ### end Alembic commands ### diff --git a/requirements.txt b/requirements.txt index a6cbdf1..7afc657 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,18 @@ alembic==1.2.1 +aniso8601==7.0.0 atomicwrites==1.3.0 attrs==19.1.0 Click==7.0 Flask==1.0.2 +Flask-Cors==3.0.8 +Flask-GraphQL==2.0.1 Flask-Migrate==2.5.2 Flask-SQLAlchemy==2.4.1 +graphene==2.1.8 +graphene-sqlalchemy==2.2.2 +graphql-core==2.2.1 +graphql-relay==2.0.1 +graphql-server-core==1.1.3 itsdangerous==1.1.0 Jinja2==2.10.1 Mako==1.1.0 @@ -12,11 +20,14 @@ MarkupSafe==1.1.1 mock==3.0.5 more-itertools==7.0.0 pluggy==0.9.0 +promise==2.2.1 psycopg2-binary==2.8.3 py==1.8.0 pytest==4.4.0 python-dateutil==2.8.0 python-editor==1.0.4 +Rx==1.6.1 +singledispatch==3.4.0.3 six==1.12.0 SQLAlchemy==1.3.8 uWSGI==2.0.18 diff --git a/run.py b/run.py index cd7d29a..f19e9b2 100644 --- a/run.py +++ b/run.py @@ -9,4 +9,4 @@ if __name__ == '__main__': # Bind to PORT if defined, otherwise default to 5000. port = int(os.environ.get('PORT', 5000)) - app.run(host='0.0.0.0', debug=DEBUG, port=port) + app.run(host='0.0.0.0', debug=DEBUG, port=port, threaded=True) diff --git a/tests/conftest.py b/tests/conftest.py index 3359466..6383c49 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,33 +1,11 @@ import pytest +from graphene.test import Client from app import create_app +from app.schema import schema -# config = { -# 'dsn': 'postgresql://admin:mypassword@localhost/test' -# } -# -# -# @pytest.fixture(scope='session') -# def app(): -# app = create_app(config) -# return app -# -# -# client = app.test_client() -# USER = { -# 'username': 'test_user', -# 'email': 'test_user_email@test', -# 'password': '12345' -# } - - -# @pytest.fixture() -# def client(app): -# return app.test_client() -# -# -# @pytest.fixture(autouse=True) -# def db_fixture(app): -# app.db.execute('TRUNCATE TABLE users') -# yield app.db -# app.db.execute() +@pytest.fixture(autouse=True, scope='session', name='client') +def init_client(): + app = create_app() + with app.app_context(): + yield Client(schema=schema) diff --git a/app/repositories/users/__init__.py b/tests/dashboards/__init__.py similarity index 100% rename from app/repositories/users/__init__.py rename to tests/dashboards/__init__.py diff --git a/tests/dashboards/test_queries.py b/tests/dashboards/test_queries.py new file mode 100644 index 0000000..261e22c --- /dev/null +++ b/tests/dashboards/test_queries.py @@ -0,0 +1,13 @@ +def test_get_all_dashboards(client): + assert {} == client.execute("""{ +dashboards { + edges { + node { + name + description + dateOfCreation + creatorId + } + } + } +}""")['data'] diff --git a/tests/mocks.py b/tests/mocks.py deleted file mode 100644 index 564c94a..0000000 --- a/tests/mocks.py +++ /dev/null @@ -1,16 +0,0 @@ -from mock import Mock - - -def create_db_mock(): - mock_db = Mock() - mock_db._con = Mock() - mock_db._con.execute = Mock() - - class ConManager: - def __enter__(self): - return mock_db._con - - def __exit__(self, exc_type, exc_val, exc_tb): ... - - mock_db.begin = ConManager - return mock_db diff --git a/tests/repositories/__init__.py b/tests/repositories/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/repositories/tickets/__init__.py b/tests/repositories/tickets/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/repositories/tickets/test_raw_sql.py b/tests/repositories/tickets/test_raw_sql.py deleted file mode 100644 index f6ee014..0000000 --- a/tests/repositories/tickets/test_raw_sql.py +++ /dev/null @@ -1,78 +0,0 @@ -import pytest -from mock import Mock -from sqlalchemy.exc import SQLAlchemyError -from app.models import TicketModel -from app.queries import SQLQuery -from app.repositories.exceptions import DataBaseError -from app.repositories.tickets.raw_sql import RawSqlTicketsRepository -from tests.mocks import create_db_mock - - -class TestRawSqlTicketsRepository: - ticket = TicketModel( - id=1, title='First ticket', creator_id=1, date_of_creation='15-01-2019' - ) - - def setup_method(self): - self.db = create_db_mock() - self.repo = RawSqlTicketsRepository(self.db) - - # @pytest.mark.parametrize( - # 'method,query,query_args', - # [ - # ( - # 'add', - # SQLQuery.INSERT_TICKET, - # ticket.to_dict() - # ), - # ( - # 'delete', - # SQLQuery.DELETE_TICKET, - # 1 - # ), - # ( - # 'update', - # SQLQuery.UPDATE_TICKET, - # ticket.to_dict() - # ) - # ] - # ) - # def test_calls_db(self, method, query, query_args): - # actual = getattr(self.repo, method)(self.ticket) - # - # self.db._con.execute.assert_called_with(query, query_args) - # assert self.ticket == actual - # - # @pytest.mark.parametrize( - # 'method', - # ['add', 'delete', 'update'] - # ) - # def test_unable_to_connect_to_db(self, method): - # self.db.begin = Mock(side_effect=SQLAlchemyError) - # - # with pytest.raises(DataBaseError): - # getattr(self.repo, method)(self.ticket) - # - # @pytest.mark.parametrize( - # 'args', - # [ - # {'id': 1}, - # {'title': 'First ticket'}, - # {'assignee': 1}, - # {'creator_id': 1}, - # {'status': 'open'}, - # ] - # ) - # def test_query_calls_db(self, args): - # self.db._con.execute = Mock(return_value=[self.ticket.to_dict()]) - # - # actual = self.repo.query(**args) - # - # self.db._con.execute.assert_called_with(SQLQuery.GET_TICKETS, args) - # assert [self.ticket] == actual - # - # def test_query_unable_to_connect_to_db(self): - # self.db.begin = Mock(side_effect=SQLAlchemyError) - # - # with pytest.raises(DataBaseError): - # self.repo.query(id=1) diff --git a/tests/test_endpoints.py b/tests/test_endpoints.py deleted file mode 100644 index 22fee2f..0000000 --- a/tests/test_endpoints.py +++ /dev/null @@ -1,43 +0,0 @@ -def test_pass(): - pass - - -# @pytest.mark.parametrize('path,result', [ -# ('/register', '
'), -# ('/login', '') -# ]) -# def test_pages_loading(path, result): -# response = client.get(path, follow_redirects=True) -# -# assert response.status == '200 OK' -# assert result in response.data.decode() -# -# -# def test_register_login(db_fixture): -# # register and login user -# response = client.post('/register', data=USER, follow_redirects=True) -# assert response.status == '200 OK' -# assert db_fixture.execute(SQLQuery.HAS_USER, (USER['email'],)).fetchone() -# -# # fail to register user with the same email -# response = client.post('/register', data=USER, follow_redirects=True) -# assert response.status == '409 CONFLICT' -# assert 'User already exist' == response.data.decode() -# -# # login user -# response = client.post('/login', data=USER, follow_redirects=True) -# assert response.status == '200 OK' -# -# # fail to login user -# # user_with_wrong_email = copy(USER) -# # user_with_wrong_email['email'] = 'wrong_email@test' -# user_with_wrong_email = {**USER, 'email': 'wrong_email@test'} -# response = client.post('/login', data=user_with_wrong_email, follow_redirects=True) -# assert response.status == '400 BAD REQUEST' -# assert 'Unable to login. Wrong credentials provided.' == response.data.decode() -# -# user_with_wrong_password = copy(USER) -# user_with_wrong_password['password'] = 'wrong_password' -# response = client.post('/login', data=user_with_wrong_password, follow_redirects=True) -# assert response.status == '400 BAD REQUEST' -# assert 'Unable to login. Wrong credentials provided.' == response.data.decode()