From 29506edca732f16784bf39c0ce5c3ea92c86c733 Mon Sep 17 00:00:00 2001 From: vdruzhinin Date: Sun, 19 Jan 2020 20:04:44 +0200 Subject: [PATCH 1/3] Base migration to Graphene --- app/__init__.py | 27 +++--- app/{repositories => dashboards}/__init__.py | 0 app/dashboards/connections.py | 8 ++ app/dashboards/models.py | 21 +++++ app/dashboards/nodes.py | 10 +++ app/db.py | 6 -- app/models/__init__.py | 27 ------ app/models/sql_alchemy.py | 83 ------------------- app/repositories/dashboards/raw_sql.py | 54 ------------ app/repositories/dashboards/sql_alchemy.py | 48 ----------- app/repositories/exceptions.py | 3 - app/repositories/tickets/raw_sql.py | 54 ------------ app/repositories/tickets/sql_alchemy.py | 48 ----------- app/repositories/users/__init__.py | 0 app/repositories/users/sql_alchemy.py | 48 ----------- app/schema.py | 15 ++++ app/status_codes.py | 2 - app/templates/dashboard.html | 23 ----- app/templates/dashboards.html | 41 --------- app/templates/index.html | 32 ------- app/templates/login.html | 15 ---- app/templates/register.html | 17 ---- app/templates/ticket.html | 25 ------ app/templates/tickets.html | 41 --------- app/templates/users.html | 13 --- .../dashboards => tickets}/__init__.py | 0 app/tickets/connections.py | 8 ++ app/tickets/models.py | 33 ++++++++ app/tickets/nodes.py | 10 +++ app/urls.py | 29 ------- .../tickets => users}/__init__.py | 0 app/users/connections.py | 8 ++ app/users/models.py | 17 ++++ app/users/nodes.py | 10 +++ app/views/__init__.py | 0 app/views/board.py | 9 -- app/views/boards.py | 14 ---- app/views/index.py | 7 -- app/views/login.py | 15 ---- app/views/logout.py | 7 -- app/views/register.py | 21 ----- app/views/ticket.py | 9 -- app/views/tickets.py | 21 ----- app/views/users.py | 6 -- init_db_scripts/1init.sql | 49 ----------- migrations/versions/a22b9033a38d_.py | 45 ++++++++++ requirements.txt | 11 ++- run.py | 2 +- 48 files changed, 208 insertions(+), 784 deletions(-) rename app/{repositories => dashboards}/__init__.py (100%) create mode 100644 app/dashboards/connections.py create mode 100644 app/dashboards/models.py create mode 100644 app/dashboards/nodes.py delete mode 100644 app/db.py delete mode 100644 app/models/__init__.py delete mode 100644 app/models/sql_alchemy.py delete mode 100644 app/repositories/dashboards/raw_sql.py delete mode 100644 app/repositories/dashboards/sql_alchemy.py delete mode 100644 app/repositories/exceptions.py delete mode 100644 app/repositories/tickets/raw_sql.py delete mode 100644 app/repositories/tickets/sql_alchemy.py delete mode 100644 app/repositories/users/__init__.py delete mode 100644 app/repositories/users/sql_alchemy.py create mode 100644 app/schema.py delete mode 100644 app/status_codes.py delete mode 100644 app/templates/dashboard.html delete mode 100644 app/templates/dashboards.html delete mode 100644 app/templates/index.html delete mode 100644 app/templates/login.html delete mode 100644 app/templates/register.html delete mode 100644 app/templates/ticket.html delete mode 100644 app/templates/tickets.html delete mode 100644 app/templates/users.html rename app/{repositories/dashboards => tickets}/__init__.py (100%) create mode 100644 app/tickets/connections.py create mode 100644 app/tickets/models.py create mode 100644 app/tickets/nodes.py delete mode 100644 app/urls.py rename app/{repositories/tickets => users}/__init__.py (100%) create mode 100644 app/users/connections.py create mode 100644 app/users/models.py create mode 100644 app/users/nodes.py delete mode 100644 app/views/__init__.py delete mode 100644 app/views/board.py delete mode 100644 app/views/boards.py delete mode 100644 app/views/index.py delete mode 100644 app/views/login.py delete mode 100644 app/views/logout.py delete mode 100644 app/views/register.py delete mode 100644 app/views/ticket.py delete mode 100644 app/views/tickets.py delete mode 100644 app/views/users.py delete mode 100644 init_db_scripts/1init.sql create mode 100644 migrations/versions/a22b9033a38d_.py diff --git a/app/__init__.py b/app/__init__.py index 953448a..004dbaa 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -7,6 +7,7 @@ from app.auth import auth from app.signer import Signer from config import get_config +from flask_graphql import GraphQLView env = os.getenv('ENVIRONMENT', 'local') @@ -14,29 +15,25 @@ db = SQLAlchemy(engine_options={ 'isolation_level': 'REPEATABLE_READ' }) +db.Model.query = db.session.query_property() def create_app(): app = Flask(__name__) 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..d58b7fa --- /dev/null +++ b/app/dashboards/connections.py @@ -0,0 +1,8 @@ +from graphene import relay +from .nodes import Dashboard + + +class DashboardConnection(relay.Connection): + """A Dashboard Connection""" + class Meta: + node = Dashboard diff --git a/app/dashboards/models.py b/app/dashboards/models.py new file mode 100644 index 0000000..569aa7d --- /dev/null +++ b/app/dashboards/models.py @@ -0,0 +1,21 @@ +from sqlalchemy.orm import backref +from app import db +from datetime import datetime + + +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=backref( + "created_dashboards", uselist=True + )) + + def __repr__(self): + return f'' + diff --git a/app/dashboards/nodes.py b/app/dashboards/nodes.py new file mode 100644 index 0000000..941d5cf --- /dev/null +++ b/app/dashboards/nodes.py @@ -0,0 +1,10 @@ +from graphene import relay +from .models import Dashboard as DashboardModel +from graphene_sqlalchemy import SQLAlchemyObjectType + + +class Dashboard(SQLAlchemyObjectType): + """A Dashboard information""" + class Meta: + model = DashboardModel + 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/__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/models/sql_alchemy.py b/app/models/sql_alchemy.py deleted file mode 100644 index 87ae8f7..0000000 --- a/app/models/sql_alchemy.py +++ /dev/null @@ -1,83 +0,0 @@ -from datetime import datetime -from app import db - - -class User(db.Model): - __tablename__ = 'Users' - - id = db.Column(db.Integer, primary_key=True) - name = db.Column(db.String(100), nullable=False) - 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'), - ) - - 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 - ) - - user = db.relationship('User', backref='authorized_dashboards') - dashboard = db.relationship('Dashboard', backref='authorized_users') - - __table_args__ = ( - db.UniqueConstraint('dashboard_id', 'user_id'), - ) - - 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'' - - -class Ticket(db.Model): - __tablename__ = 'Tickets' - - id = db.Column(db.Integer, primary_key=True) - title = 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'), nullable=False) - dashboard_id = db.Column(db.Integer, db.ForeignKey('Dashboards.id'), nullable=False) - assignee = db.Column(db.Integer, db.ForeignKey('Users.id')) - status = db.Column( - db.Enum( - 'open', 'inProgress', 'review', 'test', 'closed', name='ticketProgressStatus' - ), - default='open' - ) - - creator = db.relationship('User', backref='created_tickets', foreign_keys=[creator_id]) - dashboard = db.relationship('Dashboard', backref='tickets', foreign_keys=[dashboard_id]) - - __table_args__ = ( - db.CheckConstraint("title <> ''", name='valid_ticket'), - ) - - def __repr__(self): - return f'' 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/__init__.py b/app/repositories/users/__init__.py deleted file mode 100644 index e69de29..0000000 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..e90e97b --- /dev/null +++ b/app/tickets/connections.py @@ -0,0 +1,8 @@ +from graphene import relay +from .nodes import Ticket + + +class TicketConnection(relay.Connection): + """A Ticket Connection""" + class Meta: + node = Ticket diff --git a/app/tickets/models.py b/app/tickets/models.py new file mode 100644 index 0000000..8b32316 --- /dev/null +++ b/app/tickets/models.py @@ -0,0 +1,33 @@ +from sqlalchemy.orm import backref + +from app import db +from datetime import datetime + + +class Ticket(db.Model): + __tablename__ = 'Tickets' + + id = db.Column(db.Integer, primary_key=True) + title = 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'), nullable=False) + dashboard_id = db.Column(db.Integer, db.ForeignKey('Dashboards.id'), nullable=False) + assignee_id = db.Column(db.Integer, db.ForeignKey('Users.id')) + status = db.Column( + db.Enum( + 'open', 'inProgress', 'review', 'test', 'closed', name='ticketProgressStatus' + ), + default='open' + ) + + creator = db.relationship('User', backref=backref('created_tickets', uselist=True), foreign_keys=[creator_id]) + assignee = db.relationship('User', backref=backref('assigned_tickets', uselist=True), foreign_keys=[assignee_id]) + dashboard = db.relationship('Dashboard', backref=backref('tickets', uselist=True), foreign_keys=[dashboard_id]) + + __table_args__ = ( + db.CheckConstraint("title <> ''", name='valid_ticket'), + ) + + def __repr__(self): + return f'' diff --git a/app/tickets/nodes.py b/app/tickets/nodes.py new file mode 100644 index 0000000..94f28de --- /dev/null +++ b/app/tickets/nodes.py @@ -0,0 +1,10 @@ +from graphene import relay +from .models import Ticket as TicketModel +from graphene_sqlalchemy import SQLAlchemyObjectType + + +class Ticket(SQLAlchemyObjectType): + """A Ticket information""" + class Meta: + model = TicketModel + 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..497f75e --- /dev/null +++ b/app/users/connections.py @@ -0,0 +1,8 @@ +from graphene import relay +from .nodes import User + + +class UserConnection(relay.Connection): + """A User Connection""" + class Meta: + node = User diff --git a/app/users/models.py b/app/users/models.py new file mode 100644 index 0000000..d035b2b --- /dev/null +++ b/app/users/models.py @@ -0,0 +1,17 @@ +from app import db + + +class User(db.Model): + __tablename__ = 'Users' + + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(100), nullable=False) + 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'), + ) + + def __repr__(self): + return f'' diff --git a/app/users/nodes.py b/app/users/nodes.py new file mode 100644 index 0000000..8338b9d --- /dev/null +++ b/app/users/nodes.py @@ -0,0 +1,10 @@ +from graphene import relay +from .models import User as UserModel +from graphene_sqlalchemy import SQLAlchemyObjectType + + +class User(SQLAlchemyObjectType): + """A User information""" + class Meta: + model = UserModel + 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/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..d483372 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,22 +1,31 @@ alembic==1.2.1 +aniso8601==7.0.0 atomicwrites==1.3.0 attrs==19.1.0 Click==7.0 Flask==1.0.2 +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 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) From 28abde21536b6d94d5fd037ddd6b5f7365c7fa33 Mon Sep 17 00:00:00 2001 From: vdruzhinin Date: Fri, 24 Jan 2020 15:54:00 +0200 Subject: [PATCH 2/3] Add Flask CORS; Models refactoring --- app/__init__.py | 6 +- app/dashboards/connections.py | 4 +- app/dashboards/models.py | 21 ----- app/dashboards/nodes.py | 6 +- app/models.py | 69 ++++++++++++++++ app/queries.py | 44 ----------- app/tickets/connections.py | 4 +- app/tickets/models.py | 33 -------- app/tickets/nodes.py | 6 +- app/users/connections.py | 4 +- app/users/models.py | 17 ---- app/users/nodes.py | 6 +- .../08da4446aef6_create_dashboard_table.py | 2 +- requirements.txt | 2 + tests/conftest.py | 36 ++------- .../{repositories => dashboards}/__init__.py | 0 tests/dashboards/test_queries.py | 13 ++++ tests/mocks.py | 16 ---- tests/repositories/tickets/__init__.py | 0 tests/repositories/tickets/test_raw_sql.py | 78 ------------------- tests/test_endpoints.py | 44 ----------- 21 files changed, 110 insertions(+), 301 deletions(-) delete mode 100644 app/dashboards/models.py create mode 100644 app/models.py delete mode 100644 app/queries.py delete mode 100644 app/tickets/models.py delete mode 100644 app/users/models.py rename tests/{repositories => dashboards}/__init__.py (100%) create mode 100644 tests/dashboards/test_queries.py delete mode 100644 tests/mocks.py delete mode 100644 tests/repositories/tickets/__init__.py delete mode 100644 tests/repositories/tickets/test_raw_sql.py delete mode 100644 tests/test_endpoints.py diff --git a/app/__init__.py b/app/__init__.py index 004dbaa..b369981 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -8,18 +8,18 @@ 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) db.init_app(app) Migrate(app, db) diff --git a/app/dashboards/connections.py b/app/dashboards/connections.py index d58b7fa..e4b5623 100644 --- a/app/dashboards/connections.py +++ b/app/dashboards/connections.py @@ -1,8 +1,8 @@ from graphene import relay -from .nodes import Dashboard +from .nodes import DashboardNode class DashboardConnection(relay.Connection): """A Dashboard Connection""" class Meta: - node = Dashboard + node = DashboardNode diff --git a/app/dashboards/models.py b/app/dashboards/models.py deleted file mode 100644 index 569aa7d..0000000 --- a/app/dashboards/models.py +++ /dev/null @@ -1,21 +0,0 @@ -from sqlalchemy.orm import backref -from app import db -from datetime import datetime - - -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=backref( - "created_dashboards", uselist=True - )) - - def __repr__(self): - return f'' - diff --git a/app/dashboards/nodes.py b/app/dashboards/nodes.py index 941d5cf..b106f1e 100644 --- a/app/dashboards/nodes.py +++ b/app/dashboards/nodes.py @@ -1,10 +1,10 @@ from graphene import relay -from .models import Dashboard as DashboardModel +from app.models import Dashboard from graphene_sqlalchemy import SQLAlchemyObjectType -class Dashboard(SQLAlchemyObjectType): +class DashboardNode(SQLAlchemyObjectType): """A Dashboard information""" class Meta: - model = DashboardModel + model = Dashboard interfaces = (relay.Node, ) diff --git a/app/models.py b/app/models.py new file mode 100644 index 0000000..0549f6a --- /dev/null +++ b/app/models.py @@ -0,0 +1,69 @@ +from app import db +from datetime import datetime + + +class User(db.Model): + __tablename__ = 'Users' + + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(100), nullable=False) + email = db.Column(db.String(150), nullable=False, unique=True) + password = db.Column(db.String, nullable=False) + + created_dashboards = db.relationship('Dashboard', backref='creator') + created_tickets = db.relationship( + lambda: Ticket, foreign_keys=lambda: Ticket.creator_id, back_populates='creator' + ) + assigned_tickets = db.relationship( + lambda: Ticket, foreign_keys=lambda: Ticket.assignee_id, back_populates='assignee' + ) + + __table_args__ = ( + db.CheckConstraint('name <> "" and email <> "" and password <> ""', name='valid_user'), + ) + + def __repr__(self): + return f'' + + +class Ticket(db.Model): + __tablename__ = 'Tickets' + + id = db.Column(db.Integer, primary_key=True) + title = 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'), nullable=False) + dashboard_id = db.Column(db.Integer, db.ForeignKey('Dashboards.id'), nullable=False) + assignee_id = db.Column(db.Integer, db.ForeignKey('Users.id')) + status = db.Column( + db.Enum( + 'open', 'inProgress', 'review', 'test', 'closed', name='ticketProgressStatus' + ), + default='open' + ) + + 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'), + ) + + 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', lazy="selectin") + + def __repr__(self): + return f'' 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/tickets/connections.py b/app/tickets/connections.py index e90e97b..6d0c3b7 100644 --- a/app/tickets/connections.py +++ b/app/tickets/connections.py @@ -1,8 +1,8 @@ from graphene import relay -from .nodes import Ticket +from .nodes import TicketNode class TicketConnection(relay.Connection): """A Ticket Connection""" class Meta: - node = Ticket + node = TicketNode diff --git a/app/tickets/models.py b/app/tickets/models.py deleted file mode 100644 index 8b32316..0000000 --- a/app/tickets/models.py +++ /dev/null @@ -1,33 +0,0 @@ -from sqlalchemy.orm import backref - -from app import db -from datetime import datetime - - -class Ticket(db.Model): - __tablename__ = 'Tickets' - - id = db.Column(db.Integer, primary_key=True) - title = 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'), nullable=False) - dashboard_id = db.Column(db.Integer, db.ForeignKey('Dashboards.id'), nullable=False) - assignee_id = db.Column(db.Integer, db.ForeignKey('Users.id')) - status = db.Column( - db.Enum( - 'open', 'inProgress', 'review', 'test', 'closed', name='ticketProgressStatus' - ), - default='open' - ) - - creator = db.relationship('User', backref=backref('created_tickets', uselist=True), foreign_keys=[creator_id]) - assignee = db.relationship('User', backref=backref('assigned_tickets', uselist=True), foreign_keys=[assignee_id]) - dashboard = db.relationship('Dashboard', backref=backref('tickets', uselist=True), foreign_keys=[dashboard_id]) - - __table_args__ = ( - db.CheckConstraint("title <> ''", name='valid_ticket'), - ) - - def __repr__(self): - return f'' diff --git a/app/tickets/nodes.py b/app/tickets/nodes.py index 94f28de..936c1fc 100644 --- a/app/tickets/nodes.py +++ b/app/tickets/nodes.py @@ -1,10 +1,10 @@ from graphene import relay -from .models import Ticket as TicketModel +from app.models import Ticket from graphene_sqlalchemy import SQLAlchemyObjectType -class Ticket(SQLAlchemyObjectType): +class TicketNode(SQLAlchemyObjectType): """A Ticket information""" class Meta: - model = TicketModel + model = Ticket interfaces = (relay.Node, ) diff --git a/app/users/connections.py b/app/users/connections.py index 497f75e..ac66400 100644 --- a/app/users/connections.py +++ b/app/users/connections.py @@ -1,8 +1,8 @@ from graphene import relay -from .nodes import User +from .nodes import UserNode class UserConnection(relay.Connection): """A User Connection""" class Meta: - node = User + node = UserNode diff --git a/app/users/models.py b/app/users/models.py deleted file mode 100644 index d035b2b..0000000 --- a/app/users/models.py +++ /dev/null @@ -1,17 +0,0 @@ -from app import db - - -class User(db.Model): - __tablename__ = 'Users' - - id = db.Column(db.Integer, primary_key=True) - name = db.Column(db.String(100), nullable=False) - 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'), - ) - - def __repr__(self): - return f'' diff --git a/app/users/nodes.py b/app/users/nodes.py index 8338b9d..737a139 100644 --- a/app/users/nodes.py +++ b/app/users/nodes.py @@ -1,10 +1,10 @@ from graphene import relay -from .models import User as UserModel +from app.models import User from graphene_sqlalchemy import SQLAlchemyObjectType -class User(SQLAlchemyObjectType): +class UserNode(SQLAlchemyObjectType): """A User information""" class Meta: - model = UserModel + model = User interfaces = (relay.Node, ) 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/requirements.txt b/requirements.txt index d483372..7afc657 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,7 @@ 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 @@ -13,6 +14,7 @@ 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 MarkupSafe==1.1.1 mock==3.0.5 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/tests/repositories/__init__.py b/tests/dashboards/__init__.py similarity index 100% rename from tests/repositories/__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/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 0c0b598..0000000 --- a/tests/test_endpoints.py +++ /dev/null @@ -1,44 +0,0 @@ -# import pytest -# from app.queries import SQLQuery -# from copy import copy -# -# -# @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() From 32b3dfcb4880bb9c9e94fe584038e7fa3d3548a2 Mon Sep 17 00:00:00 2001 From: vdruzhinin Date: Mon, 27 Jan 2020 11:59:18 +0200 Subject: [PATCH 3/3] Remove "selectin" loading for tickets relationship --- app/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models.py b/app/models.py index 0549f6a..dd97c89 100644 --- a/app/models.py +++ b/app/models.py @@ -63,7 +63,7 @@ class Dashboard(db.Model): 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', lazy="selectin") + tickets = db.relationship('Ticket', backref='dashboard') def __repr__(self): return f''