From 3a79d49c15b785b408bff1b93a09c7a6f98a8ff7 Mon Sep 17 00:00:00 2001 From: Shreyas Satish Date: Mon, 12 Jun 2017 14:11:20 +0530 Subject: [PATCH 1/4] adding roles to item collection --- boxoffice/models/item_collection.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/boxoffice/models/item_collection.py b/boxoffice/models/item_collection.py index 2c2b127b..37626842 100644 --- a/boxoffice/models/item_collection.py +++ b/boxoffice/models/item_collection.py @@ -18,3 +18,18 @@ class ItemCollection(BaseScopedNameMixin, db.Model): organization_id = db.Column(db.Integer, db.ForeignKey('organization.id'), nullable=False) organization = db.relationship(Organization, backref=db.backref('item_collections', cascade='all, delete-orphan')) parent = db.synonym('organization') + + __roles__ = { + 'description': { + 'write': {'item_collection_owner'}, + 'read': {'item_collection_owner'} + } + } + + def roles(self, user=None, inherited=None, token=None): + roles = super(ItemCollection, self).roles(user, inherited, token) + if user or token: + roles.add('user') + if user: + roles.add('item_collection_owner') + return roles From 3286379af6134a242a4db782518ed75e57cda77b Mon Sep 17 00:00:00 2001 From: Shreyas Satish Date: Thu, 15 Jun 2017 13:33:42 +0530 Subject: [PATCH 2/4] added initial version of the GraphQL endpoint --- boxoffice/__init__.py | 12 +++++-- boxoffice/models/line_item.py | 6 ++++ boxoffice/models/order.py | 9 ++++- boxoffice/views/__init__.py | 2 +- boxoffice/views/graphql.py | 62 +++++++++++++++++++++++++++++++++++ requirements.txt | 2 ++ 6 files changed, 89 insertions(+), 4 deletions(-) create mode 100644 boxoffice/views/graphql.py diff --git a/boxoffice/__init__.py b/boxoffice/__init__.py index a8e96ce5..5bab82ce 100644 --- a/boxoffice/__init__.py +++ b/boxoffice/__init__.py @@ -8,12 +8,12 @@ from flask_lastuser import Lastuser from flask_lastuser.sqlalchemy import UserManager from flask_admin import Admin +from flask_graphql import GraphQLView import wtforms_json from baseframe import baseframe, assets, Version from ._version import __version__ import coaster.app - app = Flask(__name__, instance_relative_config=True) lastuser = Lastuser() @@ -29,7 +29,7 @@ from . import extapi, views # NOQA from boxoffice.models import db, User, Item, Price, DiscountPolicy, DiscountCoupon, ItemCollection, Organization, Category # noqa from siteadmin import ItemCollectionModelView, ItemModelView, PriceModelView, DiscountPolicyModelView, DiscountCouponModelView, OrganizationModelView, CategoryModelView # noqa - +from boxoffice.views.graphql import schema as graphql_schema # Configure the app coaster.app.init_app(app) @@ -45,6 +45,14 @@ mail.init_app(app) wtforms_json.init() +app.add_url_rule( + '/graphql', + view_func=GraphQLView.as_view( + 'graphql', + schema=graphql_schema, + graphiql=app.debug + ) +) # This is a temporary solution for an admin interface, only # to be used until the native admin interface is ready. diff --git a/boxoffice/models/line_item.py b/boxoffice/models/line_item.py index 4186b943..3ed18c14 100644 --- a/boxoffice/models/line_item.py +++ b/boxoffice/models/line_item.py @@ -22,6 +22,7 @@ class LINE_ITEM_STATUS(LabeledEnum): #: a line item. Eg: a discount no longer applicable on a line item as a result of a cancellation VOID = (3, __("Void")) +LINE_ITEM_STATUS.TRANSACTION = [LINE_ITEM_STATUS.CONFIRMED, LINE_ITEM_STATUS.CANCELLED] LineItemTuple = namedtuple('LineItemTuple', ['item_id', 'id', 'base_amount', 'discount_policy_id', 'discount_coupon_id', 'discounted_amount', 'final_amount']) @@ -326,6 +327,11 @@ def get_confirmed_line_items(self): Order.get_confirmed_line_items = property(get_confirmed_line_items) +def get_transacted_line_items(self): + return LineItem.query.filter(LineItem.order == self, LineItem.status.in_(LINE_ITEM_STATUS.TRANSACTION)) +Order.get_transacted_line_items = get_transacted_line_items + + def get_from_item(cls, item, qty, coupon_codes=[]): """ Returns a list of (discount_policy, discount_coupon) tuples diff --git a/boxoffice/models/order.py b/boxoffice/models/order.py index 722de32c..9d726eed 100644 --- a/boxoffice/models/order.py +++ b/boxoffice/models/order.py @@ -4,7 +4,7 @@ from datetime import datetime from collections import namedtuple from sqlalchemy import sql -from boxoffice.models import db, BaseMixin, User +from boxoffice.models import db, BaseMixin, User, ItemCollection from coaster.utils import LabeledEnum, buid from baseframe import __ @@ -115,6 +115,13 @@ def is_fully_assigned(self): return True +def get_transacted_orders(self): + """Returns a SQLAlchemy query object preset with an item collection's transacted orders""" + return Order.query.filter(Order.item_collection == self, Order.status.in_(ORDER_STATUS.TRANSACTION)) + +ItemCollection.get_transacted_orders = get_transacted_orders + + class OrderSession(BaseMixin, db.Model): """ Records the referrer and utm headers for an order diff --git a/boxoffice/views/__init__.py b/boxoffice/views/__init__.py index 6b8b0f55..787cfc8e 100644 --- a/boxoffice/views/__init__.py +++ b/boxoffice/views/__init__.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- -from . import admin, item_collection, order, participant, login, admin_item_collection, admin_order, admin_discount, admin_report, admin_item +from . import admin, item_collection, order, participant, login, admin_item_collection, admin_order, admin_discount, admin_report, admin_item, graphql diff --git a/boxoffice/views/graphql.py b/boxoffice/views/graphql.py new file mode 100644 index 00000000..97933999 --- /dev/null +++ b/boxoffice/views/graphql.py @@ -0,0 +1,62 @@ +import graphene + +from ..models import Category, ItemCollection, LineItem, Order + + +class LineItemType(graphene.ObjectType): + id = graphene.String() + line_item_seq = graphene.Int() + base_amount = graphene.Float() + discounted_amount = graphene.Float() + final_amount = graphene.Float() + + +class OrderType(graphene.ObjectType): + name = 'Order' + id = graphene.String() + invoice_no = graphene.Int() + buyer_email = graphene.String() + buyer_fullname = graphene.String() + buyer_phone = graphene.String() + line_items = graphene.List(LineItemType) + + def resolve_line_items(self, args, context, info): + return self.get_transacted_line_items() + + +class CategoryType(graphene.ObjectType): + name = 'Category' + id = graphene.Int() + name = graphene.String() + title = graphene.String() + + +class ItemCollectionType(graphene.ObjectType): + name = 'ItemCollection' + id = graphene.String() + name = graphene.String() + title = graphene.String() + categories = graphene.List(CategoryType) + orders = graphene.List(OrderType) + order = graphene.Field(OrderType, id=graphene.String()) + + def resolve_categories(self, args, context, info): + return self.categories + + def resolve_order(self, args, context, info): + return Order.query.get(args.get('id')) + + def resolve_orders(self, args, context, info): + return self.get_transacted_orders() + + +class QueryType(graphene.ObjectType): + name = 'Query' + item_collection = graphene.Field(ItemCollectionType, id=graphene.String()) + + def resolve_item_collection(self, args, context, info): + item_collection_id = args.get('id') + return ItemCollection.query.get(item_collection_id) + + +schema = graphene.Schema(query=QueryType) diff --git a/requirements.txt b/requirements.txt index 8f89e2bd..73082e47 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,3 +21,5 @@ Flask-Admin unicodecsv isoweek Flask-Migrate +graphene[sqlalchemy] +Flask-GraphQL==1.3.0 From 34d368b2fd6b98e8506c646ae30ce721b03d51e4 Mon Sep 17 00:00:00 2001 From: Shreyas Satish Date: Fri, 23 Jun 2017 16:44:20 +0530 Subject: [PATCH 3/4] graphql first version with read-only access and cookie-based auth --- boxoffice/__init__.py | 5 +++-- boxoffice/models/category.py | 17 +++++++++++++++++ boxoffice/models/item_collection.py | 15 +++++++++------ boxoffice/models/line_item.py | 24 ++++++++++++++++++++++-- boxoffice/models/order.py | 19 +++++++++++++++++++ boxoffice/views/graphql.py | 24 +++++++++++++----------- 6 files changed, 83 insertions(+), 21 deletions(-) diff --git a/boxoffice/__init__.py b/boxoffice/__init__.py index 5bab82ce..43d71bd3 100644 --- a/boxoffice/__init__.py +++ b/boxoffice/__init__.py @@ -45,13 +45,14 @@ mail.init_app(app) wtforms_json.init() + app.add_url_rule( '/graphql', - view_func=GraphQLView.as_view( + view_func=lastuser.requires_login(GraphQLView.as_view( 'graphql', schema=graphql_schema, graphiql=app.debug - ) + )) ) # This is a temporary solution for an admin interface, only diff --git a/boxoffice/models/category.py b/boxoffice/models/category.py index 0ea0b13b..839adaae 100644 --- a/boxoffice/models/category.py +++ b/boxoffice/models/category.py @@ -17,3 +17,20 @@ class Category(BaseScopedNameMixin, db.Model): backref=db.backref('categories', cascade='all, delete-orphan', order_by=seq, collection_class=ordering_list('seq', count_from=1))) parent = db.synonym('item_collection') + + __roles__ = { + 'category_owner': { + 'write': {}, + 'read': {'id', 'name', 'title'} + } + } + + def roles_for(self, user=None, token=None): + if not user and not token: + return set() + roles = super(Category, self).roles_for(user, token) + if user or token: + roles.add('user') + if self.item_collection.organization.userid in user.organizations_owned_ids(): + roles.add('category_owner') + return roles diff --git a/boxoffice/models/item_collection.py b/boxoffice/models/item_collection.py index 37626842..91e67d68 100644 --- a/boxoffice/models/item_collection.py +++ b/boxoffice/models/item_collection.py @@ -2,6 +2,7 @@ from ..models import db, BaseScopedNameMixin, Organization, MarkdownColumn + __all__ = ['ItemCollection'] @@ -20,16 +21,18 @@ class ItemCollection(BaseScopedNameMixin, db.Model): parent = db.synonym('organization') __roles__ = { - 'description': { - 'write': {'item_collection_owner'}, - 'read': {'item_collection_owner'} + 'item_collection_owner': { + 'write': {}, + 'read': {'id', 'name', 'title', 'categories'} } } - def roles(self, user=None, inherited=None, token=None): - roles = super(ItemCollection, self).roles(user, inherited, token) + def roles_for(self, user=None, token=None): + if not user and not token: + return set() + roles = super(ItemCollection, self).roles_for(user, token) if user or token: roles.add('user') - if user: + if self.organization.userid in user.organizations_owned_ids(): roles.add('item_collection_owner') return roles diff --git a/boxoffice/models/line_item.py b/boxoffice/models/line_item.py index 3ed18c14..7be6e490 100644 --- a/boxoffice/models/line_item.py +++ b/boxoffice/models/line_item.py @@ -6,10 +6,11 @@ from collections import namedtuple, OrderedDict from sqlalchemy.sql import select, func from sqlalchemy.ext.orderinglist import ordering_list -from isoweek import Week -from boxoffice.models import db, JsonDict, BaseMixin, ItemCollection, Order, Item, DiscountPolicy, DISCOUNT_TYPE, DiscountCoupon, OrderSession from coaster.utils import LabeledEnum, isoweek_datetime, midnight_to_utc +from coaster.roles import set_roles from baseframe import __ +from isoweek import Week +from boxoffice.models import db, JsonDict, BaseMixin, ItemCollection, Order, Item, DiscountPolicy, DISCOUNT_TYPE, DiscountCoupon, OrderSession __all__ = ['LineItem', 'LINE_ITEM_STATUS', 'Assignee', 'LineItemDiscounter'] @@ -93,6 +94,24 @@ class LineItem(BaseMixin, db.Model): ordered_at = db.Column(db.DateTime, nullable=True) cancelled_at = db.Column(db.DateTime, nullable=True) + __roles__ = { + 'order_owner': { + 'write': {}, + 'read': {'id', 'base_amount', 'discounted_amount', 'final_amount', + 'discount_policy_id', 'discounted_coupon_id', 'ordered_at'} + } + } + + def roles_for(self, user=None, token=None): + if not user and not token: + return set() + roles = super(LineItem, self).roles_for(user, token) + if user or token: + roles.add('user') + if self.order.item_collection.organization.userid in user.organizations_owned_ids(): + roles.add('order_owner') + return roles + def permissions(self, user, inherited=None): perms = super(LineItem, self).permissions(user, inherited) if self.order.organization.userid in user.organizations_owned_ids(): @@ -327,6 +346,7 @@ def get_confirmed_line_items(self): Order.get_confirmed_line_items = property(get_confirmed_line_items) +@set_roles(read={'all', 'order_owner'}) def get_transacted_line_items(self): return LineItem.query.filter(LineItem.order == self, LineItem.status.in_(LINE_ITEM_STATUS.TRANSACTION)) Order.get_transacted_line_items = get_transacted_line_items diff --git a/boxoffice/models/order.py b/boxoffice/models/order.py index 9d726eed..6f3630d7 100644 --- a/boxoffice/models/order.py +++ b/boxoffice/models/order.py @@ -6,6 +6,7 @@ from sqlalchemy import sql from boxoffice.models import db, BaseMixin, User, ItemCollection from coaster.utils import LabeledEnum, buid +from coaster.roles import set_roles from baseframe import __ __all__ = ['Order', 'ORDER_STATUS', 'OrderSession'] @@ -64,6 +65,23 @@ class Order(BaseMixin, db.Model): invoice_no = db.Column(db.Integer, nullable=True) + __roles__ = { + 'order_owner': { + 'write': {'id', 'buyer_email', 'buyer_fullname', 'buyer_phone'}, + 'read': {'id', 'buyer_email', 'buyer_fullname', 'buyer_phone'} + } + } + + def roles_for(self, user=None, token=None): + if not user and not token: + return set() + roles = super(Order, self).roles_for(user, token) + if user or token: + roles.add('user') + if self.item_collection.organization.userid in user.organizations_owned_ids(): + roles.add('order_owner') + return roles + def permissions(self, user, inherited=None): perms = super(Order, self).permissions(user, inherited) if self.organization.userid in user.organizations_owned_ids(): @@ -115,6 +133,7 @@ def is_fully_assigned(self): return True +@set_roles(read={'all', 'item_collection_owner'}) def get_transacted_orders(self): """Returns a SQLAlchemy query object preset with an item collection's transacted orders""" return Order.query.filter(Order.item_collection == self, Order.status.in_(ORDER_STATUS.TRANSACTION)) diff --git a/boxoffice/views/graphql.py b/boxoffice/views/graphql.py index 97933999..fd9bde50 100644 --- a/boxoffice/views/graphql.py +++ b/boxoffice/views/graphql.py @@ -1,6 +1,6 @@ +from flask import g import graphene - -from ..models import Category, ItemCollection, LineItem, Order +from ..models import ItemCollection, Order class LineItemType(graphene.ObjectType): @@ -9,10 +9,11 @@ class LineItemType(graphene.ObjectType): base_amount = graphene.Float() discounted_amount = graphene.Float() final_amount = graphene.Float() + discount_coupon_id = graphene.String() + discount_policy_id = graphene.String() class OrderType(graphene.ObjectType): - name = 'Order' id = graphene.String() invoice_no = graphene.Int() buyer_email = graphene.String() @@ -21,18 +22,17 @@ class OrderType(graphene.ObjectType): line_items = graphene.List(LineItemType) def resolve_line_items(self, args, context, info): - return self.get_transacted_line_items() + line_items = self.get('get_transacted_line_items') and self['get_transacted_line_items']().all() + return [line_item.access_for(user=g.user) for line_item in line_items] class CategoryType(graphene.ObjectType): - name = 'Category' id = graphene.Int() name = graphene.String() title = graphene.String() class ItemCollectionType(graphene.ObjectType): - name = 'ItemCollection' id = graphene.String() name = graphene.String() title = graphene.String() @@ -41,13 +41,15 @@ class ItemCollectionType(graphene.ObjectType): order = graphene.Field(OrderType, id=graphene.String()) def resolve_categories(self, args, context, info): - return self.categories + return [category.access_for(user=g.user) for category in self.categories] def resolve_order(self, args, context, info): - return Order.query.get(args.get('id')) + order = Order.query.get(args.get('id')) + return order.access_for(user=g.user) def resolve_orders(self, args, context, info): - return self.get_transacted_orders() + orders = self.get('get_transacted_orders') and self['get_transacted_orders']().all() + return [order.access_for(user=g.user) for order in orders] class QueryType(graphene.ObjectType): @@ -55,8 +57,8 @@ class QueryType(graphene.ObjectType): item_collection = graphene.Field(ItemCollectionType, id=graphene.String()) def resolve_item_collection(self, args, context, info): - item_collection_id = args.get('id') - return ItemCollection.query.get(item_collection_id) + item_collection = ItemCollection.query.get(args.get('id')) + return item_collection.access_for(user=g.user) schema = graphene.Schema(query=QueryType) From ada1a9e365e6d2fb6126dcc3679d13c2ade85740 Mon Sep 17 00:00:00 2001 From: Shreyas Satish Date: Fri, 23 Jun 2017 16:51:13 +0530 Subject: [PATCH 4/4] use empty set for order --- boxoffice/models/order.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/boxoffice/models/order.py b/boxoffice/models/order.py index 6f3630d7..5f2c207b 100644 --- a/boxoffice/models/order.py +++ b/boxoffice/models/order.py @@ -67,7 +67,7 @@ class Order(BaseMixin, db.Model): __roles__ = { 'order_owner': { - 'write': {'id', 'buyer_email', 'buyer_fullname', 'buyer_phone'}, + 'write': {}, 'read': {'id', 'buyer_email', 'buyer_fullname', 'buyer_phone'} } }