diff --git a/README.md b/README.md index 132e101..d985ea9 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,9 @@ List of available configuration options: | ---- | ----------- | | `SQLALCHEMY_DATABASE_URI` | Database connection string. e.g: `postgresql://user:password@localhost/photowall` | | `DEBUG` | Enable/disable debug mode | -| `API_KEYS` | A list of api keys that are used to authenticate to the API +| `API_KEYS` | A list of api keys that are used to authenticate to the API | +| `ESALES_URL` | URL to the eSales server. If its a cloud cluster it has the format `username:password` | +| `ESALES_IMPORT_DIR` | XML-files that should be imported to eSales are temporarily stored in this directory. e.g: `/tmp/esales` ## Authentication Authentication to the API is performed via HTTP Basic Auth. The user should provide her API key as the basic auth username parameter. An example request in cURL looks like this (adding a colon after the username prevents cURL from asking for a password): @@ -42,4 +44,8 @@ API keys should be added to the `API_KEYS` configuration option Starting the application: -`$ python manage.py runserver` \ No newline at end of file +`$ python manage.py runserver` + +Running the eSales import + +`$ python manage.py run_esales_import` diff --git a/api/__init__.py b/api/__init__.py index 64ba28d..8bdb24b 100644 --- a/api/__init__.py +++ b/api/__init__.py @@ -1,5 +1,6 @@ from flask import Flask, jsonify, Response, request from flask_sqlalchemy import SQLAlchemy +from esales.cluster import Cluster app = Flask(__name__) app.config.from_object('config') @@ -13,6 +14,9 @@ app.errorhandler(404)(on_404) # SQLAlchemy db = SQLAlchemy(app) +# eSales +esales = Cluster(app.config.get("ESALES_URL")) + from api.views import designers app.register_blueprint(designers.mod) diff --git a/api/tasks/__init__.py b/api/tasks/__init__.py new file mode 100644 index 0000000..0d97ab5 --- /dev/null +++ b/api/tasks/__init__.py @@ -0,0 +1 @@ +from . import esales diff --git a/api/tasks/esales.py b/api/tasks/esales.py new file mode 100644 index 0000000..b4c9805 --- /dev/null +++ b/api/tasks/esales.py @@ -0,0 +1,27 @@ +# coding=utf-8 +from api import app +from api.models.locale import Locale +from esales.models import Category, CategoryTree +from esales.utils import ImportFile + + +def update_designer(designer): + locales = Locale.query.all() + trees = [] + + category = Category( + "root/designer/{}".format(designer.path), + designer.name, + hidden=1, + path="/designers/{}".format(designer.path) + ) + + importfile = ImportFile() + + for locale in locales: + tree = CategoryTree("categories_{}".format(locale.value)) + tree.add_category(category) + importfile.add_operation("update", tree) + + directory = app.config.get("ESALES_IMPORT_DIR") + importfile.write(directory) diff --git a/api/views/designers.py b/api/views/designers.py index 6a547ae..f6f2aeb 100644 --- a/api/views/designers.py +++ b/api/views/designers.py @@ -1,5 +1,5 @@ from flask import Blueprint, request, jsonify -from api import db +from api import db, tasks from api.models import Designer mod = Blueprint('designers', __name__, url_prefix='/designers') @@ -34,6 +34,7 @@ def update(id): setattr(designer, k, v) db.session.add(designer) db.session.commit() + tasks.esales.update_designer(designer) return jsonify(designer.to_json()) diff --git a/config.sample.py b/config.sample.py index b8a1b98..dff50c9 100644 --- a/config.sample.py +++ b/config.sample.py @@ -1,4 +1,6 @@ SQLALCHEMY_DATABASE_URI = 'postgresql://user:password@host/db_name' SQLALCHEMY_TRACK_MODIFICATIONS = True DEBUG = True -API_KEYS = ['secretkey'] \ No newline at end of file +API_KEYS = ['secretkey'] +ESALES_URL = 'username:password' +ESALES_IMPORT_DIR = '/tmp/esales' \ No newline at end of file diff --git a/api/helpers/__init__.py b/esales/__init__.py similarity index 100% rename from api/helpers/__init__.py rename to esales/__init__.py diff --git a/esales/cluster.py b/esales/cluster.py new file mode 100644 index 0000000..338b0da --- /dev/null +++ b/esales/cluster.py @@ -0,0 +1,52 @@ +# coding=utf-8 +import glob +import os +import sys +import subprocess +import logging + +logger = logging.getLogger("esales") + + +class CommandException(Exception): + pass + + +class Cluster(object): + + def __init__(self, url): + self.url = url + + def run_command(self, command): + """ execute a command against the esales cluster """ + try: + # find the location of the jar file (provided by apptus) that is + # used to execute the commands + jar = os.path.abspath(os.path.dirname( + __file__) + "/command/command.jar") + subprocess.check_output( + "java -jar {0} {1}".format(jar, command).split(), stderr=subprocess.STDOUT) + except subprocess.CalledProcessError as e: + message = "Could not execute command {0}. Error: {1}".format( + command, e.output) + logger.error(message) + raise CommandException(message) + except OSError: + sys.exit("Java not found on this system") + + def import_products_and_categories(self, pattern): + """ imports products and categories into the eSales cluster """ + i = 0 + for f in sorted(glob.iglob(pattern)): + if os.path.isfile(f): + logger.info("Importing {}".format(f)) + try: + self.run_command("import -p {0} {1}".format(self.url, f)) + i += 1 + except CommandException as e: + logger.error("Failed to import {}".format(f)) + finally: + os.remove(f) + + logger.info("Imported {} file(s)".format(i)) + return i diff --git a/esales/command/command.jar b/esales/command/command.jar new file mode 100755 index 0000000..eea6f6d Binary files /dev/null and b/esales/command/command.jar differ diff --git a/esales/command/system/esales_common-3.25.0.jar b/esales/command/system/esales_common-3.25.0.jar new file mode 100755 index 0000000..cf86e80 Binary files /dev/null and b/esales/command/system/esales_common-3.25.0.jar differ diff --git a/esales/command/system/esales_connector_api-3.25.0.jar b/esales/command/system/esales_connector_api-3.25.0.jar new file mode 100755 index 0000000..63680ee Binary files /dev/null and b/esales/command/system/esales_connector_api-3.25.0.jar differ diff --git a/esales/models.py b/esales/models.py new file mode 100644 index 0000000..4705ad5 --- /dev/null +++ b/esales/models.py @@ -0,0 +1,47 @@ +# coding=utf-8 +from xml.etree.cElementTree import Element, SubElement + + +class Category(object): + """ represents an eSales category """ + + def __init__(self, key, display_name=None, **kwargs): + self.key = key + self.display_name = display_name + self.attributes = {} + for k, v in kwargs.items(): + self.attributes[k] = v + + @property + def etree(self): + root = Element("category", key=self.key) + if self.display_name: + SubElement(root, "display_name").text = self.display_name + if self.attributes: + attributes = SubElement(root, "attributes") + for k, v in self.attributes.items(): + s = str(v) + if s: + SubElement(attributes, k).text = s + + return root + + +class CategoryTree(object): + """represents an eSales CategoryTree""" + + def __init__(self, product_attribute): + self.categories = [] + self.product_attribute = product_attribute + + def add_category(self, category): + self.categories.append(category) + + @property + def etree(self): + root = Element("category_tree", + product_attribute=self.product_attribute) + for category in self.categories: + root.append(category.etree) + + return root diff --git a/esales/utils.py b/esales/utils.py new file mode 100644 index 0000000..0481df3 --- /dev/null +++ b/esales/utils.py @@ -0,0 +1,35 @@ +# coding=utf-8 +import os +import uuid +import time +from xml.etree.cElementTree import ElementTree, Element, SubElement, dump + + +class ImportFile(object): + + def __init__(self): + self.operations = [] + + def add_operation(self, operation, model): + assert operation in ["create", "update", "delete"] + self.operations.append((operation, model)) + + @property + def etree(self): + root = Element("operations") + for name, model in self.operations: + operation = SubElement(root, name) + operation.append(model.etree) + return root + + def dump_xml(self): + return dump(self.etree) + + def write(self, directory, filename=None): + if not os.path.exists(directory): + os.makedirs(directory) + if not filename: + filename = "{}.xml".format(time.time()) + xml = ElementTree(self.etree) + xml.write("{}/{}".format(directory, filename), + encoding="UTF-8", xml_declaration=True) diff --git a/manage.py b/manage.py index 29d6733..a8a54a1 100644 --- a/manage.py +++ b/manage.py @@ -1,5 +1,5 @@ from flask.ext.script import Manager, prompt_bool -from api import app, db +from api import app, db, esales from api.models import Designer manager = Manager(app) @@ -26,5 +26,11 @@ def seed_db(): db.session.commit() +@manager.command +def run_esales_import(): + print("Importing products and categories into eSales") + pattern = "{}/*.xml".format(app.config.get("ESALES_IMPORT_DIR")) + esales.import_products_and_categories(pattern) + if __name__ == '__main__': manager.run()