]> git.parisson.com Git - diggersdigest.git/commitdiff
get first PayPal WPS payment method (not fully tested yet)
authorGuillaume Pellerin <guillaume.pellerin@ircam.fr>
Thu, 24 Sep 2015 02:06:22 +0000 (04:06 +0200)
committerGuillaume Pellerin <guillaume.pellerin@ircam.fr>
Thu, 24 Sep 2015 02:06:22 +0000 (04:06 +0200)
13 files changed:
.gitignore
Dockerfile
diggersdigest/deploy/start_app.sh [new file with mode: 0644]
diggersdigest/diggersdigest/local_settings.py
diggersdigest/diggersdigest/settings.py
diggersdigest/diggersdigest/urls.py
diggersdigest/records/models.py
diggersdigest/static/img/favicon.ico
diggersdigest/templates/shop/includes/payment_fields.html
diggersdigest/templates/shop/payment.html
docker-compose.yml
requirements-dev.txt [new file with mode: 0644]
requirements.txt

index 775ff5e7241054b865dae6423ea6473ed52351b3..5db03625beb9f437eb8c873d7300a0c6f8231204 100644 (file)
@@ -27,4 +27,4 @@ pip-log.txt
 
 #Volume Directory for docker-compose
 mysql/
-diggersdigest/static/media
\ No newline at end of file
+diggersdigest/static/media
index e05c7cac74a0700448c1e5acb91f6b7cd345c9aa..482e1deddb1c7819bc2868f8a0efb613bda5c501 100644 (file)
@@ -1,17 +1,25 @@
 FROM python:2.7
 
 ENV PYTHONUNBUFFERED 1
+
 RUN apt-get update &&\
     apt-get -y install locales && \
     echo fr_FR.UTF-8 UTF-8 >> /etc/locale.gen &&\
     locale-gen
+
 ENV LANG fr_FR.UTF-8
 ENV LANGUAGE fr_FR:fr
 ENV LC_ALL fr_FR.UTF-8
 
 RUN mkdir /code
+RUN mkdir /opt/src
+
 WORKDIR /code
+
 ADD requirements.txt /code/
 RUN pip install -r requirements.txt
-ADD . /code/
 
+ADD requirements-dev.txt /code/
+RUN pip install -r requirements-dev.txt --src /opt/src
+
+ADD . /code/
diff --git a/diggersdigest/deploy/start_app.sh b/diggersdigest/deploy/start_app.sh
new file mode 100644 (file)
index 0000000..a58577f
--- /dev/null
@@ -0,0 +1,27 @@
+#!/bin/sh
+
+app='diggersdigest'
+
+# paths
+root_dir='/code'
+app_dir=$root_dir'/'$app
+static=$root_dir'/static/'
+sandbox=$app_dir
+manage=$sandbox'/manage.py'
+wsgi=$sandbox'/wsgi.py'
+
+# waiting for other services
+# sh $app_dir/examples/deploy/wait.sh
+
+# django init
+python $manage migrate --noinput
+python $manage collectstatic --noinput
+
+# static files auto update
+#watchmedo shell-command --patterns="*.js;*.css" --recursive \
+#    --command='python '$manage' collectstatic --noinput' $static &
+
+# app start
+#uwsgi --socket :8000 --wsgi-file $wsgi --chdir $sandbox --master --processes 4 --threads 2 --py-autoreload 3
+
+python $manage runserver 0.0.0.0:8000
index 2c50da43010a5ab5292f9b563cb0a178e4b1bf12..188f580394c1371608b44ac04ed9a6430eb4455a 100644 (file)
@@ -16,19 +16,6 @@ DATABASES = {
     }
 }
 
-#######################
-# FILEBROWSER settings
-#######################
-#
-# Max. Upload Size in Bytes:
-# 10MB - 10485760
-# 20MB - 20971520
-# 50MB - 5242880
-# 100MB 104857600
-# 250MB - 214958080
-# 500MB - 429916160
-FILEBROWSER_MAX_UPLOAD_SIZE = 104857600
-
 # EXTENSIONS AND FORMATS
 # Allowed Extensions for File Upload. Lower case is important.
 FILEBROWSER_EXTENSIONS = {
@@ -54,59 +41,6 @@ FILEBROWSER_SELECT_FORMATS = {
     'audio': ['Audio'],
 }
 
-
-#########
-# Shop
-#########
-
-# Add Migration Module path see : https://github.com/stephenmcd/mezzanine/blob/master/docs/model-customization.rst#field-injection-caveats
-MIGRATION_MODULES = {
-    "shop": "diggersdigest.migrations.shop",
-    "blog": "diggersdigest.migrations.blog"
-}
-
-# USE or EXTEND the custom callback-uuid form
-SHOP_CHECKOUT_FORM_CLASS = 'payments.multipayments.forms.base.CallbackUUIDOrderForm'
-
-PRIMARY_PAYMENT_PROCESSOR_IN_USE = True
-
-SECONDARY_PAYMENT_PROCESSORS = (
-# ...
-('paypal', {
-'name' : 'Pay With Pay-Pal',
-'form' : 'payments.multipayments.forms.paypal.PaypalSubmissionForm'
-}),
-# ...
-)
-
-# Currency type.
-PAYPAL_CURRENCY = "EUR"
-
-# Business account email. Sandbox emails look like this.
-PAYPAL_BUSINESS = 'pellerin@parisson.com'
-PAYPAL_RECEIVER_EMAIL = PAYPAL_BUSINESS
-
-# Use this to enable https on return URLs. This is strongly recommended! (Except for sandbox)
-PAYPAL_RETURN_WITH_HTTPS = False
-
-# Function that returns args for `reverse`.
-# URL is sent to PayPal as the for returning to a 'complete' landing page.
-PAYPAL_RETURN_URL = lambda cart, uuid, order_form: ('shop_complete', None, None)
-
-# Function that returns args for `reverse`.
-# URL is sent to PayPal as the URL to callback to for PayPal IPN.
-# Set to None if you do not wish to use IPN.
-PAYPAL_IPN_URL = lambda cart, uuid, order_form: ('my_paypal_ipn', None, {'uuid' : uuid})
-
-# URL the secondary-payment-form is submitted to
-# Dev example
-PAYPAL_SUBMIT_URL = 'https://www.sandbox.paypal.com/cgi-bin/webscr'
-# Prod example
-PAYPAL_SUBMIT_URL = 'https://www.paypal.com/cgi-bin/webscr'
-
-# For real use set to False
-PAYPAL_TEST = True
-
 ###################
 # DEPLOY SETTINGS #
 ###################
index 0b2a82d9e095649a2fb22e61ab1dd627818c1fac..7bbd0211ccf344f71af4c2d3e47115ddb8debdad 100644 (file)
@@ -4,78 +4,6 @@ import os
 from django.utils.translation import ugettext_lazy as _
 
 
-######################
-# CARTRIDGE SETTINGS #
-######################
-
-# The following settings are already defined in cartridge.shop.defaults
-# with default values, but are common enough to be put here, commented
-# out, for conveniently overriding. Please consult the settings
-# documentation for a full list of settings Cartridge implements:
-# http://cartridge.jupo.org/configuration.html#default-settings
-
-# Sequence of available credit card types for payment.
-# SHOP_CARD_TYPES = ("Mastercard", "Visa", "Diners", "Amex")
-
-# Setting to turn on featured images for shop categories. Defaults to False.
-# SHOP_CATEGORY_USE_FEATURED_IMAGE = True
-
-# Set an alternative OrderForm class for the checkout process.
-# SHOP_CHECKOUT_FORM_CLASS = 'cartridge.shop.forms.OrderForm'
-
-# If True, the checkout process is split into separate
-# billing/shipping and payment steps.
-# SHOP_CHECKOUT_STEPS_SPLIT = True
-
-# If True, the checkout process has a final confirmation step before
-# completion.
-# SHOP_CHECKOUT_STEPS_CONFIRMATION = True
-
-# Controls the formatting of monetary values accord to the locale
-# module in the python standard library. If an empty string is
-# used, will fall back to the system's locale.
-#SHOP_CURRENCY_LOCALE = ''
-
-# Dotted package path and name of the function that
-# is called on submit of the billing/shipping checkout step. This
-# is where shipping calculation can be performed and set using the
-# function ``cartridge.shop.utils.set_shipping``.
-# SHOP_HANDLER_BILLING_SHIPPING = \
-#                       "cartridge.shop.checkout.default_billship_handler"
-
-# Dotted package path and name of the function that
-# is called once an order is successful and all of the order
-# object's data has been created. This is where any custom order
-# processing should be implemented.
-# SHOP_HANDLER_ORDER = "cartridge.shop.checkout.default_order_handler"
-
-# Dotted package path and name of the function that
-# is called on submit of the payment checkout step. This is where
-# integration with a payment gateway should be implemented.
-# SHOP_HANDLER_PAYMENT = "cartridge.shop.checkout.default_payment_handler"
-
-# Sequence of value/name pairs for order statuses.
-# SHOP_ORDER_STATUS_CHOICES = (
-#     (1, "Unprocessed"),
-#     (2, "Processed"),
-# )
-
-# Sequence of value/name pairs for types of product options,
-# eg Size, Colour. NOTE: Increasing the number of these will
-# require database migrations!
-#SHOP_OPTION_TYPE_CHOICES = (
-#     (1, "Cover condition"),
-#     (2, "Vinyl condition"),
-# )
-
-SHOP_USE_VARIATIONS = False
-
-# Sequence of indexes from the SHOP_OPTION_TYPE_CHOICES setting that
-# control how the options should be ordered in the admin,
-# SHOP_OPTION_ADMIN_ORDER = (1, 2)
-
-SHOP_USE_RATINGS = False
-
 ######################
 # MEZZANINE SETTINGS #
 ######################
@@ -148,7 +76,7 @@ EXTRA_MODEL_FIELDS = (
         "django.db.models.CharField",
         (),
         {"blank" : False, "max_length" : 36},
-     )
+     ),
      # ...
      #     # Example of adding a field to *all* of Mezzanine's content types:
      #     (
@@ -169,10 +97,133 @@ USE_MODELTRANSLATION = False
 
 SEARCH_MODEL_CHOICES = ('shop.Product',)
 
+
+######################
+# CARTRIDGE SETTINGS #
+######################
+
+# The following settings are already defined in cartridge.shop.defaults
+# with default values, but are common enough to be put here, commented
+# out, for conveniently overriding. Please consult the settings
+# documentation for a full list of settings Cartridge implements:
+# http://cartridge.jupo.org/configuration.html#default-settings
+
+# Sequence of available credit card types for payment.
+# SHOP_CARD_TYPES = ("Mastercard", "Visa", "Diners", "Amex")
+
+# Setting to turn on featured images for shop categories. Defaults to False.
+# SHOP_CATEGORY_USE_FEATURED_IMAGE = True
+
+# Set an alternative OrderForm class for the checkout process.
+# SHOP_CHECKOUT_FORM_CLASS = 'cartridge.shop.forms.OrderForm'
+
+# If True, the checkout process is split into separate
+# billing/shipping and payment steps.
+# SHOP_CHECKOUT_STEPS_SPLIT = True
+
+# If True, the checkout process has a final confirmation step before
+# completion.
+# SHOP_CHECKOUT_STEPS_CONFIRMATION = True
+
+# Controls the formatting of monetary values accord to the locale
+# module in the python standard library. If an empty string is
+# used, will fall back to the system's locale.
+#SHOP_CURRENCY_LOCALE = ''
+
+# Dotted package path and name of the function that
+# is called on submit of the billing/shipping checkout step. This
+# is where shipping calculation can be performed and set using the
+# function ``cartridge.shop.utils.set_shipping``.
+# SHOP_HANDLER_BILLING_SHIPPING = \
+#                       "cartridge.shop.checkout.default_billship_handler"
+
+# Dotted package path and name of the function that
+# is called once an order is successful and all of the order
+# object's data has been created. This is where any custom order
+# processing should be implemented.
+# SHOP_HANDLER_ORDER = "cartridge.shop.checkout.default_order_handler"
+
+# Dotted package path and name of the function that
+# is called on submit of the payment checkout step. This is where
+# integration with a payment gateway should be implemented.
+# SHOP_HANDLER_PAYMENT = "cartridge.shop.checkout.default_payment_handler"
+
+# Sequence of value/name pairs for order statuses.
+# SHOP_ORDER_STATUS_CHOICES = (
+#     (1, "Unprocessed"),
+#     (2, "Processed"),
+# )
+
+# Sequence of value/name pairs for types of product options,
+# eg Size, Colour. NOTE: Increasing the number of these will
+# require database migrations!
+#SHOP_OPTION_TYPE_CHOICES = (
+#     (1, "Cover condition"),
+#     (2, "Vinyl condition"),
+# )
+
+SHOP_USE_VARIATIONS = False
+
+# Sequence of indexes from the SHOP_OPTION_TYPE_CHOICES setting that
+# control how the options should be ordered in the admin,
+# SHOP_OPTION_ADMIN_ORDER = (1, 2)
+
+SHOP_USE_RATINGS = False
+
+# Add Migration Module path see : https://github.com/stephenmcd/mezzanine/blob/master/docs/model-customization.rst#field-injection-caveats
+MIGRATION_MODULES = {
+    "shop": "diggersdigest.migrations.shop",
+    "blog": "diggersdigest.migrations.blog"
+}
+
+# USE or EXTEND the custom callback-uuid form
+SHOP_CHECKOUT_FORM_CLASS = 'payments.multipayments.forms.base.CallbackUUIDOrderForm'
+
+PRIMARY_PAYMENT_PROCESSOR_IN_USE = False
+
+SECONDARY_PAYMENT_PROCESSORS = (
+    ('paypal', {
+    'name' : 'Pay With Pay-Pal',
+    'form' : 'payments.multipayments.forms.paypal.PaypalSubmissionForm'
+    }),
+)
+
+
+# Currency type.
+PAYPAL_CURRENCY = "EUR"
+
+# Business account email. Sandbox emails look like this.
+PAYPAL_BUSINESS = 'pellerin@parisson.com'
+PAYPAL_RECEIVER_EMAIL = PAYPAL_BUSINESS
+
+# Use this to enable https on return URLs. This is strongly recommended! (Except for sandbox)
+PAYPAL_RETURN_WITH_HTTPS = False
+
+# Function that returns args for `reverse`.
+# URL is sent to PayPal as the for returning to a 'complete' landing page.
+PAYPAL_RETURN_URL = lambda cart, uuid, order_form: ('shop_complete', None, None)
+
+# Function that returns args for `reverse`.
+# URL is sent to PayPal as the URL to callback to for PayPal IPN.
+# Set to None if you do not wish to use IPN.
+PAYPAL_IPN_URL = lambda cart, uuid, order_form: ('paypal.standard.ipn.views.ipn', None, {})
+
+
+# URL the secondary-payment-form is submitted to
+# Dev example
+PAYPAL_SUBMIT_URL = 'https://www.sandbox.paypal.com/cgi-bin/webscr'
+# Prod example
+# PAYPAL_SUBMIT_URL = 'https://www.paypal.com/cgi-bin/webscr'
+
+# For real use set to False
+PAYPAL_TEST = True
+
 ########################
 # MAIN DJANGO SETTINGS #
 ########################
 
+SITE_DOMAIN = 'localhost'
+
 # Hosts/domain names that are valid for this site; required if DEBUG is False
 # See https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts
 ALLOWED_HOSTS = []
index 7d5ee682646bdd8472e7fcaa39935461a2ef59ae..6efe25e5579128a2a7dbe70eb40ee8a073557f4a 100644 (file)
@@ -101,6 +101,8 @@ urlpatterns += patterns('',
 
     # ("^%s/" % settings.SITE_PREFIX, include("mezzanine.urls"))
 
+    (r'^paypal-ipn-8c5erc9ye49ia51rn655mi4xs7/', include('paypal.standard.ipn.urls')),
+
 )
 
 # Adds ``STATIC_URL`` to the context of error pages, so that error
index 5fc1c51376270d77e80d883ed070f8807e57bf43..277b13c3c7fb67a2613fabeb1aa7302cf4ed91dc 100644 (file)
@@ -9,14 +9,19 @@ from django.utils.translation import ugettext_lazy as _
 import datetime
 import os
 import fnmatch
+from importlib import import_module
 from diggersdigest import settings
+# from mezzanine.conf import settings
 
 from mezzanine.core.fields import FileField
 from mezzanine.core.models import CONTENT_STATUS_DRAFT, CONTENT_STATUS_PUBLISHED
 from mezzanine.blog.models import BlogPost
 from mezzanine.utils.models import upload_to
 
-from cartridge.shop.models import Product, Category
+from cartridge.shop.models import Product, Category, Cart, Order, ProductVariation, DiscountCode
+from paypal.standard.ipn.signals import payment_was_successful
+from paypal.standard.models import ST_PP_COMPLETED
+from paypal.standard.ipn.signals import valid_ipn_received
 
 
 # Auto-generated Django models with manage.py inspectdb on the old database
@@ -282,3 +287,60 @@ class Podcast(BlogPost):
     #visu1 = models.IntegerField()
     # ordre : on laisse tombé ?
     # published --> status / 0 --> CONTENT_STATUS_DRAFT = 1 / 1 CONTENT_STATUS_PUBLISHED = 2
+
+
+def payment_complete(sender, **kwargs):
+    """Performs the same logic as the code in
+    cartridge.shop.models.Order.complete(), but fetches the session,
+    order, and cart objects from storage, rather than relying on the
+    request object being passed in (which it isn't, since this is
+    triggered on PayPal IPN callback)."""
+
+    ipn_obj = sender
+
+    if ipn_obj.custom and ipn_obj.invoice and ipn_obj.payment_status == ST_PP_COMPLETED:
+        s_key, cart_pk = ipn_obj.custom.split(',')
+        SessionStore = import_module(settings.SESSION_ENGINE) \
+                           .SessionStore
+        session = SessionStore(s_key)
+
+        try:
+            cart = Cart.objects.get(id=cart_pk)
+            try:
+                order = Order.objects.get(
+                    transaction_id=ipn_obj.invoice)
+                for field in order.session_fields:
+                    if field in session:
+                        del session[field]
+                try:
+                    del session["order"]
+                except KeyError:
+                    pass
+
+                # Since we're manually changing session data outside of
+                # a normal request, need to force the session object to
+                # save after modifying its data.
+                session.save()
+
+                for item in cart:
+                    try:
+                        variation = ProductVariation.objects.get(
+                            sku=item.sku)
+                    except ProductVariation.DoesNotExist:
+                        pass
+                    else:
+                        variation.update_stock(item.quantity * -1)
+                        variation.product.actions.purchased()
+
+                code = session.get('discount_code')
+                if code:
+                    DiscountCode.objects.active().filter(code=code) \
+                        .update(uses_remaining=F('uses_remaining') - 1)
+                cart.delete()
+            except Order.DoesNotExist:
+                pass
+        except Cart.DoesNotExist:
+            pass
+
+payment_was_successful.connect(payment_complete)
+valid_ipn_received.connect(payment_complete)
index 8c4212f1a1c93efcc368344229ecbeb0716e6ea8..550d600ee8531545eb17aa6d73e63280c6329287 100644 (file)
Binary files a/diggersdigest/static/img/favicon.ico and b/diggersdigest/static/img/favicon.ico differ
index 238a97d6b2178ef317caee66380dbb32d61fe5d8..51a5349a0f0b685e75907fb54ffb8dea536897b4 100644 (file)
@@ -1,14 +1,16 @@
-{% load i18n mezzanine_tags %}
-<fieldset>
-    <legend>{% trans "Payment Details" %}</legend>
-    {% fields_for form.card_name_field %}
-    {% fields_for form.card_type_field %}
-    {% with form.card_expiry_fields as card_expiry_fields %}
-    <div class="form-group card-expiry-fields{% if card_expiry_fields.errors.card_expiry_year %} error{% endif %}">
-        <label>{% trans "Card Expiry" %}</label>
-        {% fields_for card_expiry_fields %}
-    </div>
-    <div class="clearfix"></div>
-    {% endwith %}
-    {% fields_for form.card_fields %}
-</fieldset>
+{% load i18n mezzanine_tags multipayment_forms %}
+
+{% if PRIMARY_PAYMENT_PROCESSOR_IN_USE %}
+       <fieldset>
+               <legend>{% trans "Payment Details" %}</legend>
+               {% fields_for form.card_name_field %}
+               {% fields_for form.card_type_field %}
+               {% with form.card_expiry_fields as card_expiry_fields %}
+               <div class="control-group card-expiry-fields{% if card_expiry_fields.errors.card_expiry_year %} error{% endif %}">
+                       <label>{% trans "Card Expiry" %}</label>
+                       {% fields_for card_expiry_fields %}
+               </div>
+               {% endwith %}
+               {% fields_for form.card_fields %}
+       </fieldset>
+{% endif %}
index 6be458e63ae8500150c37edc18e10a9ef0810132..8de7e12c6fb0cccef1f06ae96826cbcf6b2b7777 100644 (file)
@@ -1,10 +1,54 @@
 {% extends "shop/checkout.html" %}
-{% load i18n mezzanine_tags %}
+
+{% load i18n multipayment_forms %}
+
+
+<div class="form-actions clearfix">
+        <div class="form-actions-wrap">
+           <input type="submit" value="Next" class="btn btn-large btn-primary">
+        
+        </div>
+    </div>
+
+{% block before-form %}
+       {% if request.cart.has_items %}
+               {% multipayment_forms request form as multipayment_forms %}
+               {% if multipayment_forms %}
+                       <div class="form-actions clearfix">
+                               <div class="form-actions-wrap">
+                                       {% for name,form in multipayment_forms %}<form method="post" action="{{ form.action }}">
+                                               {{ form.as_p }}
+                                               <input type="submit" value="{% trans name %}" class="btn btn-large">
+                                       </form>{% endfor %}
+                               </div>
+                       </div>
+               {% endif %}
+       {% endif %}
+{% endblock %}
 
 {% block fields %}
-{% if request.cart.has_items %}
-{% errors_for form %}
-{% include "shop/includes/payment_fields.html" %}
-{% fields_for form.other_fields %}
-{% endif %}
+       {% if request.cart.has_items %}
+               {% include "shop/includes/payment_fields.html" %}
+               {% if not PRIMARY_PAYMENT_PROCESSOR_IN_USE %}<div style="display:none">{% endif %}
+               {{ form.other_fields.as_ul }}
+               {% if not PRIMARY_PAYMENT_PROCESSOR_IN_USE %}</div>{% endif %}
+       {% endif %}
 {% endblock %}
+
+
+{% block nav-buttons %}
+       {% if request.cart.has_items %}
+               <div class="form-actions clearfix">
+                       <div class="form-actions-wrap">
+                       {% if PRIMARY_PAYMENT_PROCESSOR_IN_USE %}
+                               <input type="submit" class="btn btn-large btn-primary" value="{% trans "Next" %}">
+                       {% endif %}
+                       {% if not CHECKOUT_STEP_FIRST %}
+                               <input type="submit" class="btn btn-large" name="back" value="{% trans "Back" %}">
+                       {% endif %}
+                       </div>
+               </div>
+       {% else %}
+               {{ block.super }}
+       {% endif %}
+{% endblock %}
\ No newline at end of file
index 3759fc0cb64359478ed4ac3601b7cc45a631c33d..02e1949423c3890115159177983cdee9ef0399cd 100644 (file)
@@ -6,7 +6,7 @@ dbdata:
   command: data only container for db
 
 db:
-  image: mysql  
+  image: mysql
   environment:
     - MYSQL_ROOT_PASSWORD=mysecretpassword
     - MYSQL_DATABASE=diggersdigest
@@ -17,10 +17,10 @@ db:
 
 web:
   build: .
-  command: python diggersdigest/manage.py runserver 0.0.0.0:8000
+  command: /bin/sh diggersdigest/deploy/start_app.sh
   volumes:
     - .:/code
   ports:
     - "8000:8000"
   links:
-    - db
\ No newline at end of file
+    - db
diff --git a/requirements-dev.txt b/requirements-dev.txt
new file mode 100644 (file)
index 0000000..e10f4b5
--- /dev/null
@@ -0,0 +1,3 @@
+
+-e git+https://github.com/Parisson/cartridge-payments.git#egg=cartridge-payments
+-e git+https://github.com/Parisson/django-paypal.git#egg=django-paypal-0.2.5
index f9ca26962689bd8df2e626034eac6081e8479f85..fe436d76d942b8984fc443edcde0126860a3bdc9 100644 (file)
@@ -1,8 +1,9 @@
-Django==1.8.3
+--index-url https://pypi.python.org/simple/
+
+setuptools
+Django==1.8.4
 MySQL-python==1.2.5
 cartridge==0.10.0
-cartridge-payments==0.97.0
-django-paypal==0.2.5
 django-uuidfield==0.5.0
 django-newsletter==0.5.2
 parsedatetime==1.5