├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.rst ├── oscar_vue_api ├── __init__.py ├── admin.py ├── app.py ├── apps.py ├── authentication.py ├── authtoken.py ├── management │ └── commands │ │ └── oav_export.py ├── migrations │ └── __init__.py ├── models.py ├── renderers.py ├── search.py ├── serializers.py ├── services.py ├── signals.py ├── tests.py └── views.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | .coverage 2 | coverage.xml 3 | nosetests.xml 4 | oscar_commerceconnect.egg-info/ 5 | dist/ 6 | logs/ 7 | public/ 8 | db.sqlite3 9 | .ropeproject 10 | .Python 11 | bin/ 12 | include/ 13 | lib/ 14 | *.egg-info/ 15 | *.pyc 16 | version-django-oscar-commerce-connect.txt 17 | sandbox/media/ 18 | sandbox/static/ 19 | docs/build/ 20 | demosite/images 21 | demosite/cache 22 | demosite/*.sqlite -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, 2016, 2017, 2018 Lukkien BV, Tangent Communications PLC and 2 | individual contributors. All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of Lukkien BV, Tangent Communications PLC nor the names of its 15 | contributors may be used to endorse or promote products derived from this 16 | software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.rst 2 | include LICENSE 3 | include Makefile 4 | recursive-include oscarapi *.py 5 | recursive-include oscarapi *.xml 6 | recursive-include sandbox *.py 7 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | =================================== 2 | Django Oscar API for Vue-Storefront 3 | =================================== 4 | 5 | This package provides a RESTful API for `django-oscar `_ adapted to `vue-storefront `_. 6 | 7 | Usage 8 | ===== 9 | 10 | Still under development, no release yet. 11 | 12 | 13 | Notes 14 | ===== 15 | 16 | `Documentation `_ we are following for the api. 17 | 18 | `Better definition of required fields `_ 19 | 20 | `Data Definitions `_ 21 | 22 | Developing 23 | ========== 24 | 25 | 1. To start using ``git clone`` then cd into directory and run ``pip install -e .`` 26 | 27 | 2. Add ``oscar_vue_api`` to INSTALLED_APPS: 28 | :: 29 | INSTALLED_APPS = [ 30 | .... 31 | 'rest_framework', 32 | 'corsheaders', 33 | 'oscar_vue_api', 34 | ] 35 | MIDDLEWARE = ( 36 | 'corsheaders.middleware.CorsMiddleware', 37 | #... 38 | ) 39 | CORS_ORIGIN_ALLOW_ALL = True 40 | REST_FRAMEWORK = { 41 | 'DEFAULT_AUTHENTICATION_CLASSES': ( 42 | .... 43 | 'oscar_vue_api.authentication.TokenAuthSupportQueryString', 44 | ) 45 | } 46 | 47 | 3. Import the api urls to your projects ``urls.py`` file: 48 | :: 49 | from oscar_vue_api.app import application as api 50 | 51 | urlpatterns = [ 52 | .... 53 | url(r'^vsbridge/', api.urls), 54 | ] 55 | 56 | 57 | Notes 58 | ===== 59 | 60 | Export exsisting categories and products to elasticsearch: ``./manage.py oav_export`` 61 | 62 | Delete all ElasticSearch entries ``curl -X DELETE 'http://localhost:9200/_all'`` 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /oscar_vue_api/__init__.py: -------------------------------------------------------------------------------- 1 | default_app_config = 'oscar_vue_api.apps.OscarVueApiConfig' 2 | -------------------------------------------------------------------------------- /oscar_vue_api/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /oscar_vue_api/app.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | from oscarapi.app import RESTApiApplication 3 | from oscar_vue_api.authtoken import obtain_auth_token 4 | from . import views 5 | 6 | class MyRESTApiApplication(RESTApiApplication): 7 | 8 | def get_urls(self): 9 | urls = [ 10 | url(r'^user/login', obtain_auth_token), 11 | url(r'^user/me', views.CurrentUserView.as_view()), 12 | url(r'^cart/create', views.CreateBasketView.as_view()), 13 | url(r'^cart/pull', views.PullBasketView.as_view()), 14 | url(r'^cart/update', views.UpdateBasketItemView.as_view()), 15 | url(r'^cart/delete', views.DeleteBasketItemView.as_view()), 16 | url(r'^cart/totals', views.BasketTotalsView.as_view()), 17 | url(r'^cart/shipping-information', views.BasketTotalsView.as_view()), 18 | #url(r'^products/index', views.ProductList.as_view(), name='product-list'), 19 | url(r'^catalog', views.ElasticView.as_view()), 20 | ] 21 | return urls + super(MyRESTApiApplication, self).get_urls() 22 | 23 | application = MyRESTApiApplication() 24 | -------------------------------------------------------------------------------- /oscar_vue_api/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class OscarVueApiConfig(AppConfig): 5 | name = 'oscar_vue_api' 6 | def ready(self): 7 | import oscar_vue_api.signals 8 | -------------------------------------------------------------------------------- /oscar_vue_api/authentication.py: -------------------------------------------------------------------------------- 1 | from rest_framework.authentication import TokenAuthentication 2 | 3 | class TokenAuthSupportQueryString(TokenAuthentication): 4 | """ 5 | Extend the TokenAuthentication class to support querystring authentication 6 | in the form of "http://www.example.com/?auth_token=" 7 | """ 8 | def authenticate(self, request): 9 | # Check if 'token_auth' is in the request query params. 10 | # Give precedence to 'Authorization' header. 11 | if 'token' in request.query_params and \ 12 | 'HTTP_AUTHORIZATION' not in request.META and request.query_params.get('token') != '': 13 | return self.authenticate_credentials(request.query_params.get('token')) 14 | else: 15 | return super(TokenAuthSupportQueryString, self).authenticate(request) 16 | -------------------------------------------------------------------------------- /oscar_vue_api/authtoken.py: -------------------------------------------------------------------------------- 1 | from rest_framework.authtoken.views import ObtainAuthToken 2 | from oscar_vue_api.renderers import CustomJSONRenderer 3 | from rest_framework.authtoken.models import Token 4 | from rest_framework.response import Response 5 | from django.contrib.auth import authenticate 6 | from django.utils.translation import ugettext_lazy as _ 7 | from rest_framework import serializers 8 | 9 | class CustomAuthTokenSerializer(serializers.Serializer): 10 | username = serializers.CharField(label=_("Username")) 11 | password = serializers.CharField( 12 | label=_("Password"), 13 | style={'input_type': 'password'}, 14 | trim_whitespace=False 15 | ) 16 | 17 | def validate(self, attrs): 18 | username = attrs.get('username') 19 | password = attrs.get('password') 20 | 21 | if username and password: 22 | user = authenticate(request=self.context.get('request'), 23 | email=username, password=password) 24 | 25 | # The authenticate call simply returns None for is_active=False 26 | # users. (Assuming the default ModelBackend authentication 27 | # backend.) 28 | if not user: 29 | msg = _('Unable to log in with provided credentials.') 30 | raise serializers.ValidationError(msg, code='authorization') 31 | else: 32 | msg = _('Must include "username" and "password".') 33 | raise serializers.ValidationError(msg, code='authorization') 34 | 35 | attrs['user'] = user 36 | return attrs 37 | 38 | class CustomObtainAuthToken(ObtainAuthToken): 39 | renderer_classes = (CustomJSONRenderer,) 40 | serializer_class = CustomAuthTokenSerializer 41 | def post(self, request, *args, **kwargs): 42 | serializer = self.serializer_class(data=request.data, 43 | context={'request': request}) 44 | serializer.is_valid(raise_exception=True) 45 | user = serializer.validated_data['user'] 46 | token, created = Token.objects.get_or_create(user=user) 47 | return Response(token.key) 48 | 49 | obtain_auth_token = CustomObtainAuthToken.as_view() 50 | 51 | 52 | -------------------------------------------------------------------------------- /oscar_vue_api/management/commands/oav_export.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand, CommandError 2 | from oscar_vue_api import search 3 | 4 | class Command(BaseCommand): 5 | help = "Export products to ElasticSearch" 6 | 7 | def handle(self, *args, **kwargs): 8 | bulk_products = search.bulk_indexing_products() 9 | bulk_categories = search.bulk_indexing_categories() 10 | bulk_taxrules = search.bulk_indexing_taxrules() 11 | self.stdout.write("Just finished indexing") 12 | -------------------------------------------------------------------------------- /oscar_vue_api/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ladrua/django-oscar-api-vue-storefront/07714c5278e5a54ef3f8261597e60526050a5550/oscar_vue_api/migrations/__init__.py -------------------------------------------------------------------------------- /oscar_vue_api/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /oscar_vue_api/renderers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import renderers 2 | from rest_framework import status 3 | 4 | class CustomJSONRenderer(renderers.JSONRenderer): 5 | def render(self, data, accepted_media_type=None, renderer_context=None): 6 | response_data = {} 7 | response_data['code'] = renderer_context['response'].status_code 8 | response_data['result'] = data 9 | response = super(CustomJSONRenderer, self).render(response_data, accepted_media_type, renderer_context) 10 | return response 11 | -------------------------------------------------------------------------------- /oscar_vue_api/search.py: -------------------------------------------------------------------------------- 1 | from elasticsearch_dsl.connections import connections 2 | from elasticsearch_dsl import DocType, Text, Date, Integer, Float, Boolean, Object, Nested, Keyword, Long, InnerDoc 3 | from elasticsearch.helpers import bulk 4 | from elasticsearch import Elasticsearch 5 | from oscar.core.loading import get_model, get_class 6 | 7 | connections.create_connection(hosts=['elastic:changeme@db.local'], timeout=20) 8 | 9 | class TaxRulesIndex(DocType): 10 | id = Integer() 11 | code = Text() 12 | priority = Integer() 13 | position = Integer() 14 | customer_tax_class_ids = Long() 15 | product_tax_class_ids = Long() 16 | tax_rate_ids = Long() 17 | calculate_subtotal = Boolean() 18 | rates = Object( 19 | properties = { 20 | 'id': Integer(), 21 | 'tax_country_id': Text(), 22 | 'tax_region_id': Integer(), 23 | 'tax_postcode': Text(), 24 | 'rate': Integer(), 25 | 'code': Text(), 26 | } 27 | ) 28 | 29 | class Index: 30 | name = 'vue_storefront_catalog' 31 | doc_type = 'taxrule' 32 | 33 | class Meta: 34 | doc_type = 'taxrule' 35 | 36 | def bulk_indexing_taxrules(): 37 | TaxRulesIndex().init() 38 | es = connections.get_connection() 39 | all_taxrules = [1] 40 | bulk(client=es, actions=(obj_indexing_taxrule() for b in all_taxrules )) 41 | 42 | def obj_indexing_taxrule(): 43 | 44 | obj = TaxRulesIndex( 45 | meta={ 46 | 'id': 2, 47 | }, 48 | id = 2, 49 | code = "Norway", 50 | priority = 0, 51 | position = 0, 52 | customer_tax_class_ids = [3], 53 | product_tax_class_ids = [2], 54 | tax_rate_ids = [4], 55 | calculate_subtotal = False, 56 | rates = [{ 57 | 'id': 2, 58 | 'tax_country_id': 'NO', 59 | 'tax_region_id': 0, 60 | 'tax_postcode': '*', 61 | 'rate': 25, 62 | 'code': 'VAT25%', 63 | }] 64 | 65 | ) 66 | obj.save() 67 | return obj.to_dict(include_meta=True, skip_empty=False) 68 | 69 | 70 | class CategoriesIndex(DocType): 71 | id = Integer() 72 | parent_id = Integer() 73 | name = Text() 74 | is_active = Boolean() 75 | position = Integer() 76 | level = Integer() 77 | product_count = Integer() 78 | include_in_menu = Integer() 79 | children_data = Nested(include_in_parent=True) 80 | tsk = Long() 81 | sgn = Text() 82 | 83 | class Index: 84 | name = 'vue_storefront_catalog' 85 | doc_type = 'category' 86 | 87 | class Meta: 88 | doc_type = 'category' 89 | 90 | class InnerCategoriesIndex(InnerDoc): 91 | id = Integer() 92 | parent_id = Integer() 93 | name = Text() 94 | is_active = Boolean() 95 | position = Integer() 96 | level = Integer() 97 | product_count = Integer() 98 | include_in_menu = Integer() 99 | children_data = Nested(include_in_parent=True) 100 | tsk = Long() 101 | sgn = Text() 102 | 103 | class Index: 104 | name = 'vue_storefront_catalog' 105 | doc_type = 'category' 106 | 107 | class Meta: 108 | doc_type = 'category' 109 | 110 | def bulk_indexing_categories(): 111 | CategoriesIndex().init() 112 | es = connections.get_connection() 113 | Category = get_model('catalogue', 'category') 114 | bulk(client=es, actions=(obj_indexing_category(b) for b in Category.get_root_nodes().iterator())) 115 | 116 | def category_subs(category, parent): 117 | depth = category.get_depth() 118 | sub_categories = [] 119 | obj = InnerCategoriesIndex( 120 | id = category.id, 121 | parent_id = parent.id, 122 | name = category.name, 123 | is_active = True, 124 | position = 2, 125 | level = depth + 1, 126 | product_count = 1, 127 | children_data = {}, 128 | tsk = 0, 129 | include_in_menu = 0, 130 | sgn = "", 131 | ) 132 | obj.children_data 133 | return obj.to_dict(skip_empty=False) 134 | 135 | def obj_indexing_category(category): 136 | rootpage = category.get_root() 137 | depth = category.get_depth() 138 | children_data = [] 139 | if category.get_children(): 140 | for child in category.get_children(): 141 | obj_child = category_subs(child, category) 142 | children_data.append(obj_child) 143 | 144 | depth = category.get_depth() 145 | obj = CategoriesIndex( 146 | meta={ 147 | 'id': category.id, 148 | }, 149 | id = category.id, 150 | parent_id = 0, 151 | name = category.name, 152 | is_active = True, 153 | position = 2, 154 | level = depth + 1, 155 | product_count = 1, 156 | children_data = children_data, 157 | tsk = 0, 158 | include_in_menu = 0, 159 | sgn = "", 160 | ) 161 | obj.save(skip_empty=False) 162 | return obj.to_dict(include_meta=True, skip_empty=False) 163 | 164 | 165 | class ProductsIndex(DocType): 166 | 167 | id = Integer() 168 | sku = Keyword() 169 | name = Text() 170 | attribute_set_id = Integer() 171 | price = Integer() 172 | status = Integer() 173 | visibility = Integer() 174 | type_id = Text() 175 | created_at = Date() 176 | updated_at = Date() 177 | extension_attributes = Long() 178 | product_links = Long() 179 | tier_prices= Long() 180 | custom_attributes = Long() 181 | category = Object( 182 | properties = { 183 | 'category_id': Long(), 184 | 185 | 'name': Text(), 186 | } 187 | ) 188 | description = Text() 189 | image = Text() 190 | small_image = Text() 191 | thumbnail = Text() 192 | options_container = Text() 193 | required_options = Integer() 194 | has_options = Integer() 195 | url_key = Text() 196 | tax_class_id = Integer() 197 | children_data = Nested() 198 | 199 | configurable_options = Object() 200 | configurable_children = Object() 201 | 202 | category_ids = Long() 203 | stock = Object(properties={'is_in_stock': Boolean()}) 204 | 205 | special_price = Float() 206 | new = Integer() 207 | sale = Integer() 208 | 209 | special_from_date = Date() 210 | special_to_date = Date() 211 | priceInclTax = Float() 212 | originalPriceInclTax = Float() 213 | originalPrice = Float() 214 | specialPriceInclTax = Float() 215 | sgn = Text() 216 | 217 | class Index: 218 | name = 'vue_storefront_catalog' 219 | doc_type = 'product' 220 | 221 | class Meta: 222 | doc_type = 'product' 223 | 224 | 225 | def bulk_indexing_products(): 226 | ProductsIndex().init() 227 | es = connections.get_connection() 228 | Product = get_model('catalogue', 'product') 229 | bulk(client=es, actions=(obj_indexing_product(b) for b in Product.objects.all().iterator())) 230 | 231 | def obj_indexing_product(product): 232 | Selector = get_class('partner.strategy', 'Selector') 233 | if product.images.first(): 234 | image=product.images.first().original.path 235 | else: 236 | image="" 237 | 238 | all_categories = [] 239 | category_ids = [] 240 | for category in product.categories.all(): 241 | category_mapping = [{ 242 | 'category_id': category.id, 243 | 'name': category.name 244 | }] 245 | #category_ids += str(category.id) 246 | all_categories += category_mapping 247 | category_ids.append(category.id) 248 | strategy = Selector().strategy() 249 | price = strategy.fetch_for_product(product).price 250 | obj = ProductsIndex( 251 | meta={ 252 | 'id': product.id, 253 | }, 254 | id = product.id, 255 | sku=product.upc, 256 | name=product.title, 257 | attribute_set_id=None, 258 | price=price.incl_tax, 259 | priceInclTax=price.incl_tax, 260 | status=1, 261 | visibility=4, 262 | type_id="simple", 263 | created_at=product.date_created, 264 | updated_at=product.date_updated, 265 | extension_attributes=[], 266 | product_links = [], 267 | tier_prices = [], 268 | custom_attributes = None, 269 | category=all_categories, 270 | description=product.description, 271 | image=image, 272 | small_image="", 273 | thumbnail="", 274 | options_container="container2", 275 | required_options=0, 276 | has_options=0, 277 | url_key=product.slug, 278 | tax_class_id=2, 279 | children_data={}, 280 | configurable_options=[], 281 | configurable_children=[], 282 | category_ids=category_ids, 283 | 284 | stock={ 285 | 'is_in_stock': True, 286 | }, 287 | sgn = "", 288 | ) 289 | obj.save() 290 | return obj.to_dict(include_meta=True) 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | -------------------------------------------------------------------------------- /oscar_vue_api/serializers.py: -------------------------------------------------------------------------------- 1 | from oscar.core.loading import get_class 2 | 3 | from rest_framework import serializers 4 | 5 | from oscarapi.serializers import checkout, product 6 | 7 | 8 | Selector = get_class('partner.strategy', 'Selector') 9 | 10 | class TotalSegmentSerializer(serializers.Serializer): 11 | code = serializers.CharField() 12 | title = serializers.CharField() 13 | value = serializers.DecimalField(decimal_places=2, max_digits=12) 14 | 15 | class Meta: 16 | fields = '__all__' 17 | 18 | class FullBasketItemSerializer(serializers.Serializer): 19 | item_id = serializers.IntegerField(source='id') 20 | price = serializers.IntegerField(source='price_incl_tax') 21 | base_price = serializers.IntegerField(source='price_excl_tax') 22 | qty = serializers.IntegerField(source='quantity') 23 | row_total = serializers.IntegerField(source='price_incl_tax') 24 | base_row_total = serializers.IntegerField(source='price_excl_tax') 25 | row_total_with_discount = serializers.IntegerField(source='price_incl_tax') 26 | tax_amount = serializers.IntegerField(default=0) 27 | base_tax_amount = serializers.IntegerField(default=0) 28 | tax_percent = serializers.IntegerField(default=0) 29 | discount_amount = serializers.IntegerField(default=0) 30 | base_discount_amount = serializers.IntegerField(default=0) 31 | discount_percent = serializers.IntegerField(default=0) 32 | options = serializers.ListField(default=None) 33 | wee_tax_applied_amount = serializers.IntegerField(default=0) 34 | wee_tax_applied = serializers.IntegerField(default=0) 35 | name = serializers.CharField(source='product.title') 36 | product_option = serializers.ListField(default=None) 37 | 38 | class Meta: 39 | fields = '__all__' 40 | 41 | 42 | 43 | class FullBasketSerializer(serializers.Serializer): 44 | grand_total = serializers.IntegerField(source='total_incl_tax_excl_discounts') 45 | weee_tax_applied_amount = serializers.IntegerField(default=0) 46 | base_currency_code = serializers.CharField(source='currency') 47 | quote_currency_code = serializers.CharField(source='currency') 48 | items_qty = serializers.IntegerField(source='num_items') 49 | items = FullBasketItemSerializer(many=True, source='lines') 50 | total_segments = serializers.SerializerMethodField() 51 | 52 | def get_total_segments(self, obj): 53 | segments = TotalSegmentSerializer(many=True, data=self.context['total_segments']) 54 | segments.is_valid() 55 | return segments.data 56 | class Meta: 57 | fields = '__all__' 58 | 59 | 60 | class BasketItemSerializer(serializers.Serializer): 61 | sku = serializers.CharField(source='product.upc') 62 | qty = serializers.IntegerField(source='quantity') 63 | item_id = serializers.IntegerField(source='id') 64 | price = serializers.IntegerField(source='price_incl_tax') 65 | name = serializers.CharField(source='product.title') 66 | product_type = serializers.CharField(default='simple') 67 | quote_id = serializers.CharField(source='basket.id') 68 | product_option = serializers.DictField(default={}) 69 | 70 | class Meta: 71 | fields = '__all__' 72 | 73 | class WrapperBasketItemSerializer(serializers.Serializer): 74 | cartItem = serializers.SerializerMethodField() 75 | 76 | def get_cartItem(self, obj): 77 | sub_data = SubSerializer(obj) 78 | return sub_data.data 79 | 80 | class Meta: 81 | fields = '__all__' 82 | 83 | class BasketUpdateResponseSerializer(serializers.Serializer): 84 | item_id = serializers.CharField() 85 | sku = serializers.CharField() 86 | qty = serializers.IntegerField() 87 | name = serializers.CharField() 88 | price = serializers.IntegerField() 89 | product_type = serializers.CharField(default="simple") 90 | quote_id = serializers.IntegerField() 91 | 92 | class Meta: 93 | fields = '__all_' 94 | 95 | 96 | class UserSerializer(serializers.Serializer): 97 | 98 | group_id = serializers.ReadOnlyField(default=1) 99 | created_at = serializers.DateTimeField(source='date_joined') 100 | updated_at = serializers.DateTimeField(source='date_joined') 101 | created_in =serializers.ReadOnlyField(default="Default") 102 | email = serializers.EmailField() 103 | firstname = serializers.CharField(source='first_name') 104 | lastname = serializers.CharField(source='last_name') 105 | 106 | class Meta(): 107 | fields = ( 108 | 'id', 109 | 'group_id', 110 | 'created_at', 111 | 'updated_at', 112 | 'created_in', 113 | 'email', 114 | 'firstname', 115 | 'lastname', 116 | ) 117 | 118 | class MyProductLinkSerializer(product.ProductLinkSerializer): 119 | images = product.ProductImageSerializer(many=True, required=False) 120 | price = serializers.SerializerMethodField() 121 | name = serializers.CharField(source='title') 122 | created_at = serializers.DateTimeField(source='date_created') 123 | updated_at = serializers.DateTimeField(source='date_updated') 124 | has_options = serializers.ReadOnlyField(default=0) 125 | type_id = serializers.ReadOnlyField(default="simple") 126 | 127 | class Meta(product.ProductLinkSerializer.Meta): 128 | fields = ( 129 | 'id', 130 | 'name', 131 | 'images', 132 | 'price', 133 | 'created_at', 134 | 'updated_at', 135 | 'description', 136 | 'sku', 137 | 'has_options', 138 | 'type_id' 139 | ) 140 | 141 | def get_price(self, obj): 142 | request = self.context.get("request") 143 | strategy = Selector().strategy( 144 | request=request, user=request.user) 145 | 146 | ser = checkout.PriceSerializer( 147 | strategy.fetch_for_product(obj).price, 148 | context={'request': request}) 149 | 150 | return float(ser.data['incl_tax']) 151 | -------------------------------------------------------------------------------- /oscar_vue_api/services.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | from rest_framework.response import Response 4 | 5 | def elastic_result(self, request): 6 | 7 | path = request.META['PATH_INFO'].partition("catalog")[2] 8 | fullpath = 'http://db.local:9200' + path 9 | 10 | if request.method == "POST": 11 | requestdata = json.loads(request.body) 12 | r = requests.post(fullpath, json=requestdata) 13 | 14 | if request.method == "GET": 15 | r = requests.get(fullpath) 16 | 17 | items = r.json() 18 | return Response(items) 19 | -------------------------------------------------------------------------------- /oscar_vue_api/signals.py: -------------------------------------------------------------------------------- 1 | from django.db.models.signals import post_save 2 | from django.dispatch import receiver 3 | from oscar.core.loading import get_model 4 | 5 | from .search import obj_indexing_product 6 | 7 | ProductModel = get_model('catalogue', 'product') 8 | 9 | @receiver(post_save, sender=ProductModel) 10 | def index_post(sender, instance, **kwargs): 11 | obj_indexing_product(instance) 12 | -------------------------------------------------------------------------------- /oscar_vue_api/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /oscar_vue_api/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | from oscarapi.views import product, basket 4 | 5 | from .serializers import * 6 | from .services import elastic_result 7 | from .renderers import CustomJSONRenderer 8 | from rest_framework.views import APIView 9 | from rest_framework.response import Response 10 | from oscarapi.basket import operations 11 | from rest_framework.renderers import JSONRenderer 12 | from oscar.core.loading import get_model 13 | from rest_framework import status 14 | 15 | BasketModel = get_model('basket', 'Basket') 16 | ProductModel = get_model('catalogue', 'Product') 17 | LineModel = get_model('basket', 'Line') 18 | 19 | Selector = get_class('partner.strategy', 'Selector') 20 | 21 | selector = Selector() 22 | 23 | #class ProductList(product.ProductList): 24 | # serializer_class = MyProductLinkSerializer 25 | 26 | class CurrentUserView(APIView): 27 | 28 | renderer_classes = (CustomJSONRenderer, ) 29 | 30 | def get(self, request): 31 | serializer = UserSerializer(request.user) 32 | return Response(serializer.data) 33 | 34 | class CreateBasketView(APIView): 35 | """ 36 | Api for retrieving a user's basket id. 37 | GET: 38 | Retrieve your basket id. 39 | """ 40 | 41 | renderer_classes = (CustomJSONRenderer, ) 42 | 43 | def post(self, request, format=None): 44 | basket = operations.get_basket(request) 45 | return Response(basket.id) 46 | 47 | class PullBasketView(APIView): 48 | 49 | renderer_classes = (CustomJSONRenderer, ) 50 | 51 | def get(self, request, format=None): 52 | basket_id = request.query_params.get('cartId') 53 | basket = BasketModel.objects.filter(pk=basket_id).first() 54 | serializer = BasketItemSerializer(data=basket.lines, many=True) 55 | serializer.is_valid() 56 | print(serializer.data) 57 | return Response(serializer.data) 58 | 59 | class UpdateBasketItemView(APIView): 60 | 61 | renderer_classes = (CustomJSONRenderer, ) 62 | 63 | def validate(self, basket, product, quantity, options): 64 | availability = basket.strategy.fetch_for_product( 65 | product).availability 66 | 67 | # check if product is available at all 68 | if not availability.is_available_to_buy: 69 | return False, availability.message 70 | 71 | # check if we can buy this quantity 72 | allowed, message = availability.is_purchase_permitted(quantity) 73 | if not allowed: 74 | return False, message 75 | 76 | # check if there is a limit on amount 77 | allowed, message = basket.is_quantity_allowed(quantity) 78 | if not allowed: 79 | return False, message 80 | return True, None 81 | 82 | def post(self, request, format=None): 83 | 84 | quantity = int(request.data['cartItem']['qty']) 85 | product_sku = request.data['cartItem']['sku'] 86 | quote_id = request.data['cartItem']['quoteId'] 87 | product = ProductModel.objects.filter(upc=product_sku).first() 88 | 89 | if 'item_id' in request.data['cartItem']: 90 | line_id = request.data['cartItem']['item_id'] 91 | current_line = LineModel.objects.filter(pk=line_id).first() 92 | current_line.quantity = quantity 93 | current_line.save() 94 | else: 95 | basket_id = request.query_params.get('cartId') 96 | basket = BasketModel.objects.filter(pk=basket_id).first() 97 | basket._set_strategy(selector.strategy(request=request, user=request.user)) 98 | basket_valid, message = self.validate( 99 | basket, product, int(quantity), options=None) 100 | if not basket_valid: 101 | return Response( 102 | message, 103 | status=status.HTTP_406_NOT_ACCEPTABLE) 104 | line = basket.add_product(product, quantity, options=None) 105 | line_id = line[0].id 106 | 107 | response_item = {} 108 | response_item['item_id'] = line_id 109 | response_item['sku'] = product_sku 110 | response_item['qty'] = quantity 111 | response_item['name'] = product.title 112 | response_item['price'] = 200 113 | response_item['product_type'] = 'simple' 114 | response_item['quote_id'] = quote_id 115 | response = BasketUpdateResponseSerializer(data=response_item) 116 | response.is_valid() 117 | print(response.data) 118 | return Response(response.data) 119 | 120 | class DeleteBasketItemView(APIView): 121 | renderer_classes = (CustomJSONRenderer, ) 122 | 123 | def post(self, request): 124 | line_id = request.data['cartItem']['item_id'] 125 | line = LineModel.objects.filter(pk=line_id).first() 126 | response = line.delete() 127 | return Response(response) 128 | 129 | class BasketTotalsView(APIView): 130 | renderer_classes = (CustomJSONRenderer, ) 131 | 132 | def post(self, request, format=None): 133 | return self.do_it(request) 134 | 135 | def get(self, request, format=None): 136 | return self.do_it(request) 137 | 138 | def do_it(self, request, format=None): 139 | basket_id = request.query_params.get('cartId') 140 | basket = BasketModel.objects.filter(pk=basket_id).first() 141 | basket._set_strategy(selector.strategy(request=request, user=request.user)) 142 | total_segments = [] 143 | total_segments.append({ 'code': 'subtotal', 'title': 'Subtotal', 'value': basket.total_excl_tax}) 144 | total_segments.append({ 'code': 'tax', 'title': 'Tax', 'value': basket.total_tax }) 145 | total_segments.append({ 'code': 'grand_total', 'title': 'Grand Total', 'value': basket.total_incl_tax }) 146 | serializer = FullBasketSerializer(basket, context={'total_segments': total_segments}) 147 | return Response(serializer.data) 148 | 149 | class ElasticView(APIView): 150 | permission_classes=[] 151 | renderer_classes = (JSONRenderer, ) 152 | 153 | def get(self, request, format=None): 154 | _search = elastic_result(self, request) 155 | return _search 156 | pass 157 | 158 | def post(self, request): 159 | _search = elastic_result(self, request) 160 | return _search 161 | pass 162 | 163 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup 2 | 3 | __version__ = "0.1.0b0" 4 | 5 | 6 | setup( 7 | # package name in pypi 8 | name='django-oscar-api-vue-storefront', 9 | # extract version from module. 10 | version=__version__, 11 | description="REST API module for django-oscar", 12 | long_description=open('README.rst').read(), 13 | classifiers=[ 14 | 'Development Status :: 5 - Production/Stable', 15 | 'Environment :: Web Environment', 16 | 'Framework :: Django', 17 | 'Framework :: Django :: 1.11', 18 | 'Framework :: Django :: 2.0', 19 | 'Framework :: Django :: 2.1', 20 | 'Intended Audience :: Developers', 21 | 'License :: OSI Approved :: BSD License', 22 | 'Operating System :: Unix', 23 | 'Programming Language :: Python', 24 | 'Programming Language :: Python :: 2', 25 | 'Programming Language :: Python :: 2.7', 26 | 'Programming Language :: Python :: 3', 27 | 'Programming Language :: Python :: 3.5', 28 | 'Programming Language :: Python :: 3.6', 29 | 'Programming Language :: Python :: 3.7', 30 | 'Topic :: Software Development :: Libraries :: Application Frameworks', 31 | ], 32 | keywords='', 33 | author='Stian Aurdal', 34 | author_email='stian@aurdal.io', 35 | url='https://github.com/ladrua/django-oscar-api-vue-storefront', 36 | license='BSD', 37 | # include all packages in the egg, except the test package. 38 | packages=find_packages( 39 | exclude=['ez_setup', 'examples', '*tests', '*fixtures', 'sandbox']), 40 | # for avoiding conflict have one namespace for all apc related eggs. 41 | namespace_packages=[], 42 | # include non python files 43 | include_package_data=True, 44 | zip_safe=False, 45 | # specify dependencies 46 | install_requires=[ 47 | 'setuptools', 48 | 'elasticsearch-dsl', 49 | 'django-cors-headers', 50 | 'django-oscar-api' 51 | ], 52 | # mark test target to require extras. 53 | extras_require={ 54 | 'dev': ['coverage', 'mock', 'twine'], 55 | 'docs': ['sphinx', 'sphinx_rtd_theme'], 56 | }, 57 | ) 58 | --------------------------------------------------------------------------------