[
  {
    "path": ".coveragerc",
    "content": "[run]\nomit = price_monitor/migrations/*"
  },
  {
    "path": ".editorconfig",
    "content": "[*.rst]\nindent_style = tab\nindent_size = 4\n\n[*.json]\nindent_style = space\nindent_size = 4\n\n[Makefile]\nindent_style = tab\nindent_size = 4"
  },
  {
    "path": ".gitignore",
    "content": "*.log\n*.pot\n*.pyc\n.coverage\n.cache\n.env\n.idea\n.project\n.pydevproject\n.settings\n.tox\n.vscode\nbuild\ndist\ndjango_amazon_price_monitor.egg-info\nprice_monitor/management/commands/price_monitor_dev.py"
  },
  {
    "path": ".landscape.yaml",
    "content": "doc-warnings: yes\nmax-line-length: 160\nuses:\n  - django\n  - celery\nignore-paths:\n  - docs\n  - hooks\n  - price_monitor/migrations\npylint:\n  disable:\n    - invalid-encoded-data\n    - model-missing-unicode\npython-targets:\n  - 3"
  },
  {
    "path": ".travis.yml",
    "content": "language: python\nsudo: false\nmatrix:\n  include:\n  - python: 3.4\n    env:\n    - TOXENV=py34-django1.8\n  - python: 3.4\n    env:\n    - TOXENV=py34-django1.9\n  - python: 3.4\n    env:\n    - TOXENV=py34-django1.10\n  - python: 3.4\n    env:\n    - TOXENV=py34-django1.11\n  - python: 3.5\n    env:\n    - TOXENV=py35-django1.9\n  - python: 3.5\n    env:\n    - TOXENV=py35-django1.10\n  - python: 3.5\n    env:\n    - TOXENV=py35-django1.11\n  - python: 3.6\n    env:\n    - TOXENV=py36-django1.11\ninstall:\n- pip install codecov tox\nscript:\n- tox\nafter_success:\n- codecov\nnotifications:\n  email: false\ndeploy:\n  provider: pypi\n  user: ponyriders\n  password:\n    secure: n04DQkYdiwg+XLVfJd/O3Jil7kUV1GeLK/gqTgAjOlpXmChhI3+2Xzg8hKoWYmtxXLqZu0zpvepHVi/y5Xz2R1va+eNjnQ9XKzZBt6t40+YaMKpUTZsP0fGocJr0imxuqmOOV8YJ7cZ3r4eX+4aUMMq2tE2j6b37MczTfBw1YmM=\n  distributions: sdist bdist_wheel\n  on:\n    tags: true\n    repo: ponyriders/django-amazon-price-monitor\n    condition: \"$TOXENV = py35-django1.11\"\n"
  },
  {
    "path": "CONTRIBUTING.rst",
    "content": "Contributing\n============\n\nIf you like to lend us a hand, feel free to contribute code to the project. Pick an issue or add what you miss.\nPlease remember that we are only humans and offer this in our spare time.\n\nFork the repo, then clone it:\n\n::\n\n    git clone git@github.com:your-username/django-amazon-price-monitor.git\n\nEnsure the tests run:\n\n    tox\n\nMake your change. Add tests for your change. Make the tests pass:\n\n    tox\n\nPush to your fork and `submit a pull request`_.\n\nAt this point you're waiting on us. We like to at least comment on pull requests\nand we may suggest some changes or improvements or alternatives.\n\nSome things that will increase the chance that your pull request is accepted:\n\n* Write tests.\n* Follow the PEP8 style guide.\n* Write a `good commit message`_.\n\n.. _code of conduct: https://thoughtbot.com/open-source-code-of-conduct\n.. _submit a pull request: https://github.com/ponyriders/django-amazon-price-monitor/compare/\n.. _good commit message: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html"
  },
  {
    "path": "HISTORY.rst",
    "content": "Change Log\n==========\nTBA\n---\n**Maintenance**\n\n- updated the following packages\n\t- ``Celery`` from ``3`` to ``4``, **the setting** ``BROKER_URL`` **is now named** ``CELERY_BROKER_URL``\n\t- ``Django`` up to ``1.11``\n\t- ``CairoSVG`` is not pinned to version below ``2`` any more, with this we drop support for Python ``3.3`` as it is not compatible\n- dropped support for ``Python 3.3``\n\n**Bugfixes:**\n\n- fixed handling of ISBN-13 values in the ISBN-10 return value from Amazon Product Advertising API `#121 <https://github.com/ponyriders/django-amazon-price-monitor/issues/121>`__ (`PR#122 <https://github.com/ponyriders/django-amazon-price-monitor/pull/122>`__)\n\n`0.7 <https://pypi.python.org/pypi/django-amazon-price-monitor/0.7>`__\n----------------------------------------------------------------------\n**Features:**\n\n- footer can now be extended through template block *footer*\n- product addition in frontend improved `#79 <https://github.com/ponyriders/django-amazon-price-monitor/issues/79>`__ (`PR#104 <https://github.com/ponyriders/django-amazon-price-monitor/pull/104>`__)\n- removed ``urlpatterns`` to please Django 1.10 deprecation\n- added docker setup for development (`PR#101 <https://github.com/ponyriders/django-amazon-price-monitor/pull/101>`__)\n- list products with audience rating 18+ in notification mail if region is Germany and product is also 18+ `#92 <https://github.com/ponyriders/django-amazon-price-monitor/issues/92>`__ (`PR#93 <https://github.com/ponyriders/django-amazon-price-monitor/pull/93>`__)\n\n**Bugfixes:**\n\n- now catching parsing errors of returned XML from Amazon API `#96 <https://github.com/ponyriders/django-amazon-price-monitor/issues/96>`__\n- fixed date range of displayed prices in price graph `#90 <https://github.com/ponyriders/django-amazon-price-monitor/issues/90>`__\n- fixed display of old prices of price graph `#97 <https://github.com/ponyriders/django-amazon-price-monitor/issues/97>`__\n- updated to latest ``python-dateutil`` version, somehow refs `#95 <https://github.com/ponyriders/django-amazon-price-monitor/issues/95>`__\n\n`0.6.1 <https://pypi.python.org/pypi/django-amazon-price-monitor/0.6.1>`__\n--------------------------------------------------------------------------\n**Bugfixes:**\n\n- StartupTask fails with exception `#94 <https://github.com/ponyriders/django-amazon-price-monitor/issues/94>`__\n- Tests fail if today is the last day of November `#95 <https://github.com/ponyriders/django-amazon-price-monitor/issues/95>`__\n\n`0.6 <https://pypi.python.org/pypi/django-amazon-price-monitor/0.6>`__\n----------------------------------------------------------------------\n**Features:**\n\n- djangorestframework 3.2 compatibility `#86 <https://github.com/ponyriders/django-amazon-price-monitor/issues/86>`__ (`PR#88 <https://github.com/ponyriders/django-amazon-price-monitor/pull/88>`__)\n\n**Bugfixes:**\n\n- FindProductsToSynchronizeTask is rescheduled twice or more `#89 <https://github.com/ponyriders/django-amazon-price-monitor/issues/89>`__ (`PR#91 <https://github.com/ponyriders/django-amazon-price-monitor/pull/91>`__)\n- Unable to parse 2015-02 to a datetime `#57 <https://github.com/ponyriders/django-amazon-price-monitor/issues/57>`__\n- lots of codestyle\n- minor bugfixes\n\n`0.5 <https://pypi.python.org/pypi/django-amazon-price-monitor/0.5>`__\n----------------------------------------------------------------------\n**Features:**\n\n- Add link to PM frontend in notification email `#76 <https://github.com/ponyriders/django-amazon-price-monitor/issues/76>`__\n- Django 1.9 support (see `pull request #80 <https://github.com/ponyriders/django-amazon-price-monitor/pull/80>`__)\n\n**Bugfixes:**\n\n- FindProductsToSynchronizeTask is not always rescheduled `#61 <https://github.com/ponyriders/django-amazon-price-monitor/issues/61>`__\n- Font files not included in package `#75 <https://github.com/ponyriders/django-amazon-price-monitor/issues/75>`__\n- Identify as Amazon associate `#77 <https://github.com/ponyriders/django-amazon-price-monitor/issues/77>`__\n\n**Pull requests:**\n\n- Ensured that FindProductsToSynchronizeTask will be scheduled `#78 <https://github.com/ponyriders/django-amazon-price-monitor/pull/78>`__ (`dArignac <https://github.com/dArignac>`__)\n- Django 1.9 support `#80 <https://github.com/ponyriders/django-amazon-price-monitor/pull/80>`__ (`dArignac <https://github.com/dArignac>`__)\n\n`0.4 <https://pypi.python.org/pypi/django-amazon-price-monitor/0.4>`__\n----------------------------------------------------------------------\n**Features:**\n\n- Deprecate old frontend `#73 <https://github.com/ponyriders/django-amazon-price-monitor/issues/73>`__\n- Make angular the default frontend `#70 <https://github.com/ponyriders/django-amazon-price-monitor/issues/70>`__\n\n**Bugfixes:**\n\n- Products with the same price over graph timespae have an empty graph `#67 <https://github.com/ponyriders/django-amazon-price-monitor/issues/67>`__\n- Notification of music albums `#33 <https://github.com/ponyriders/django-amazon-price-monitor/issues/33>`__\n- Add artist for audio products `#71 <https://github.com/ponyriders/django-amazon-price-monitor/pull/71>`__\n\n**Pull requests:**\n\n- Remove old frontend `#74 <https://github.com/ponyriders/django-amazon-price-monitor/pull/74>`__ (`dArignac <https://github.com/dArignac>`__)\n- Fix for empty graphs is packaged now #67 `#72 <https://github.com/ponyriders/django-amazon-price-monitor/pull/72>`__ (`mmrose <https://github.com/mmrose>`__)\n\n`0.3b2 <https://pypi.python.org/pypi/django-amazon-price-monitor/0.3b2>`__\n--------------------------------------------------------------------------\n**Features:**\n\n- Prepare for automatic releases `#68 <https://github.com/ponyriders/django-amazon-price-monitor/issues/68>`__\n- Increase performance of Amazon calls `#41 <https://github.com/ponyriders/django-amazon-price-monitor/issues/41>`__\n- Django 1.8 compatibility `#32 <https://github.com/ponyriders/django-amazon-price-monitor/issues/32>`__\n- Data reduction and clean up `#27 <https://github.com/ponyriders/django-amazon-price-monitor/issues/27>`__\n- Limit graphs `#26 <https://github.com/ponyriders/django-amazon-price-monitor/issues/26>`__\n- Show highest and lowest price ever `#25 <https://github.com/ponyriders/django-amazon-price-monitor/issues/25>`__\n- Implement a full-usable frontend`#8 <https://github.com/ponyriders/django-amazon-price-monitor/issues/8>`__\n- Add more tests `#2 <https://github.com/ponyriders/django-amazon-price-monitor/issues/2>`__\n\n**Bugfixes:**\n\n- Graphs empty for some products `#65 <https://github.com/ponyriders/django-amazon-price-monitor/issues/65>`__\n- Don't show other peoples price limits `#63 <https://github.com/ponyriders/django-amazon-price-monitor/issues/63>`__\n- Graphs do not render correct values `#60 <https://github.com/ponyriders/django-amazon-price-monitor/issues/60>`__\n- 'NoneType' object has no attribute 'url' `#59 <https://github.com/ponyriders/django-amazon-price-monitor/issues/59>`__\n- Rename SynchronizeSingleProductTask `#56 <https://github.com/ponyriders/django-amazon-price-monitor/issues/56>`__\n- Sync on product creation not working `#55 <https://github.com/ponyriders/django-amazon-price-monitor/issues/55>`__\n- Clear old products and prices `#47 <https://github.com/ponyriders/django-amazon-price-monitor/issues/47>`__\n- Deleting a product subscription does not remove it from list view `#42 <https://github.com/ponyriders/django-amazon-price-monitor/issues/42>`__\n- Endless synchronization queue `#38 <https://github.com/ponyriders/django-amazon-price-monitor/issues/38>`__\n- Mark unavailable products `#14 <https://github.com/ponyriders/django-amazon-price-monitor/issues/14>`__\n\n**Closed issues:**\n\n- Unpin beautifulsoup4==4.3.2 `#50 <https://github.com/ponyriders/django-amazon-price-monitor/issues/50>`__\n\n**Pull requests:**\n\n- fixed access of unavilable image urls #59 `#66 <https://github.com/ponyriders/django-amazon-price-monitor/pull/66>`__ (`dArignac <https://github.com/dArignac>`__)\n- 63 subscriptions of other users `#64 <https://github.com/ponyriders/django-amazon-price-monitor/pull/64>`__ (`mmrose <https://github.com/mmrose>`__)\n- Mark unavailable products `#62 <https://github.com/ponyriders/django-amazon-price-monitor/pull/62>`__ (`mmrose <https://github.com/mmrose>`__)\n- Sync on product creation not working `#58 <https://github.com/ponyriders/django-amazon-price-monitor/pull/58>`__ (`dArignac <https://github.com/dArignac>`__)\n- Products are now requeried after deletion in list view #42 `#54 <https://github.com/ponyriders/django-amazon-price-monitor/pull/54>`__ (`mmrose <https://github.com/mmrose>`__)\n- Show highest and lowest price (#25) `#53 <https://github.com/ponyriders/django-amazon-price-monitor/pull/53>`__ (`mmrose <https://github.com/mmrose>`__)\n- Now the new FKs are also set during sync #25 `#52 <https://github.com/ponyriders/django-amazon-price-monitor/pull/52>`__ (`mmrose <https://github.com/mmrose>`__)\n- Adding datamigration for new min, max and current price FKs #25 `#51 <https://github.com/ponyriders/django-amazon-price-monitor/pull/51>`__ (`mmrose <https://github.com/mmrose>`__)\n- Performance improvements on product API view `#49 <https://github.com/ponyriders/django-amazon-price-monitor/pull/49>`__ (`mmrose <https://github.com/mmrose>`__)\n- Remove unused data`#48 <https://github.com/ponyriders/django-amazon-price-monitor/pull/48>`__ (`dArignac <https://github.com/dArignac>`__)\n- Amazon query performance increase `#46 <https://github.com/ponyriders/django-amazon-price-monitor/pull/46>`__ (`dArignac <https://github.com/dArignac>`__)\n- Django 1.8 compatibility `#45 <https://github.com/ponyriders/django-amazon-price-monitor/pull/45>`__ (`dArignac <https://github.com/dArignac>`__)\n- Bugfix: Endless queue `#40 <https://github.com/ponyriders/django-amazon-price-monitor/pull/40>`__ (`dArignac <https://github.com/dArignac>`__)\n- waffle.io Badge `#37 <https://github.com/ponyriders/django-amazon-price-monitor/pull/37>`__ (`waffle-iron <https://github.com/waffle-iron>`__)\n\nPre-Releases\n------------\n- unfortunately everything before was not packaged and released nor tracked."
  },
  {
    "path": "LICENSE",
    "content": "Permission is hereby granted, free of charge, to any person obtaining a\ncopy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish, dis-\ntribute, sublicense, and/or sell copies of the Software, and to permit\npersons to whom the Software is furnished to do so, subject to the fol-\nlowing conditions:\n\nThe above copyright notice and this permission notice shall be included\nin all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS\nOR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL-\nITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT\nSHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nWHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\nIN THE SOFTWARE."
  },
  {
    "path": "MANIFEST.in",
    "content": "include README.rst\ninclude HISTORY.rst\nrecursive-include price_monitor *.html *.png *.gif *js *.css *jpg *jpeg *svg *py *eot *ttf *woff\n"
  },
  {
    "path": "Makefile",
    "content": "help:\n\t@echo \"docker-build-base: - builds the base docker image (not necessary normally as image is on docker hub)\"\n\t@echo \"docker-build-web:  - builds the web docker image\"\n\ndocker-build-base:\n\tdocker build -t pricemonitor/base docker/base/\n\ndocker-build-web:\n\tcp setup.py docker/web/django-amazon-price-monitor/setup.py\n\tsed -i 's/readme = .*/readme = \"\"/g' docker/web/django-amazon-price-monitor/setup.py\n\tsed -i 's/history = .*/history = \"\"/g' docker/web/django-amazon-price-monitor/setup.py\n\tdocker build -t pricemonitor/web docker/web/\n\n"
  },
  {
    "path": "README.rst",
    "content": "|Build Status| |codecov.io| |Requirements Status| |Stories in Ready| |Landscape|\n\n.. contents:: Table of Contents\n\ndjango-amazon-price-monitor\n===========================\n\nMonitors prices of Amazon products via Product Advertising API.\n\n**Necessary settings changes for upcoming version 0.8 can be found in the** `history <https://github.com/ponyriders/django-amazon-price-monitor/blob/master/HISTORY.rst>`_ **.**\nUPDATE: 0.8 will never come, see the end of life announcement below.\n\nEND OF LIFE\n-----------\n\n**!!! IMPORTANT !!!**\n\n\nSince January 23rd 2019 the efficiency guidelines for using the Product Advertising API have changed dramatically.\nRequests limits for the API are now calculated based on the revenue generated with the Amazon Associate account the\nProduct Advertising API is connected with.\n\nYou can find the details here: https://docs.aws.amazon.com/AWSECommerceService/latest/DG/TroubleshootingApplications.html#efficiency-guidelines\n\n**For django-amazon-price-monitor this means the END OF LIFE from now on.**\n\nBut why?\n\nWe started the project to solve the issue of saving money with Amazon while having control over your data by self\nhosting this project. To continue maintaining and extending the project we'd need to have enough revenue in our\nassociate account, which we do not have. The result is that our API account used for developing this project is blocked\nand we cannot continue. Also our private little instance of pricemonitor, used by some of our friends and us, will be\nshut down, too.\n\nThe changes in the guidelines make it pretty hard for small projects that do not primarily focus on generating revenue\nwith Amazon to continue. As the `django-amazon-price-monitor` did not get that much love in the last months and years\nfrom us as we focused on other things, the only logic consequence for us is to pull the plug.\n\nThanks to all involved for participating in this journey with us!\n\nBest\n\nAlex + Martin\n\n\nBasic structure\n---------------\n\nThis is a reusable Django app that can be plugged into your project. It\nconsists basically of this parts:\n\n-  Models\n-  Frontend components\n-  Angular Frontend API\n-  Amazon API component\n\nModels\n~~~~~~\n\n-  Product\n\n   -  representation of an Amazon product\n\n-  Price\n\n   -  representation of a price of an Amazon product at a specific time\n\n-  Subscription\n\n   -  subscribe to a product at a specific price with an email\n      notification\n\nFrontend components\n~~~~~~~~~~~~~~~~~~~\n\nThe frontend displays all subscribed products with additional\ninformation and some graphs for price history.\n\nThe features are the following:\n\n-  list products\n-  show product details\n-  show product price graphs\n-  add subscriptions\n-  adjust subscription price value\n-  delete subscriptions\n\nAngular Frontend API\n~~~~~~~~~~~~~~~~~~~~\n\nSimply the API consumed by AngularJS, based on Django REST Framework.\n\nAmazon API component\n~~~~~~~~~~~~~~~~~~~~\n\nFetches product information from Amazon Product Advertising API through\nseveral tasks powered by Celery and weaves the data into the models.\n\nLicense\n-------\n\nThis software is licensed with the MIT license. So feel free to do with\nit whatever you like.\n\nSetup\n-----\n\nPrerequisites\n~~~~~~~~~~~~~\n\n+--------+----------------------+-----------------+------+\n| Python | 3.4                  | 3.5             | 3.6  |\n+========+======================+=================+======+\n| Django | 1.8, 1.9, 1.10, 1.11 | 1.9, 1.10, 1.11 | 1.11 |\n+--------+----------------------+-----------------+------+\n\nFor additional used packages see `setup.py <https://github.com/ponyriders/django-amazon-price-monitor/blob/master/setup.py#L23>`__.\n\nIncluded angular libraries\n~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n-  angular-django-rest-resource (`commit:\n   81d752b363668d674201c09d7a2ce6f418a44f13 <https://github.com/blacklocus/angular-django-rest-resource/tree/81d752b363668d674201c09d7a2ce6f418a44f13>`__)\n\nBasic setup\n~~~~~~~~~~~\n\nAdd the following apps to *INSTALLED\\_APPS*:\n\n::\n\n    INSTALLED_APPS = (\n        ...\n        'price_monitor',\n        'price_monitor.product_advertising_api',\n        'rest_framework',\n    )\n\nThen migrate:\n\n::\n\n    python manage.py migrate\n\nAdjust the settings appropriately, `see next chapter <#settings>`__.\n\nInclude the url configuration.\n\nSetup celery - you'll need the beat and a worker.\n\nSettings\n~~~~~~~~\n\n*The values of the following displayed settings are their default\nvalues. If the value is '...' then there is no default value.*\n\nMust have settings\n^^^^^^^^^^^^^^^^^^\n\nThe following settings are absolutely necessary to the price monitor\nrunning, please set them:\n\nCelery\n''''''\n\nYou need to have a broker and a result backend set.\n\n::\n\n    CELERY_BROKER_URL = ...\n    CELERY_RESULT_BACKEND = ...\n      \n    # some additional settings\n    CELERY_ACCEPT_CONTENT = ['pickle', 'json']\n    CELERY_CHORD_PROPAGATES = True\n\nRest-Framework\n''''''''''''''\n\nWe use Rest-Framework for Angular frontend:\n\n::\n\n    REST_FRAMEWORK = {\n        'PAGINATE_BY': 50,\n        'PAGINATE_BY_PARAM': 'page_size',\n        'MAX_PAGINATE_BY': 100,\n    }\n\nSite URL\n''''''''\nSpecify the base URL under which your site will be available. Defaults to: *http://localhost:8000*\nNecessary for creating links to the site within the notification emails.\n\n::\n\n    # base url to the site\n    PRICE_MONITOR_BASE_URL = 'https://....'\n\nAWS and Product Advertising API credentials\n'''''''''''''''''''''''''''''''''''''''''''\n\n::\n\n    # your Amazon Web Services access key id\n    PRICE_MONITOR_AWS_ACCESS_KEY_ID = '...'\n\n    # your Amazon Web Services secret access key\n    PRICE_MONITOR_AWS_SECRET_ACCESS_KEY = '...'\n\n    # the region endpoint you want to use.\n    # Typically the country you'll run the price monitor in.\n    # possible values: CA, CN, DE, ES, FR, IT, JP, UK, US\n    PRICE_MONITOR_AMAZON_PRODUCT_API_REGION = '...'\n\n    # the assoc tag of the Amazon Product Advertising API\n    PRICE_MONITOR_AMAZON_PRODUCT_API_ASSOC_TAG = '...'\n\nAmazon associates\n'''''''''''''''''\nAs the links to Amazon will be affiliate links with your Amazon associate tag (see above), you have to set your name for the disclaimer\n(see `https://partnernet.amazon.de/gp/associates/agreement <https://partnernet.amazon.de/gp/associates/agreement>`__).\n\n::\n\n    # name of you/your site\n    PRICE_MONITOR_AMAZON_ASSOCIATE_NAME = 'name/sitename'\n    # Amazon site being used, choose from on of the following\n        'Amazon.co.uk'\n        'Local.Amazon.co.uk'\n        'Amazon.de'\n        'de.BuyVIP.com'\n        'Amazon.fr'\n        'Amazon.it'\n        'it.BuyVIP.com'\n        'Amazon.es'\n        'es.BuyVIP.com'\n    PRICE_MONITOR_AMAZON_ASSOCIATE_SITE = '<ONE FROM ABOVE>'\n\n\nImages protocol and domain\n''''''''''''''''''''''''''\n\n::\n\n    # if to use the HTTPS URLs for Amazon images.\n    # if you're running the monitor on SSL, set this to True\n    # INFO:\n    #  Product images are served directly from Amazon.\n    #  This is a restriction when using the Amazon Product Advertising API\n    PRICE_MONITOR_IMAGES_USE_SSL = True\n\n    # domain to use for image serving.\n    # typically analog to the api region following the URL pattern\n    #  https://images-<REGION>.ssl-images-amazon.com\n    PRICE_MONITOR_AMAZON_SSL_IMAGE_DOMAIN = 'https://images-eu.ssl-images-amazon.com'\n\nOptional settings\n^^^^^^^^^^^^^^^^^\n\nThe following settings can be adjusted but come with reasonable default\nvalues.\n\nProduct synchronization\n'''''''''''''''''''''''\n\n::\n\n    # time after which products shall be refreshed\n    # Amazon only allows caching up to 24 hours, so the maximum value is 1440!\n    PRICE_MONITOR_AMAZON_PRODUCT_REFRESH_THRESHOLD_MINUTES = 720  # 12 hours\n\nNotifications\n'''''''''''''\n\nTo be able to send out the notification emails, set up a proper email\nbackend (see `Django\ndocumentation <https://docs.djangoproject.com/en/1.5/topics/email/#topic-email-backends>`__).\n\n::\n\n    # time after which to notify the user again about a price limit hit (in minutes)\n    PRICE_MONITOR_SUBSCRIPTION_RENOTIFICATION_MINUTES = 10080  # 7 days\n\n    # sender address of the notification email\n    PRICE_MONITOR_EMAIL_SENDER = 'noreply@localhost'\n\n    # currency name to use on notifications\n    PRICE_MONITOR_DEFAULT_CURRENCY = 'EUR'\n\n    # subject and body of the notification emails\n    gettext = lambda x: x\n    PRICE_MONITOR_I18N_EMAIL_NOTIFICATION_SUBJECT = gettext(\n        'Price limit for %(product)s reached'\n    )\n    PRICE_MONITOR_I18N_EMAIL_NOTIFICATION_BODY = gettext(\n        'The price limit of %(price_limit)0.2f %(currency)s has been reached for the '\n        'article \"%(product_title)s\" - the current price is %(price)0.2f %(currency)s.'\n        '\\n\\nPlease support our platform by using this '\n        'link for buying: %(link)s\\n\\n\\nRegards,\\nThe Team'\n    )\n\n    # name of the site in notifications\n    PRICE_MONITOR_SITENAME = 'Price Monitor'\n\nCaching\n'''''''\n\n::\n\n    # key of cache (according to project config) to use for graphs\n    # None disables caching.\n    PRICE_MONITOR_GRAPH_CACHE_NAME = None\n\n    # prefix for cache key used for graphs\n    PRICE_MONITOR_GRAPH_CACHE_KEY_PREFIX = 'graph_'\n\nCelery settings\n~~~~~~~~~~~~~~~\nTo be able to run the required Celery tasks, Celery itself has to be set up. Please see the `Celery Documentation <http://docs.celeryproject.org/en/latest/index.html>`__ about how to setup the whole thing. You'll need a broker and a result backend configured.\n\nDevelopment setup with Docker\n-----------------------------\nThe package comes with an easy to use Docker setup - you just need ``docker`` and ``docker-compose``.\n\nStructure\n~~~~~~~~~\nThere are 5 containers:\n\n====== =======================================================================\ndb     Postgres database\n------ -----------------------------------------------------------------------\nredis  Celery broker\n------ -----------------------------------------------------------------------\nweb    a django project containing the ``django-amazon-price-monitor`` package\n------ -----------------------------------------------------------------------\ncelery the celery for the django project\n------ -----------------------------------------------------------------------\ndata   container for mounted volumes\n====== =======================================================================\n\nThe ``web`` and ``celery`` containers are using a docker image being set up under ``docker/web``.\n\nImage: base\n^^^^^^^^^^^\nBasic image with all necessary system packages and pre-installed ``lxml`` and ``psycopg2``.\nThe image can be found on `Docker Hub <https://hub.docker.com/r/pricemonitor/base/>`__.\n\nImage: web\n^^^^^^^^^^\nIt comes with a Django project with login/logout view, that can be found under ``docker/web/project``.\nThe image derives from ``pricemonitor/base`` from above.\n\nThe directory structure within the container is the following (base dir: ``/srv/``):\n::\n\n\troot:/srv tree\n\t├── logs\t\t[log files]\n\t├── media\t\t[media files]\n\t├── project\t\t[the django project]\n\t├── static\t\t[static files]\n\t└── pricemonitor\t[the pricemonitor package]\n\nStarts via the start script ``docker/web/web_run.sh`` that does migrations and the starts the ``runserver``.\n\nImage: celery\n^^^^^^^^^^^^^\nBasically the same as ``web``, but starts the Celery worker with beat.\n\nIf you want to develop anything involving tasks, see the `Usage <_docker-usage-override-settings>`__ section below.\n\nVolumes\n^^^^^^^\nThe containers mount several paths:\n\n+--------------------------+----------------------------------+----------------------------------------------------+\n| Folder in container      | Folder on host                   | Information                                        |\n+==========================+==================================+====================================================+\n| /var/lib/postgresql/data | <PROJECTROOT>/docker/postgres    | * Postgres data directory                          |\n|                          |                                  | * Keeps the DB data even if container is removed   |\n+--------------------------+----------------------------------+----------------------------------------------------+\n| /srv/logs                | <PROJECTROOT>/docker/logs        | Django logs (see project settings)                 |\n+--------------------------+----------------------------------+----------------------------------------------------+\n| /srv/media               | <PROJECTROOT>/docker/media       | Django media files                                 |\n+--------------------------+----------------------------------+----------------------------------------------------+\n| /srv/project             | <PROJECTROOT>/docker/web/project | * the Django project                               |\n|                          |                                  | * is copied on Dockerfile to get it up and running |\n|                          |                                  | * then mounted over (the copy is overwritten)      |\n+--------------------------+----------------------------------+----------------------------------------------------+\n| /srv/pricemonitor        | <PROJECTROOT>                    | * the ``django-amazon-price-monitor`` lib          |\n|                          |                                  | * is copied on Dockerfile to get it up and running |\n|                          |                                  | * then mounted over (the copy is overwritten)      |\n+--------------------------+----------------------------------+----------------------------------------------------+\n\nUsage\n~~~~~\n\n.. _docker-usage-override-settings:\n\nOverride settings\n^^^^^^^^^^^^^^^^^\nTo override some settings as well as to set up the **required AWS settings** you can create a ``docker-compose.override.yml`` and fill with the specific values\n(also see `docker-compose documentation <https://docs.docker.com/compose/extends/>`__).\n\nPlease see or adjust the ``docker\\web\\project\\glue\\settings.py`` for all settings that are read from the environment.\nThey can be overwritten.\n\nA sample ``docker-compose.override.yml`` file could look like this:\n::\n\n\tversion: '3'\n\tservices:\n\t  celery:\n\t    command: /bin/true\n\t    environment:\n\t      PRICE_MONITOR_AWS_ACCESS_KEY_ID: XXX\n\t      PRICE_MONITOR_AWS_SECRET_ACCESS_KEY: XXX\n\t      PRICE_MONITOR_AMAZON_PRODUCT_API_REGION: DE\n\t      PRICE_MONITOR_AMAZON_PRODUCT_API_ASSOC_TAG: XXX\n\t      PRICE_MONITOR_AMAZON_PRODUCT_REFRESH_THRESHOLD_MINUTES: 5\n\t      PRICE_MONITOR_SUBSCRIPTION_RENOTIFICATION_MINUTES: 60\n\nIt will avoid the automatic startup of celery (``command: /bin/true``) and set the required settings for AWS (in fact they are only needed in the celery\ncontainer). You can then manually start the container and execute celery which is quite useful if you develop anything that includes changes in the tasks and\nthus requires the celery to be restarted (execute from the ``docker`` folder!):\n::\n\n\talex@tyrion:~/projects/github/django-amazon-price-monitor/docker$ docker-compose run celery bash\n\tStarting docker_data_1\n\n\n\t# check environment variables\n\n\troot@9d64bbd23e98:/srv/project# env\n\tHOSTNAME=9d64bbd23e98\n\tEMAIL_BACKEND=django.core.mail.backends.filebased.EmailBackend\n\tPOSTGRES_DB=pm_db\n\tTERM=xterm\n\tPYTHONUNBUFFERED=1\n\tPRICE_MONITOR_SUBSCRIPTION_RENOTIFICATION_MINUTES=60\n\tPOSTGRES_PASSWORD=6i2vmzq5C6BuSf5k33A6tmMSHwKKv0Pu\n\tPATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\n\tSECRET_KEY=Vceev7yWMtEQzHaTZX52\n\tPWD=/srv/project\n\tCELERY_BROKER_URL=redis://redis/1\n\tC_FORCE_ROOT='True'\n\tPRICE_MONITOR_AWS_SECRET_ACCESS_KEY=XXX\n\tPOSTGRES_USER=pm_user\n\tSHLVL=1\n\tHOME=/root\n\tPRICE_MONITOR_AMAZON_PRODUCT_REFRESH_THRESHOLD_MINUTES=5\n\tPRICE_MONITOR_AMAZON_PRODUCT_API_REGION=DE\n\tPRICE_MONITOR_AMAZON_PRODUCT_API_ASSOC_TAG=XXX\n\tDEBUG='True'\n\tPRICE_MONITOR_AWS_ACCESS_KEY_ID=XXX\n\t_=/usr/bin/env\n\n\n\t# start celery (worker and beat) (can also execute /srv/celery_run.sh)\n\n\troot@9d64bbd23e98:/srv/project# celery --beat -A glue worker\n\n\t -------------- celery@9d64bbd23e98 v3.1.23 (Cipater)\n\t---- **** -----\n\t--- * ***  * -- Linux-3.16.0-4-amd64-x86_64-with-debian-8.0\n\t-- * - **** ---\n\t- ** ---------- [config]\n\t- ** ---------- .> app:         glue:0x7fc6b5269e10\n\t- ** ---------- .> transport:   redis://redis:6379/1\n\t- ** ---------- .> results:     disabled://\n\t- *** --- * --- .> concurrency: 8 (prefork)\n\t-- ******* ----\n\t--- ***** ----- [queues]\n\t -------------- .> celery           exchange=celery(direct) key=celery\n\n\t[2016-03-20 10:02:26,776: WARNING/MainProcess] celery@9d64bbd23e98 ready.\n\n\nStart/Stop/Build\n^^^^^^^^^^^^^^^^\n::\n\n\tcd docker\n\n\t# start\n\tdocker-compose up -d\n\n\t# stop\n\tdocker-compose stop\n\n\t# inspect\n\tdocker-compose logs -f\n\nA fixture with a Django user ``admin`` and the password ``password`` is loaded automatically.\n\nTo build the images, use the `Makefile` from the root directory.\n\nTemplates\n---------\nAs the fronted is done by Angular, there is only a single template with very limited possibilities to adjust, ``price_monitor/angular_index_view.html``. You\ncan extends the template and adjust the following blocks.\n\nfooter\n~~~~~~\nIs rendered on the very bottom of the page. You have to use Bootstrap compatible markup, e.g.:\n::\n\n\t{% block footer %}\n\t\t<div class=\"row\">\n\t\t\t<div class=\"col-md-12\">Additonal footer</div>\n\t\t</div>\n\t{% endblock %}\n\nManagement Commands\n-------------------\n\nprice\\_monitor\\_batch\\_create\\_products\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nA management command to batch create a number of products by providing\ntheir ASIN:\n\n::\n\n    python manage.py price_monitor_batch_create_products <ASIN1> <ASIN2> <ASIN3>\n\nprice\\_monitor\\_recreate\\_product\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nRecreates a product with the given asin. If product already exists, it\nis deleted. *Only use in development!*\n\n::\n\n    python manage.py price_monitor_recreate_product <ASIN>\n\nprice\\_monitor\\_search\n~~~~~~~~~~~~~~~~~~~~~~\n\nSearches for products at Amazon (not within the database) with the given\nASINs and prints out their details.\n\n::\n\n    python manage.py price_monitor_search <ASIN1> <ASIN2> ...\n\nLoggers\n-------\n\nprice\\_monitor\n~~~~~~~~~~~~~~\n\nThe app uses the logger \"price\\_monitor\" to log all error and info\nmessages that are not included within a dedicated other logger. Please\nsee the `Django logging\ndocumentation <https://docs.djangoproject.com/en/1.6/topics/logging/>`__\nfor how to setup loggers.\n\nprice\\_monitor.product\\_advertising\\_api\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nLogger for everything related to the ProductAdvertisingAPI wrapper class\nthat accesses the Amazon Product Advertising API through bottlenose.\n\nprice\\_monitor.utils\n~~~~~~~~~~~~~~~~~~~~\n\nLogger for the utils module.\n\nTests\n-----\n\nCoverage\n~~~~~~~~\n\n|codecov-graph|\n\nInternals\n---------\n\nModel graph\n~~~~~~~~~~~\n\n.. figure:: https://github.com/ponyriders/django-amazon-price-monitor/raw/master/models.png\n   :alt: Model Graph\n\nProduct advertising api synchronization\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nTask workflow\n^^^^^^^^^^^^^\n\n.. figure:: https://raw.githubusercontent.com/ponyriders/django-amazon-price-monitor/master/docs/price_monitor.product_advertising_api.tasks.png\n    :alt: Image of Product advertising api synchronization workflow\n\nImage of Product advertising api synchronization workflow\n\n.. |Build Status| image:: https://travis-ci.org/ponyriders/django-amazon-price-monitor.svg?branch=master\n    :target: https://travis-ci.org/ponyriders/django-amazon-price-monitor\n.. |codecov.io| image:: http://codecov.io/github/ponyriders/django-amazon-price-monitor/coverage.svg?branch=master\n    :target: http://codecov.io/github/ponyriders/django-amazon-price-monitor?branch=master\n.. |codecov-graph| image:: http://codecov.io/github/ponyriders/django-amazon-price-monitor/branch.svg?branch=master\n.. |Requirements Status| image:: https://requires.io/github/ponyriders/django-amazon-price-monitor/requirements.svg?branch=master\n    :target: https://requires.io/github/ponyriders/django-amazon-price-monitor/requirements/?branch=master\n.. |Stories in Ready| image:: https://badge.waffle.io/ponyriders/django-amazon-price-monitor.png?label=ready&title=Ready\n    :target: https://waffle.io/ponyriders/django-amazon-price-monitor\n.. |Landscape| image:: https://landscape.io/github/ponyriders/django-amazon-price-monitor/master/landscape.svg?style=flat\n    :target: https://landscape.io/github/ponyriders/django-amazon-price-monitor/master\n    :alt: Code Health\n"
  },
  {
    "path": "docker/.gitignore",
    "content": "docker-compose.override.yml\nlogs\nmedia\npostgres\nweb/project/celerybeat-schedule.db"
  },
  {
    "path": "docker/base/Dockerfile",
    "content": "# basic setup\nFROM philcryer/min-jessie\nMAINTAINER Alexander Herrmann <darignac@gmail.com>\n\n# install basic packages: lxml dependencies, python3 and git\n# see recommendation https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices/#apt-get\nRUN apt-get update && apt-get install -y \\\n    git \\\n    libffi-dev \\\n    libjpeg-dev \\\n    libpq-dev \\\n    libxml2-dev \\\n    libxslt1-dev \\\n    postgresql-client-9.4 \\\n    python3-cairo \\\n    python3-minimal \\\n    python3-pip \\\n&& rm -rf /tmp/* /var/tmp/* \\\n&& apt-get clean \\\n&& rm -rf /var/lib/apt/lists/*\n\n# install lxml and psycopg2 - they take the most amount of time compiling\nRUN pip3 install lxml psycopg2 setuptools"
  },
  {
    "path": "docker/compose.env",
    "content": "PYTHONUNBUFFERED=1\nPOSTGRES_USER=pm_user\nPOSTGRES_DB=pm_db\nPOSTGRES_PASSWORD=6i2vmzq5C6BuSf5k33A6tmMSHwKKv0Pu\nDEBUG='True'\nSECRET_KEY=Vceev7yWMtEQzHaTZX52\nEMAIL_BACKEND=django.core.mail.backends.filebased.EmailBackend\nC_FORCE_ROOT='True'\nCELERY_BROKER_URL=redis://redis/1"
  },
  {
    "path": "docker/docker-compose.yml",
    "content": "version: '3'\nservices:\n  # database container\n  db:\n    image: postgres:9\n    env_file: compose.env\n    volumes:\n      - ./postgres:/var/lib/postgresql/data\n  \n  # redis for celery\n  redis:\n    image: redis:3\n\n  # web container with Django project\n  web:\n    build: ./web\n    image: pricemonitor/web\n    depends_on:\n      - db\n    ports:\n      - \"8000:8000\"\n    env_file: compose.env\n    command: /srv/web_run.sh\n    volumes:\n      - ./logs:/srv/logs\n      - ./media:/srv/media\n      - ./web/project:/srv/project\n      - ../:/srv/pricemonitor\n  \n  # celery container\n  celery:\n    build: ./web\n    image: pricemonitor/web\n    depends_on:\n      - redis\n    env_file: compose.env\n    command: /srv/celery_run.sh\n    volumes:\n      - ./web/project:/srv/project\n      - ../:/srv/pricemonitor\n"
  },
  {
    "path": "docker/web/Dockerfile",
    "content": "# basic setup, use base image of treasury project\nFROM pricemonitor/base:latest\nMAINTAINER Alexander Herrmann <darignac@gmail.com>\n\n# django setup, create default folder and volumes\nWORKDIR /srv/\nRUN mkdir static\nVOLUME [\"/srv/media\", \"/srv/logs\", \"/srv/pricemonitor\", \"/srv/project\"]\n\n# copy the django project files\nCOPY project /srv/project\nCOPY web_run.sh /srv/web_run.sh\nCOPY celery_run.sh /srv/celery_run.sh\n\n# install python dependencies for the django project\nRUN pip3 install -r /srv/project/requirements.pip\n\n# copy the treasury package and install - will be mounted later through data container (and thus overwritten with the host files)\nADD django-amazon-price-monitor /srv/pricemonitor\nRUN pip3 install -e /srv/pricemonitor\n\n# ports\nEXPOSE 8000\n\n# entrypoint\nWORKDIR /srv/project"
  },
  {
    "path": "docker/web/celery_run.sh",
    "content": "#!/bin/sh\n# wait for redis\nsleep 5\ncelery --beat -A glue worker"
  },
  {
    "path": "docker/web/django-amazon-price-monitor/price_monitor/__init__.py",
    "content": "\"\"\"Dummy init module for the price_monitor package. It will be overwritten when docker mounts the real package.\"\"\"\n\n\ndef get_version():\n    \"\"\"\n    Returns DEV as version. Just a placeholder while building the web docker image, will be overwritten by mounted docker volume.\n\n    :return: the version identifier\n    \"\"\"\n    return 'DEV'\n"
  },
  {
    "path": "docker/web/django-amazon-price-monitor/setup.py",
    "content": "#!/usr/bin/env python\n\"\"\"Setup file for the django-amazon-price-monitor package.\"\"\"\ntry:\n    from setuptools import setup\nexcept ImportError:\n    from distutils.core import setup\n\n\nreadme = \"\"\nhistory = \"\"\n\nsetup(\n    name='django-amazon-price-monitor',\n    version=__import__('price_monitor').get_version().replace(' ', '-'),\n    description='Monitors prices of Amazon products via Product Advertising API',\n    long_description=readme + '\\n\\n' + history,\n    author='Alexander Herrmann, Martin Mrose',\n    author_email='django-amazon-price-monitor@googlegroups.com',\n    url='https://github.com/ponyriders/django-amazon-price-monitor',\n    packages=[\n        'price_monitor'\n    ],\n    include_package_data=True,\n    install_requires=[\n        # main dependencies\n        'Django>=1.8,<2',\n        # for product advertising api\n        'beautifulsoup4<=4.6',\n        'bottlenose>=0.6.2,<1.2',\n        'celery>=4,<4.1',\n        'python-dateutil>=2.5.1,<2.7',\n        'kombu>=4.1.0,<4.2',\n        # for pm api\n        'djangorestframework>=3.3,<3.7',\n        # for graphs\n        'pygal>=2.0.7,<2.5',\n        'lxml>=4,<4.1',\n        # pygal png output\n        'CairoSVG>=2,<2.1',\n        'tinycss>=0.4,<0.5',\n        'cssselect>=1.0.1,<1.1',\n    ],\n    license='MIT',\n    zip_safe=False,\n)\n"
  },
  {
    "path": "docker/web/project/glue/__init__.py",
    "content": "\"\"\"Glue project init\"\"\"\nfrom __future__ import absolute_import\n\n# This will make sure the app is always imported when\n# Django starts so that shared_task will use this app.\nfrom .celery import app as celery_app  # noqa\n"
  },
  {
    "path": "docker/web/project/glue/celery.py",
    "content": "\"\"\"Celery setup for the glue project.\"\"\"\nfrom __future__ import absolute_import\n\nimport os\n\nfrom celery import Celery\n\nfrom django.conf import settings\n\n\n# set the default Django settings module for the 'celery' program.\nos.environ.setdefault('DJANGO_SETTINGS_MODULE', 'glue.settings')\n\napp = Celery('glue')\n\n# Using a string here means the worker will not have to\n# pickle the object when using Windows.\napp.config_from_object('django.conf:settings', namespace='CELERY')\napp.autodiscover_tasks(lambda: settings.INSTALLED_APPS)\n"
  },
  {
    "path": "docker/web/project/glue/settings.py",
    "content": "\"\"\"\nDjango settings for glue project.\n\nGenerated by 'django-admin startproject' using Django 1.8.2.\n\nFor more information on this file, see\nhttps://docs.djangoproject.com/en/1.8/topics/settings/\n\nFor the full list of settings and their values, see\nhttps://docs.djangoproject.com/en/1.8/ref/settings/\n\"\"\"\n\n# Build paths inside the project like this: os.path.join(BASE_DIR, ...)\nimport os\n\nBASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))\n\n\n# Quick-start development settings - unsuitable for production\n# See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/\n\n# SECURITY WARNING: keep the secret key used in production secret!\nSECRET_KEY = os.environ.get('SECRET_KEY')\n\nDEBUG = os.environ.get('DEBUG', False)\n\nALLOWED_HOSTS = ['127.0.0.1', '0.0.0.0', ]\n\n# Application definition\n\nINSTALLED_APPS = [\n    'django.contrib.admin',\n    'django.contrib.auth',\n    'django.contrib.contenttypes',\n    'django.contrib.sessions',\n    'django.contrib.messages',\n    'django.contrib.staticfiles',\n    'glue_auth',\n    'rest_framework',\n    'price_monitor',\n    'price_monitor.product_advertising_api',\n]\n\nMIDDLEWARE = [\n    'django.middleware.security.SecurityMiddleware',\n    'django.contrib.sessions.middleware.SessionMiddleware',\n    'django.middleware.common.CommonMiddleware',\n    'django.middleware.csrf.CsrfViewMiddleware',\n    'django.contrib.auth.middleware.AuthenticationMiddleware',\n    'django.contrib.auth.middleware.SessionAuthenticationMiddleware',\n    'django.contrib.messages.middleware.MessageMiddleware',\n    'django.middleware.clickjacking.XFrameOptionsMiddleware',\n]\n\nROOT_URLCONF = 'glue.urls'\n\nTEMPLATES = [\n    {\n        'BACKEND': 'django.template.backends.django.DjangoTemplates',\n        'DIRS': [],\n        'APP_DIRS': True,\n        'OPTIONS': {\n            'context_processors': [\n                'django.template.context_processors.debug',\n                'django.template.context_processors.request',\n                'django.contrib.auth.context_processors.auth',\n                'django.contrib.messages.context_processors.messages',\n            ],\n        },\n    },\n]\n\nWSGI_APPLICATION = 'glue.wsgi.application'\n\n\n# Database\n# https://docs.djangoproject.com/en/1.8/ref/settings/#databases\n\nDATABASES = {\n    'default': {\n        'ENGINE': 'django.db.backends.postgresql_psycopg2',\n        'NAME': os.environ.get('POSTGRES_DB'),\n        'USER': os.environ.get('POSTGRES_USER'),\n        'PASSWORD': os.environ.get('POSTGRES_PASSWORD'),\n        'HOST': 'db',\n        'PORT': '5432',\n    }\n}\n\n\n# Internationalization\n# https://docs.djangoproject.com/en/1.8/topics/i18n/\n\nLANGUAGE_CODE = 'en-us'\n\nTIME_ZONE = 'UTC'\n\nUSE_I18N = True\n\nUSE_L10N = True\n\nUSE_TZ = True\n\n\n# Static files (CSS, JavaScript, Images)\n# https://docs.djangoproject.com/en/1.8/howto/static-files/\n\nSTATIC_URL = '/static/'\nSTATIC_ROOT = '/srv/static/'\n\n# Logging\nLOGGING = {\n    'version': 1,\n    'disable_existing_loggers': False,\n    'formatters': {\n        'verbose': {\n            'format': '%(levelname)s %(asctime)s %(filename)s %(lineno)d %(message)s'\n        },\n        'simple': {\n            'format': '%(levelname)s %(message)s'\n        },\n    },\n    'handlers': {\n        'file_error': {\n            'level': 'ERROR',\n            'class': 'logging.FileHandler',\n            'filename': os.path.join(BASE_DIR, '..', 'logs', 'error.log'),\n            'formatter': 'verbose',\n        },\n        'price_monitor': {\n            'level': 'DEBUG',\n            'class': 'logging.FileHandler',\n            'filename': os.path.join(BASE_DIR, '..', 'logs', 'price_monitor.log'),\n            'formatter': 'verbose',\n        },\n        'price_monitor.product_advertising_api': {\n            'level': 'DEBUG',\n            'class': 'logging.FileHandler',\n            'filename': os.path.join(BASE_DIR, '..', 'logs', 'price_monitor.product_advertising_api.log'),\n            'formatter': 'verbose',\n        },\n        'price_monitor.tasks': {\n            'level': 'DEBUG',\n            'class': 'logging.FileHandler',\n            'filename': os.path.join(BASE_DIR, '..', 'logs', 'price_monitor.tasks.log'),\n            'formatter': 'verbose',\n        },\n        'price_monitor.utils': {\n            'level': 'DEBUG',\n            'class': 'logging.FileHandler',\n            'filename': os.path.join(BASE_DIR, '..', 'logs', 'price_monitor.utils.log'),\n            'formatter': 'verbose',\n        },\n        'mail_admins': {\n            'level': 'ERROR',\n            'class': 'django.utils.log.AdminEmailHandler',\n            'include_html': True,\n        },\n    },\n    'loggers': {\n        'django.request': {\n            'handlers': ['file_error', 'mail_admins'],\n            'level': 'ERROR',\n            'propagate': True,\n        },\n        'price_monitor': {\n            'handlers': ['price_monitor'],\n            'level': 'INFO',\n            'propagate': True,\n        },\n        'price_monitor.product_advertising_api': {\n            'handlers': ['price_monitor.product_advertising_api'],\n            'level': 'INFO',\n            'propagate': True,\n        },\n        'price_monitor.tasks': {\n            'handlers': ['price_monitor.tasks'],\n            'level': 'INFO',\n            'propagate': True,\n        },\n        'price_monitor.utils': {\n            'handlers': ['price_monitor.utils'],\n            'level': 'INFO',\n            'propagate': True,\n        },\n    },\n}\n\n# caching\n# CACHES = {\n#     'default': {\n#         'BACKEND': 'redis_cache.RedisCache',\n#         'LOCATION': 'redis',\n#         'OPTIONS': {\n#             'DB': 0,\n#             'PARSER_CLASS': 'redis.connection.HiredisParser',\n#             'CONNECTION_POOL_CLASS': 'redis.BlockingConnectionPool',\n#             'CONNECTION_POOL_CLASS_KWARGS': {\n#                 'max_connections': 50,\n#                 'timeout': 20,\n#             }\n#         },\n#     },\n# }\n# CACHE_MIDDLEWARE_KEY_PREFIX = 'pm_glue'\n\n# glue login\nLOGIN_REDIRECT_URL = '/'\nLOGIN_URL = '/login/'\n\n# E-Mail\nEMAIL_BACKEND = os.environ.get('EMAIL_BACKEND', 'django.core.mail.backends.smtp.EmailBackend')  # smtp is the Django default\nif EMAIL_BACKEND == 'django.core.mail.backends.smtp.EmailBackend':\n    EMAIL_HOST = os.environ.get('EMAIL_HOST')\n    EMAIL_PORT = os.environ.get('EMAIL_PORT')\n    EMAIL_HOST_USER = os.environ.get('EMAIL_HOST_USER')\n    EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_HOST_PASSWORD')\n    EMAIL_USE_TSL = os.environ.get('EMAIL_USE_TSL', True)\nelif EMAIL_BACKEND == 'django.core.mail.backends.filebased.EmailBackend':\n    EMAIL_FILE_PATH = os.path.join(BASE_DIR, '..', 'logs', 'emails.out')\n\n# Celery\nCELERY_ACCEPT_CONTENT = ['pickle', 'json']\nCELERY_BROKER_URL = os.environ.get('CELERY_BROKER_URL')\nCELERY_RESULT_BACKEND = os.environ.get('CELERY_RESULT_BACKEND', '')\nCELERY_CHORD_PROPAGATES = True\n# redis specific, see http://celery.readthedocs.org/en/latest/getting-started/brokers/redis.html#caveats\nCELERY_BROKER_TRANSPORT_OPTIONS = {\n    'fanout_prefix': True,\n    'fanout_patterns': True,\n}\n\n# price_monitor\nPRICE_MONITOR_BASE_URL = os.environ.get('PRICE_MONITOR_BASE_URL', 'http://0.0.0.0:8000')\nPRICE_MONITOR_AWS_ACCESS_KEY_ID = os.environ.get('PRICE_MONITOR_AWS_ACCESS_KEY_ID', '')\nPRICE_MONITOR_AWS_SECRET_ACCESS_KEY = os.environ.get('PRICE_MONITOR_AWS_SECRET_ACCESS_KEY', '')\nPRICE_MONITOR_AMAZON_PRODUCT_API_REGION = os.environ.get('PRICE_MONITOR_AMAZON_PRODUCT_API_REGION', 'DE')\nPRICE_MONITOR_AMAZON_PRODUCT_API_ASSOC_TAG = os.environ.get('PRICE_MONITOR_AMAZON_PRODUCT_API_ASSOC_TAG', '')\nPRICE_MONITOR_AMAZON_ASSOCIATE_NAME = 'John Doe'\nPRICE_MONITOR_AMAZON_ASSOCIATE_SITE = 'Amazon.de'\nPRICE_MONITOR_EMAIL_SENDER = 'Amazon Pricemonitor <pm@localhost>'\nPRICE_MONITOR_SITENAME = 'Pricemonitor Site'\n# refresh product after 1 hours\nPRICE_MONITOR_AMAZON_PRODUCT_REFRESH_THRESHOLD_MINUTES = os.environ.get('PRICE_MONITOR_AMAZON_PRODUCT_REFRESH_THRESHOLD_MINUTES', 60)\n# time after when to notify about a subscription again\nPRICE_MONITOR_SUBSCRIPTION_RENOTIFICATION_MINUTES = os.environ.get('PRICE_MONITOR_SUBSCRIPTION_RENOTIFICATION_MINUTES', 24 * 3 * 60)\n\nREST_FRAMEWORK = {\n    'PAGINATE_BY': 50,\n    'PAGINATE_BY_PARAM': 'page_size',  # Allow client to override, using `?page_size=xxx`.\n    'MAX_PAGINATE_BY': 100  # Maximum limit allowed when using `?page_size=xxx`.\n}\n"
  },
  {
    "path": "docker/web/project/glue/urls.py",
    "content": "\"\"\"URL definitions for the glue project.\"\"\"\nfrom django.conf.urls import include, url\nfrom django.contrib import admin\n\n\nadmin.autodiscover()\n\nurlpatterns = [\n    url(r'^admin/', admin.site.urls),\n    url(r'^', include('glue_auth.urls')),\n    url(r'^', include('price_monitor.urls')),\n]\n"
  },
  {
    "path": "docker/web/project/glue/wsgi.py",
    "content": "\"\"\"\nWSGI config for glue project.\n\nIt exposes the WSGI callable as a module-level variable named ``application``.\n\nFor more information on this file, see\nhttps://docs.djangoproject.com/en/1.8/howto/deployment/wsgi/\n\"\"\"\n\nimport os\n\nfrom django.core.wsgi import get_wsgi_application\n\n# FIXME this is not production ready\nos.environ.setdefault(\"DJANGO_SETTINGS_MODULE\", \"glue.settings\")\n\napplication = get_wsgi_application()\n"
  },
  {
    "path": "docker/web/project/glue_auth/__init__.py",
    "content": ""
  },
  {
    "path": "docker/web/project/glue_auth/fixtures/admin.json",
    "content": "[\n    {\n        \"model\": \"auth.user\",\n        \"pk\": 1,\n        \"fields\": {\n            \"password\": \"pbkdf2_sha256$24000$A7AExuKNKQp3$I4oqUrkVc6LVIZSv2f8DIbjWTSoD1entAJDHjOMV5OI=\",\n            \"last_login\": null,\n            \"is_superuser\": true,\n            \"username\": \"admin\",\n            \"first_name\": \"\",\n            \"last_name\": \"\",\n            \"email\": \"admin@localhost\",\n            \"is_staff\": true,\n            \"is_active\": true,\n            \"date_joined\": \"2016-03-19T13:16:19.168Z\",\n            \"groups\": [],\n            \"user_permissions\": []\n        }\n    }\n]"
  },
  {
    "path": "docker/web/project/glue_auth/models.py",
    "content": ""
  },
  {
    "path": "docker/web/project/glue_auth/templates/glue_auth/base.html",
    "content": "<!DOCTYPE html>{% load static %}\n<html lang=\"en\">\n    <head>\n        <meta charset=\"utf-8\">\n        <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge,chrome=1\">\n        <title>{% block pagetitle %}Pricemonitor Site{% endblock %}</title>\n        <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n        <link rel=\"icon\" href=\"/favicon.ico\" type=\"image/x-icon\" />\n        {% block css_links %}{% endblock %}\n        {% block css_inline %}{% endblock %}\n        {% block js_links %}{% endblock %}\n        {% block js_inline %}{% endblock %}\n    </head>\n    <body>\n        {% block content %}Intentionally blank page.{% endblock %}\n    </body>\n</html>"
  },
  {
    "path": "docker/web/project/glue_auth/templates/glue_auth/login.html",
    "content": "{% extends 'glue_auth/base.html' %}\n\n\n{% block content %}\n    {% if form.errors %}\n        <p>Your username and password didn't match. Please try again.</p>\n    {% endif %}\n    <form method=\"post\" action=\"{% url 'glue_auth:login' %}\">\n        {% csrf_token %}\n        <table>\n            <tr>\n                <td>{{ form.username.label_tag }}</td>\n                <td>{{ form.username }}</td>\n            </tr>\n            <tr>\n                <td>{{ form.password.label_tag }}</td>\n                <td>{{ form.password }}</td>\n            </tr>\n        </table>\n\n        <input type=\"submit\" value=\"login\" />\n        <input type=\"hidden\" name=\"next\" value=\"{{ next }}\" />\n    </form>\n{% endblock %}"
  },
  {
    "path": "docker/web/project/glue_auth/templates/price_monitor/angular_index_view.html",
    "content": "{% extends \"price_monitor/angular_index_view.html\" %}\n\n\n{% block footer %}\n<div class=\"row\">\n    <div class=\"col-md-12\"><em>Template-Block: footer</em></div>\n</div>\n{% endblock %}"
  },
  {
    "path": "docker/web/project/glue_auth/urls.py",
    "content": "\"\"\"URL definitions for the glue_auth module.\"\"\"\nfrom django.conf.urls import url\nfrom django.contrib.auth.views import login, logout_then_login\n\n\napp_name = 'glue_auth'\nurlpatterns = [\n    url(r'^login/$', login, {'template_name': 'glue_auth/login.html'}, name='login'),\n    url(r'^logout/$', logout_then_login, name='logout'),\n]\n"
  },
  {
    "path": "docker/web/project/manage.py",
    "content": "#!/usr/bin/env python\n\"\"\"Main Django entry point.\"\"\"\nimport os\nimport sys\n\nif __name__ == \"__main__\":\n    os.environ.setdefault(\"DJANGO_SETTINGS_MODULE\", \"glue.settings\")\n\n    from django.core.management import execute_from_command_line\n\n    execute_from_command_line(sys.argv)\n"
  },
  {
    "path": "docker/web/project/requirements.pip",
    "content": "Django<2\ndj-database-url\npsycopg2>=2.5.4\ncelery>=4,<5\ndjango-redis-cache>=1.5.4\nhiredis<0.3\nPillow<6"
  },
  {
    "path": "docker/web/web_run.sh",
    "content": "#!/bin/sh\n# wait for postgres\nsleep 5\ncd /srv/project/\npython3 manage.py migrate\npython3 manage.py loaddata admin\npython3 manage.py runserver 0.0.0.0:8000"
  },
  {
    "path": "docs/price_monitor.product_advertising_api.tasks.activity.violet.html",
    "content": "<HTML>\n<HEAD>\n<META name=\"description\"\n\tcontent=\"Violet UML Editor cross format document\" />\n<META name=\"keywords\" content=\"Violet, UML\" />\n<META charset=\"UTF-8\" />\n<SCRIPT type=\"text/javascript\">\n\tfunction switchVisibility() {\n\t\tvar obj = document.getElementById(\"content\");\n\t\tobj.style.display = (obj.style.display == \"block\") ? \"none\" : \"block\";\n\t}\n</SCRIPT>\n</HEAD>\n<BODY>\n\tThis file was generated with Violet UML Editor 2.1.0.\n\t&nbsp;&nbsp;(&nbsp;<A href=# onclick=\"switchVisibility()\">View Source</A>&nbsp;/&nbsp;<A href=\"http://sourceforge.net/projects/violet/files/violetumleditor/\" target=\"_blank\">Download Violet</A>&nbsp;)\n\t<BR />\n\t<BR />\n\t<SCRIPT id=\"content\" type=\"text/xml\"><![CDATA[<ActivityDiagramGraph id=\"1\">\n  <nodes id=\"2\">\n    <ScenarioStartNode id=\"3\">\n      <children id=\"4\"/>\n      <location class=\"Point2D.Double\" id=\"5\" x=\"150.0\" y=\"20.0\"/>\n      <id id=\"6\" value=\"596b93d4-16df-4581-b555-c7de74936216\"/>\n      <revision>1</revision>\n      <backgroundColor id=\"7\">\n        <red>255</red>\n        <green>255</green>\n        <blue>255</blue>\n        <alpha>255</alpha>\n      </backgroundColor>\n      <borderColor id=\"8\">\n        <red>0</red>\n        <green>0</green>\n        <blue>0</blue>\n        <alpha>255</alpha>\n      </borderColor>\n      <textColor reference=\"8\"/>\n    </ScenarioStartNode>\n    <ActivityNode id=\"9\">\n      <children id=\"10\"/>\n      <location class=\"Point2D.Double\" id=\"11\" x=\"20.0\" y=\"100.0\"/>\n      <id id=\"12\" value=\"57d658da-2004-40b0-80dd-037c25c1db17\"/>\n      <revision>1</revision>\n      <backgroundColor reference=\"7\"/>\n      <borderColor reference=\"8\"/>\n      <textColor reference=\"8\"/>\n      <name id=\"13\" justification=\"1\" size=\"4\" underlined=\"false\">\n        <text>check if FindProductsToSynchronizeTask is already scheduled</text>\n      </name>\n    </ActivityNode>\n    <DecisionNode id=\"14\">\n      <children id=\"15\"/>\n      <location class=\"Point2D.Double\" id=\"16\" x=\"180.0\" y=\"210.0\"/>\n      <id id=\"17\" value=\"72132dd4-b1e0-4caa-92c9-bafec679ba31\"/>\n      <revision>1</revision>\n      <backgroundColor reference=\"7\"/>\n      <borderColor reference=\"8\"/>\n      <textColor reference=\"8\"/>\n      <condition id=\"18\" justification=\"1\" size=\"4\" underlined=\"false\">\n        <text></text>\n      </condition>\n    </DecisionNode>\n    <ScenarioEndNode id=\"19\">\n      <children id=\"20\"/>\n      <location class=\"Point2D.Double\" id=\"21\" x=\"590.0\" y=\"720.0\"/>\n      <id id=\"22\" value=\"03341895-bacf-48a0-8219-145598b87c40\"/>\n      <revision>1</revision>\n      <backgroundColor reference=\"7\"/>\n      <borderColor reference=\"8\"/>\n      <textColor reference=\"8\"/>\n    </ScenarioEndNode>\n    <ActivityNode id=\"23\">\n      <children id=\"24\"/>\n      <location class=\"Point2D.Double\" id=\"25\" x=\"470.0\" y=\"260.0\"/>\n      <id id=\"26\" value=\"7fd6c068-b6fd-4571-ae5e-cd1cdf2a79d8\"/>\n      <revision>1</revision>\n      <backgroundColor reference=\"7\"/>\n      <borderColor reference=\"8\"/>\n      <textColor reference=\"8\"/>\n      <name id=\"27\" justification=\"1\" size=\"4\" underlined=\"false\">\n        <text>query for products to update</text>\n      </name>\n    </ActivityNode>\n    <ActivityNode id=\"28\">\n      <children id=\"29\"/>\n      <location class=\"Point2D.Double\" id=\"30\" x=\"600.0\" y=\"400.0\"/>\n      <id id=\"31\" value=\"23238468-9241-4ec6-baf6-a63dbc8d93d3\"/>\n      <revision>1</revision>\n      <backgroundColor reference=\"7\"/>\n      <borderColor reference=\"8\"/>\n      <textColor reference=\"8\"/>\n      <name id=\"32\" justification=\"1\" size=\"4\" underlined=\"false\">\n        <text>for each 10 products execute SynchronizeProductsTask</text>\n      </name>\n    </ActivityNode>\n    <DecisionNode id=\"33\">\n      <children id=\"34\"/>\n      <location class=\"Point2D.Double\" id=\"35\" x=\"530.0\" y=\"350.0\"/>\n      <id id=\"36\" value=\"b9c5b980-201e-4993-abdf-131180c5994c\"/>\n      <revision>1</revision>\n      <backgroundColor reference=\"7\"/>\n      <borderColor reference=\"8\"/>\n      <textColor reference=\"8\"/>\n      <condition id=\"37\" justification=\"1\" size=\"4\" underlined=\"false\">\n        <text></text>\n      </condition>\n    </DecisionNode>\n    <ActivityNode id=\"38\">\n      <children id=\"39\"/>\n      <location class=\"Point2D.Double\" id=\"40\" x=\"640.0\" y=\"510.0\"/>\n      <id id=\"41\" value=\"6b01331c-b058-4499-b09a-874eaa007643\"/>\n      <revision>1</revision>\n      <backgroundColor reference=\"7\"/>\n      <borderColor reference=\"8\"/>\n      <textColor reference=\"8\"/>\n      <name id=\"42\" justification=\"1\" size=\"4\" underlined=\"false\">\n        <text>schedule FindProductsToSynchronizeTask</text>\n      </name>\n    </ActivityNode>\n    <ActivityNode id=\"43\">\n      <children id=\"44\"/>\n      <location class=\"Point2D.Double\" id=\"45\" x=\"230.0\" y=\"400.0\"/>\n      <id id=\"46\" value=\"63226117-a9d8-412b-a5b2-e6a4ebd85fc5\"/>\n      <revision>1</revision>\n      <backgroundColor reference=\"7\"/>\n      <borderColor reference=\"8\"/>\n      <textColor reference=\"8\"/>\n      <name id=\"47\" justification=\"1\" size=\"4\" underlined=\"false\">\n        <text>find date for next update cycle</text>\n      </name>\n    </ActivityNode>\n    <ActivityNode id=\"48\">\n      <children id=\"49\"/>\n      <location class=\"Point2D.Double\" id=\"50\" x=\"110.0\" y=\"510.0\"/>\n      <id id=\"51\" value=\"c0026e8d-0e61-4834-af9c-69298ef3aa94\"/>\n      <revision>1</revision>\n      <backgroundColor reference=\"7\"/>\n      <borderColor reference=\"8\"/>\n      <textColor reference=\"8\"/>\n      <name id=\"52\" justification=\"1\" size=\"4\" underlined=\"false\">\n        <text>schedule FindProductsToSynchronizeTask with next cycle date as eta</text>\n      </name>\n    </ActivityNode>\n  </nodes>\n  <edges id=\"53\">\n    <ActivityTransitionEdge id=\"54\">\n      <start class=\"ScenarioStartNode\" reference=\"3\"/>\n      <end class=\"ActivityNode\" reference=\"9\"/>\n      <startLocation class=\"Point2D.Double\" id=\"55\" x=\"10.0\" y=\"10.0\"/>\n      <endLocation class=\"Point2D.Double\" id=\"56\" x=\"170.0\" y=\"10.0\"/>\n      <transitionPoints id=\"57\">\n        <Point2D.Double id=\"58\" x=\"220.0\" y=\"30.0\"/>\n      </transitionPoints>\n      <id id=\"59\" value=\"ae418b6a-2954-4425-8f6a-3a41d1c0515f\"/>\n      <revision>1</revision>\n      <lineStyle name=\"SOLID\"/>\n      <startArrowHead name=\"NONE\"/>\n      <bentStyle name=\"FREE\"/>\n      <startLabel></startLabel>\n      <middleLabel></middleLabel>\n      <endLabel></endLabel>\n    </ActivityTransitionEdge>\n    <ActivityTransitionEdge id=\"60\">\n      <start class=\"ActivityNode\" reference=\"9\"/>\n      <end class=\"DecisionNode\" reference=\"14\"/>\n      <startLocation class=\"Point2D.Double\" id=\"61\" x=\"160.0\" y=\"30.0\"/>\n      <endLocation class=\"Point2D.Double\" id=\"62\" x=\"40.0\" y=\"10.0\"/>\n      <transitionPoints id=\"63\"/>\n      <id id=\"64\" value=\"889b8012-e61b-4db8-b8be-4a067b2f724e\"/>\n      <revision>1</revision>\n      <lineStyle name=\"SOLID\"/>\n      <startArrowHead name=\"NONE\"/>\n      <bentStyle name=\"AUTO\"/>\n      <startLabel></startLabel>\n      <middleLabel></middleLabel>\n      <endLabel></endLabel>\n    </ActivityTransitionEdge>\n    <ActivityTransitionEdge id=\"65\">\n      <start class=\"DecisionNode\" reference=\"14\"/>\n      <end class=\"ActivityNode\" reference=\"23\"/>\n      <startLocation class=\"Point2D.Double\" id=\"66\" x=\"60.0\" y=\"20.0\"/>\n      <endLocation class=\"Point2D.Double\" id=\"67\" x=\"50.0\" y=\"40.0\"/>\n      <transitionPoints class=\"Point2D.Double-array\" id=\"68\"/>\n      <id id=\"69\" value=\"0c21ae49-d53a-45c1-ae12-1eae41fcad68\"/>\n      <revision>1</revision>\n      <lineStyle name=\"SOLID\"/>\n      <startArrowHead name=\"NONE\"/>\n      <bentStyle name=\"HV\"/>\n      <startLabel>no</startLabel>\n      <middleLabel></middleLabel>\n      <endLabel></endLabel>\n    </ActivityTransitionEdge>\n    <ActivityTransitionEdge id=\"70\">\n      <start class=\"ActivityNode\" reference=\"23\"/>\n      <end class=\"DecisionNode\" reference=\"33\"/>\n      <startLocation class=\"Point2D.Double\" id=\"71\" x=\"170.0\" y=\"40.0\"/>\n      <endLocation class=\"Point2D.Double\" id=\"72\" x=\"40.0\" y=\"10.0\"/>\n      <transitionPoints id=\"73\"/>\n      <id id=\"74\" value=\"8583a4f0-df19-4f25-a956-d89bc9cfd671\"/>\n      <revision>1</revision>\n      <lineStyle name=\"SOLID\"/>\n      <startArrowHead name=\"NONE\"/>\n      <bentStyle name=\"AUTO\"/>\n      <startLabel></startLabel>\n      <middleLabel></middleLabel>\n      <endLabel></endLabel>\n    </ActivityTransitionEdge>\n    <ActivityTransitionEdge id=\"75\">\n      <start class=\"DecisionNode\" reference=\"33\"/>\n      <end class=\"ActivityNode\" reference=\"28\"/>\n      <startLocation class=\"Point2D.Double\" id=\"76\" x=\"50.0\" y=\"20.0\"/>\n      <endLocation class=\"Point2D.Double\" id=\"77\" x=\"50.0\" y=\"10.0\"/>\n      <transitionPoints id=\"78\"/>\n      <id id=\"79\" value=\"1133f62d-c35d-4102-b14a-f2768df03359\"/>\n      <revision>1</revision>\n      <lineStyle name=\"SOLID\"/>\n      <startArrowHead name=\"NONE\"/>\n      <bentStyle name=\"HV\"/>\n      <startLabel>products available</startLabel>\n      <middleLabel></middleLabel>\n      <endLabel></endLabel>\n    </ActivityTransitionEdge>\n    <ActivityTransitionEdge id=\"80\">\n      <start class=\"ActivityNode\" reference=\"28\"/>\n      <end class=\"ActivityNode\" reference=\"38\"/>\n      <startLocation class=\"Point2D.Double\" id=\"81\" x=\"160.0\" y=\"50.0\"/>\n      <endLocation class=\"Point2D.Double\" id=\"82\" x=\"130.0\" y=\"40.0\"/>\n      <transitionPoints id=\"83\"/>\n      <id id=\"84\" value=\"1cd5b5b2-4af8-46d4-bdbc-d3cdcec06aa3\"/>\n      <revision>1</revision>\n      <lineStyle name=\"SOLID\"/>\n      <startArrowHead name=\"NONE\"/>\n      <bentStyle name=\"AUTO\"/>\n      <startLabel></startLabel>\n      <middleLabel></middleLabel>\n      <endLabel></endLabel>\n    </ActivityTransitionEdge>\n    <ActivityTransitionEdge id=\"85\">\n      <start class=\"ActivityNode\" reference=\"38\"/>\n      <end class=\"ScenarioEndNode\" reference=\"19\"/>\n      <startLocation class=\"Point2D.Double\" id=\"86\" x=\"110.0\" y=\"40.0\"/>\n      <endLocation class=\"Point2D.Double\" id=\"87\" x=\"10.0\" y=\"10.0\"/>\n      <transitionPoints id=\"88\">\n        <Point2D.Double id=\"89\" x=\"780.0\" y=\"730.0\"/>\n      </transitionPoints>\n      <id id=\"90\" value=\"22f988b3-95be-426c-a69e-c2ce47aec62a\"/>\n      <revision>1</revision>\n      <lineStyle name=\"SOLID\"/>\n      <startArrowHead name=\"NONE\"/>\n      <bentStyle name=\"FREE\"/>\n      <startLabel></startLabel>\n      <middleLabel></middleLabel>\n      <endLabel></endLabel>\n    </ActivityTransitionEdge>\n    <ActivityTransitionEdge id=\"91\">\n      <start class=\"DecisionNode\" reference=\"33\"/>\n      <end class=\"ActivityNode\" reference=\"43\"/>\n      <startLocation class=\"Point2D.Double\" id=\"92\" x=\"50.0\" y=\"20.0\"/>\n      <endLocation class=\"Point2D.Double\" id=\"93\" x=\"190.0\" y=\"20.0\"/>\n      <transitionPoints id=\"94\"/>\n      <id id=\"95\" value=\"b7d5e7c1-c3d6-436e-b035-9bfe8ebb556f\"/>\n      <revision>1</revision>\n      <lineStyle name=\"SOLID\"/>\n      <startArrowHead name=\"NONE\"/>\n      <bentStyle name=\"HV\"/>\n      <startLabel>no products available</startLabel>\n      <middleLabel></middleLabel>\n      <endLabel></endLabel>\n    </ActivityTransitionEdge>\n    <ActivityTransitionEdge id=\"96\">\n      <start class=\"ActivityNode\" reference=\"43\"/>\n      <end class=\"ActivityNode\" reference=\"48\"/>\n      <startLocation class=\"Point2D.Double\" id=\"97\" x=\"60.0\" y=\"50.0\"/>\n      <endLocation class=\"Point2D.Double\" id=\"98\" x=\"170.0\" y=\"10.0\"/>\n      <transitionPoints id=\"99\"/>\n      <id id=\"100\" value=\"e6cce7eb-e720-4f94-9c22-6ecb1ab60981\"/>\n      <revision>1</revision>\n      <lineStyle name=\"SOLID\"/>\n      <startArrowHead name=\"NONE\"/>\n      <bentStyle name=\"AUTO\"/>\n      <startLabel></startLabel>\n      <middleLabel></middleLabel>\n      <endLabel></endLabel>\n    </ActivityTransitionEdge>\n    <ActivityTransitionEdge id=\"101\">\n      <start class=\"ActivityNode\" reference=\"48\"/>\n      <end class=\"ScenarioEndNode\" reference=\"19\"/>\n      <startLocation class=\"Point2D.Double\" id=\"102\" x=\"100.0\" y=\"40.0\"/>\n      <endLocation class=\"Point2D.Double\" id=\"103\" x=\"10.0\" y=\"10.0\"/>\n      <transitionPoints id=\"104\">\n        <Point2D.Double id=\"105\" x=\"600.0\" y=\"540.0\"/>\n      </transitionPoints>\n      <id id=\"106\" value=\"0725e3f4-7140-4799-8073-5e3b6d1e916f\"/>\n      <revision>1</revision>\n      <lineStyle name=\"SOLID\"/>\n      <startArrowHead name=\"NONE\"/>\n      <bentStyle name=\"FREE\"/>\n      <startLabel></startLabel>\n      <middleLabel></middleLabel>\n      <endLabel></endLabel>\n    </ActivityTransitionEdge>\n    <ActivityTransitionEdge id=\"107\">\n      <start class=\"DecisionNode\" reference=\"14\"/>\n      <end class=\"ScenarioEndNode\" reference=\"19\"/>\n      <startLocation class=\"Point2D.Double\" id=\"108\" x=\"50.0\" y=\"30.0\"/>\n      <endLocation class=\"Point2D.Double\" id=\"109\" x=\"10.0\" y=\"10.0\"/>\n      <transitionPoints id=\"110\">\n        <Point2D.Double id=\"111\" x=\"70.0\" y=\"230.0\"/>\n        <Point2D.Double id=\"112\" x=\"70.0\" y=\"730.0\"/>\n      </transitionPoints>\n      <id id=\"113\" value=\"d4d2be9c-3e06-47a8-876b-edc360eefe33\"/>\n      <revision>1</revision>\n      <lineStyle name=\"SOLID\"/>\n      <startArrowHead name=\"NONE\"/>\n      <bentStyle name=\"FREE\"/>\n      <startLabel>yes</startLabel>\n      <middleLabel></middleLabel>\n      <endLabel></endLabel>\n    </ActivityTransitionEdge>\n  </edges>\n</ActivityDiagramGraph>]]></SCRIPT>\n\t<BR />\n\t<BR />\n\t<IMG alt=\"embedded diagram image\" src=\"data:image/png;base64,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\" />\n</BODY>\n</HTML>"
  },
  {
    "path": "hooks/pre-commit",
    "content": "#!/bin/sh\nflake8 price_monitor --ignore=E501,E128 --exclude=migrations"
  },
  {
    "path": "price_monitor/__init__.py",
    "content": "\"\"\"\ndjango-amazon-price-monitor monitors prices of Amazon products.\n\"\"\"\n__version_info__ = {\n    'major': 0,\n    'minor': 7,\n    'micro': 0,\n    'releaselevel': 'final',\n    'serial': 0,\n}\n\n\ndef get_version(short=False):\n    assert __version_info__['releaselevel'] in ('alpha', 'beta', 'final')\n    version = [\"{major:d}.{minor:d}\".format(**__version_info__), ]\n    if __version_info__['micro']:\n        version.append(\".{micro:d}\".format(**__version_info__))\n    if __version_info__['releaselevel'] != 'final' and not short:\n        version.append('{0!s}{1:d}'.format(__version_info__['releaselevel'][0], __version_info__['serial']))\n    return ''.join(version)\n\n__version__ = get_version()\n"
  },
  {
    "path": "price_monitor/admin.py",
    "content": "\"\"\"AdminSite definitions\"\"\"\nfrom django.contrib import admin\nfrom django.utils.translation import ugettext_lazy\n\nfrom price_monitor.models import (\n    EmailNotification,\n    Price,\n    Product,\n    Subscription,\n)\n\n\nclass PriceAdmin(admin.ModelAdmin):\n\n    \"\"\"Admin for the model Price\"\"\"\n\n    list_display = ('date_seen', 'value', 'currency', )\n    list_filter = ('product', )\n\n\nclass ProductAdmin(admin.ModelAdmin):\n\n    \"\"\"Admin for the model Product\"\"\"\n\n    list_display = ('asin', 'title', 'artist', 'audience_rating', 'status', 'date_updated', 'date_last_synced', )\n    list_filter = ('status', 'audience_rating', )\n    search_fields = ('asin', )\n    readonly_fields = ('current_price', 'highest_price', 'lowest_price',)\n\n    actions = ['reset_to_created', 'resynchronize', ]\n\n    def reset_to_created(self, request, queryset):  # pylint:disable=unused-argument\n        \"\"\"\n        Resets the status of the product back to created.\n        :param request: sent request\n        :param queryset: queryset containing the products\n        \"\"\"\n        queryset.update(status=0)\n    reset_to_created.short_description = ugettext_lazy('Reset to status \"Created\".')\n\n    def resynchronize(self, request, queryset):  # pylint:disable=unused-argument\n        \"\"\"\n        Synchronizes the sent products with the product advertising api.\n        :param request: sent request\n        :param queryset: queryset containing the products\n        \"\"\"\n        from price_monitor.product_advertising_api.tasks import SynchronizeProductsTask\n        for product in queryset:\n            SynchronizeProductsTask.delay([product.asin])\n    resynchronize.short_description = ugettext_lazy('Resynchronize with API')\n\n\nclass SubscriptionAdmin(admin.ModelAdmin):\n\n    \"\"\"Admin for the model Subscription\"\"\"\n\n    list_display = ('product', 'price_limit', 'owner', 'date_last_notification', 'get_email_address', 'public_id',)\n    list_filter = ('owner__username', 'price_limit', )\n\n\nclass EmailNotificationAdmin(admin.ModelAdmin):\n\n    \"\"\"Admin for the model EmailNotification\"\"\"\n\n    list_display = ('email', 'owner', 'public_id',)\n\n\nadmin.site.register(Price, PriceAdmin)\nadmin.site.register(Product, ProductAdmin)\nadmin.site.register(Subscription, SubscriptionAdmin)\nadmin.site.register(EmailNotification, EmailNotificationAdmin)\n"
  },
  {
    "path": "price_monitor/api/__init__.py",
    "content": ""
  },
  {
    "path": "price_monitor/api/renderers/PriceChartPNGRenderer.py",
    "content": "\"\"\"Module for rendering price charts as PNG\"\"\"\nimport dateutil.parser\nimport hashlib\n\nfrom ... import app_settings\n\nfrom django.core.cache import caches\nfrom django.core.cache.backends.base import InvalidCacheBackendError\n\nfrom pygal import DateTimeLine\nfrom pygal.style import RedBlueStyle\n\nfrom rest_framework.renderers import BaseRenderer\n\nfrom tempfile import TemporaryFile\n\n\ndef bool_helper(x):\n    \"\"\"\n    Returns True if the value is something that can be mapped to a boolean value.\n\n    :param x: the value to check\n    :return: the mapped boolean value or False if not mappable\n    \"\"\"\n    return x in [1, '1', 'true', 'True']\n\n\nclass PriceChartPNGRenderer(BaseRenderer):\n\n    \"\"\"A renderer to render charts as PNG for prices\"\"\"\n\n    media_type = 'image/png'\n    format = 'png'\n    charset = None\n    render_style = 'binary'\n\n    # TODO: documentation\n    allowed_chart_url_args = {\n        'height': lambda x: int(x),  # pylint:disable=unnecessary-lambda\n        'width': lambda x: int(x),  # pylint:disable=unnecessary-lambda\n        'margin': lambda x: int(x),  # pylint:disable=unnecessary-lambda\n        'spacing': lambda x: int(x),  # pylint:disable=unnecessary-lambda\n        'show_dots': bool_helper,\n        'show_legend': bool_helper,\n        'show_x_labels': bool_helper,\n        'show_y_labels': bool_helper,\n        'show_minor_y_labels': bool_helper,\n        'y_labels_major_count': lambda x: int(x),  # pylint:disable=unnecessary-lambda\n    }\n\n    allowed_style_url_args = {\n        'no_data_font_size': lambda x: int(x),  # pylint:disable=unnecessary-lambda\n    }\n\n    def render(self, data, accepted_media_type=None, renderer_context=None):  # pylint:disable=unused-argument\n        \"\"\"Renders `data` into serialized XML.\"\"\"\n        # first get the cache to use or None\n        try:\n            cache = caches[app_settings.PRICE_MONITOR_GRAPH_CACHE_NAME]\n        except InvalidCacheBackendError:\n            cache = None\n        # sanitize arguments\n        sanitized_args = self.sanitize_allowed_args(renderer_context['request']) if 'request' in renderer_context else {}\n\n        # generate cache key\n        cache_key = self.create_cache_key(data, sanitized_args)\n        # only read from cache if there is any\n        content = cache.get(cache_key) if cache is not None else None\n        if content is None:\n            # create graph instance\n            graph = self.create_graph(data, sanitized_args)\n\n            # write graph to temporary file\n            with TemporaryFile() as file_:\n                graph.render_to_png(file_)\n\n                # only write to cache if there is any\n                if cache is not None:\n                    # seek back to start\n                    file_.seek(0)\n                    cache.set(cache_key, file_.read())\n                # and back to start again\n                file_.seek(0)\n                # return the content\n                return file_.read()\n        else:\n            # return the cache content\n            return content\n\n    def sanitize_allowed_args(self, request):\n        \"\"\"Checks url arguments by using the sanitation methods given in self.allowed_*_url_args\"\"\"\n        sanitized_args = {}\n        if request.method == 'POST':\n            args = request.POST\n        elif request.method == 'GET':\n            args = request.GET\n        else:\n            return sanitized_args\n\n        for arg, sanitizer in self.allowed_chart_url_args.items() ^ self.allowed_style_url_args.items():\n            if arg in args:\n                try:\n                    sanitized_args[arg] = sanitizer(args[arg])\n                except ValueError:\n                    # sanitation gone wrong, so pass\n                    continue\n        return sanitized_args\n\n    def create_cache_key(self, data, args):\n        \"\"\"Creates a cache key based on rendering data\"\"\"\n        hash_data = str(data).encode('utf-8')\n        hash_data += str(args).encode('utf-8')\n        return app_settings.PRICE_MONITOR_GRAPH_CACHE_KEY_PREFIX + hashlib.md5(hash_data).hexdigest()\n\n    def create_graph(self, data, args):\n        \"\"\"Creates the graph based on rendering data\"\"\"\n        style_arguments = {}\n        for arg in self.allowed_style_url_args.keys():\n            if arg in args:\n                style_arguments.update({arg: args[arg]})\n\n        line_chart_arguments = {\n            'style': RedBlueStyle(**style_arguments),\n            'x_label_rotation': 25,\n            'x_value_formatter': lambda dt: dt.strftime('%y-%m-%d %H:%M'),\n        }\n        for arg in self.allowed_chart_url_args.keys():\n            if arg in args:\n                line_chart_arguments.update({arg: args[arg]})\n\n        line_chart = DateTimeLine(**line_chart_arguments)\n        if data:\n            values = [(dateutil.parser.parse(price['date_seen']), price['value']) for price in data]\n            line_chart.add(data[0]['currency'], values)\n        return line_chart\n"
  },
  {
    "path": "price_monitor/api/renderers/__init__.py",
    "content": ""
  },
  {
    "path": "price_monitor/api/serializers/EmailNotificationSerializer.py",
    "content": "\"\"\"Serializer for EmailNotification model\"\"\"\nfrom ...models import EmailNotification\n\nfrom rest_framework import serializers\n\n\nclass EmailNotificationSerializer(serializers.ModelSerializer):\n\n    \"\"\"Serializes EmailNotification objects. Just renders public_id as id and the email address\"\"\"\n\n    owner = serializers.HiddenField(\n        default=serializers.CurrentUserDefault()\n    )\n\n    class Meta(object):\n\n        \"\"\"Some model meta\"\"\"\n\n        model = EmailNotification\n        fields = ('owner', 'email',)\n"
  },
  {
    "path": "price_monitor/api/serializers/PriceSerializer.py",
    "content": "\"\"\"Serializer for Price model\"\"\"\nfrom ...models import Price\n\nfrom rest_framework import serializers\n\n\nclass PriceSerializer(serializers.ModelSerializer):\n\n    \"\"\"Serializes prices by showing currency, value and date seen\"\"\"\n\n    class Meta(object):\n\n        \"\"\"Some model meta\"\"\"\n\n        model = Price\n        fields = (\n            'value',\n            'currency',\n            'date_seen',\n        )\n"
  },
  {
    "path": "price_monitor/api/serializers/ProductSerializer.py",
    "content": "\"\"\"Serializer for Product model\"\"\"\nfrom .SubscriptionSerializer import SubscriptionSerializer\nfrom ...models import EmailNotification, Product, Subscription\n\nfrom django.db import transaction\n\nfrom rest_framework import serializers\n\n\nclass ProductSerializer(serializers.ModelSerializer):\n\n    \"\"\"\n    Product serializer. Serializes all fields needed for frontend and id from asin.\n    Also sets all fields but asin to read only\n    \"\"\"\n\n    asin = serializers.CharField(max_length=100)\n\n    # for these three values get_{{ value name }} is the default, but DRF prohibits setting the default value ...\n    current_price = serializers.SerializerMethodField()\n    highest_price = serializers.SerializerMethodField()\n    lowest_price = serializers.SerializerMethodField()\n    image_urls = serializers.SerializerMethodField()\n    subscription_set = SubscriptionSerializer(many=True)\n\n    def __render_price_dict(self, price):\n        \"\"\"\n        Renders price instance as dict\n\n        :param price: price instance\n        :type price:  Price\n        :return:      price instance as dict\n        :rtype:       dict\n        \"\"\"\n        return {\n            'value': price.value,\n            'currency': price.currency,\n            'date_seen': price.date_seen,\n        }\n\n    def get_current_price(self, obj):\n        \"\"\"\n        Renderes current price dict as read only value into product representation\n\n        :param obj: product to get price for\n        :type obj:  Product\n        :returns:   Dict with current price values\n        :rtype:     dict\n        \"\"\"\n        if obj.current_price:\n            return self.__render_price_dict(obj.current_price)\n\n    def get_highest_price(self, obj):\n        \"\"\"\n        Renders highest price dict as read only value into product representation\n\n        :param obj: product to get price for\n        :type obj:  Product\n        :returns:   Dict with highest price values\n        :rtype:     dict\n        \"\"\"\n        if obj.highest_price:\n            return self.__render_price_dict(obj.highest_price)\n\n    def get_lowest_price(self, obj):\n        \"\"\"\n        Renders lowest price dict as read only value into product representation\n\n        :param obj: product to get price for\n        :type obj:  Product\n        :returns:   Dict with lowest price values\n        :rtype:     dict\n        \"\"\"\n        if obj.lowest_price:\n            return self.__render_price_dict(obj.lowest_price)\n\n    def get_image_urls(self, obj):\n        \"\"\"\n        Renders image urls as read only value into product representation\n\n        :param obj: object to get image urls for\n        :type obj:  Product\n        :returns:   dict with image urls\n        :rtype:     dict\n        \"\"\"\n        return obj.get_image_urls()\n\n    @transaction.atomic\n    def create(self, validated_data):\n        \"\"\"\n        Overwriting default create function to ensure, that the already existing instance of product is used, if asin is already in database\n\n        :param validated_data: valid form data\n        :type validated_data:  dict\n        :return:               created or fetched product\n        :rtype:                Product\n        \"\"\"\n        # product = Product.objects.get_or_create(asin=validated_data['asin'])[0]\n        try:\n            product = Product.objects.get(asin__iexact=validated_data['asin'])\n        except Product.DoesNotExist:\n            product = Product.objects.create(asin=validated_data['asin'])\n\n        for new_subscription in validated_data['subscription_set']:\n            # first fetch EmailNotification object\n            email_notification = EmailNotification.objects.get_or_create(\n                owner=self.context['request'].user,\n                email=new_subscription['email_notification']['email']\n            )[0]\n\n            # don't create double subscriptions with same price limit\n            product.subscription_set.get_or_create(\n                owner=self.context['request'].user,\n                price_limit=new_subscription['price_limit'],\n                email_notification=email_notification\n            )\n        return product\n\n    def update(self, instance, validated_data):\n        \"\"\"\n        Overwrites parent function to enable update of products subscriptions\n\n        :param instance:        the product instance\n        :type instance:         Product\n        :param validated_data:  dict with validated data from request\n        :type validated_data:   dict\n        :returns:               Updated product instance (in fact there are only updates to subscriptions)\n        :rtype:                 Product\n        \"\"\"\n        new_public_ids = []\n        for value_dict in validated_data['subscription_set']:\n            # get public_id if there is any\n            public_id = value_dict.get('public_id')\n            new_public_ids.append(public_id)\n            if public_id:\n                subscription = Subscription.objects.get_or_create(public_id=public_id)[0]\n            else:\n                # this is a new line!\n                subscription = Subscription()\n                subscription.product = instance\n                subscription.owner = self.context['request'].user\n\n            subscription.price_limit = value_dict['price_limit']\n            # simply create email notifcation object if this is a new address\n            subscription.email_notification = EmailNotification.objects.get_or_create(\n                owner=self.context['request'].user,\n                email=value_dict['email_notification']['email']\n            )[0]\n            subscription.save()\n\n        # remove all subscriptions not in new set subscriptions\n        instance.subscription_set.filter(owner=self.context['request'].user).exclude(public_id__in=new_public_ids).delete()\n        return self.context['view'].filter_queryset(self.context['view'].get_queryset()).get(pk=instance.pk)\n\n    class Meta(object):\n\n        \"\"\"Some model meta\"\"\"\n\n        model = Product\n        fields = (\n            'date_creation',\n            'date_updated',\n            'date_last_synced',\n            'status',\n            'subscription_set',\n\n            # amazon specific fields\n            'asin',\n            'title',\n            'artist',\n            'isbn',\n            'eisbn',\n            'binding',\n            'date_publication',\n            'date_release',\n\n            # amazon urls\n            'image_urls',\n            'offer_url',\n            'current_price',\n            'highest_price',\n            'lowest_price',\n        )\n        # TODO: check if this is good\n        read_only_fields = (\n            'date_creation',\n            'date_updated',\n            'date_last_synced',\n            'status',\n\n            # amazon specific fields\n            'title',\n            'artist',\n            'isbn',\n            'eisbn',\n            'author',\n            'publisher',\n            'label',\n            'manufacturer',\n            'brand',\n            'binding',\n            'pages',\n            'date_publication',\n            'date_release',\n            'edition',\n            'model',\n            'part_number',\n\n            # amazon urls\n            'image_urls',\n            'offer_url',\n        )\n"
  },
  {
    "path": "price_monitor/api/serializers/SubscriptionSerializer.py",
    "content": "\"\"\"Serializer for Subscription model\"\"\"\nfrom .EmailNotificationSerializer import EmailNotificationSerializer\nfrom ...models import Subscription\n\nfrom rest_framework import serializers\n\n\nclass SubscriptionSerializer(serializers.ModelSerializer):\n\n    \"\"\"Serializes subscription with product inline. Also renders id frm public_id\"\"\"\n\n    # this field needs to be writable to get it's value into update function of ProductSerializer\n    id = serializers.CharField(source='public_id', required=False)\n    email_notification = EmailNotificationSerializer()\n\n    class Meta(object):\n\n        \"\"\"Some model meta\"\"\"\n\n        model = Subscription\n        fields = (\n            'id',\n            'price_limit',\n            'date_last_notification',\n            'email_notification',\n        )\n\n        read_only_fields = (\n            'date_last_notification',\n        )\n"
  },
  {
    "path": "price_monitor/api/serializers/__init__.py",
    "content": ""
  },
  {
    "path": "price_monitor/api/urls.py",
    "content": "from django.conf.urls import url\n\nfrom .views.EmailNotificationListView import EmailNotificationListView\nfrom .views.PriceListView import PriceListView\nfrom .views.ProductListView import ProductListView\nfrom .views.ProductCreateRetrieveUpdateDestroyAPIView import ProductCreateRetrieveUpdateDestroyAPIView\nfrom .views.SubscriptionRetrieveView import SubscriptionRetrieveView\nfrom .views.SubscriptionListView import SubscriptionListView\n\n\nurlpatterns = [\n    url(r'^email-notifications/$', EmailNotificationListView.as_view(), name='api_email_notification_list'),\n    url(r'^products/(?P<asin>[0-9a-zA-Z_-]+)/prices/$', PriceListView.as_view(), name='api_product_price_list'),\n    url(r'^products/(?P<asin>[0-9a-zA-Z_-]+)/$', ProductCreateRetrieveUpdateDestroyAPIView.as_view(), name='api_product_retrieve'),\n    url(r'^products/$', ProductListView.as_view(), name='api_product_list'),\n    url(\n        r'^subscriptions/(?P<public_id>(:public_id|[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}))/$',\n        SubscriptionRetrieveView.as_view(),\n        name='api_subscription_retrieve'\n    ),\n    url(r'^subscriptions/$', SubscriptionListView.as_view(), name='api_subscription_list'),\n]\n"
  },
  {
    "path": "price_monitor/api/views/EmailNotificationListView.py",
    "content": "\"\"\"View for listing email notifications\"\"\"\nfrom ..serializers.EmailNotificationSerializer import EmailNotificationSerializer\nfrom ...models.EmailNotification import EmailNotification\n\nfrom rest_framework import generics, mixins, permissions\n\n\nclass EmailNotificationListView(mixins.CreateModelMixin, generics.ListAPIView):\n\n    \"\"\"View for rendering list of EmailNotification objects\"\"\"\n\n    model = EmailNotification\n    serializer_class = EmailNotificationSerializer\n    permission_classes = [\n        # only return the list if user is authenticated\n        permissions.IsAuthenticated\n    ]\n\n    def post(self, request, *args, **kwargs):\n        \"\"\"\n        Add post method to create object\n\n        :param request: the request\n        :type request:  HttpRequest\n        :return:        Result of creation\n        :rtype:         HttpResponse\n        \"\"\"\n        return self.create(request, *args, **kwargs)\n\n    def get_queryset(self):\n        \"\"\"\n        Filters queryset by the authenticated user\n\n        :returns: filtered EmailNotification objects\n        :rtype:   QuerySet\n        \"\"\"\n        return self.model.objects.filter(owner=self.request.user)\n"
  },
  {
    "path": "price_monitor/api/views/PriceListView.py",
    "content": "\"\"\"View for listing prices\"\"\"\nfrom ..renderers.PriceChartPNGRenderer import PriceChartPNGRenderer\nfrom ..serializers.PriceSerializer import PriceSerializer\nfrom ...models.Price import Price\n\nfrom datetime import timedelta\n\nfrom django.utils import timezone\n\nfrom rest_framework.generics import ListAPIView\n\n\nclass PriceListView(ListAPIView):\n    model = Price\n    serializer_class = PriceSerializer\n    renderer_classes = ListAPIView.renderer_classes + [PriceChartPNGRenderer]\n\n    def get_queryset(self):\n        \"\"\"\n        Returns the elements matching the product's ASIN within the last 7 days.\n\n        :return: QuerySet\n        \"\"\"\n        # FIXME this has room fro improvement, we could only show the values that changes within a wider time range - but currently I don't know how to do that\n        return self.model.objects.filter(\n            product__asin=self.kwargs.get('asin'),\n            date_seen__gte=timezone.now() - timedelta(days=7),\n        ).order_by('-date_seen')\n"
  },
  {
    "path": "price_monitor/api/views/ProductCreateRetrieveUpdateDestroyAPIView.py",
    "content": "\"\"\"Mixed view for API\"\"\"\nfrom .mixins.ProductFilteringMixin import ProductFilteringMixin\nfrom ..serializers.ProductSerializer import ProductSerializer\nfrom ...models.Product import Product\n\nfrom rest_framework import generics, mixins, permissions\n\n\nclass ProductCreateRetrieveUpdateDestroyAPIView(ProductFilteringMixin, mixins.CreateModelMixin, generics.RetrieveUpdateDestroyAPIView):\n\n    \"\"\"Returns single instance of Product, if user is authenticated\"\"\"\n\n    model = Product\n    serializer_class = ProductSerializer\n    lookup_field = 'asin'\n    permission_classes = [\n        # only return the product if user is authenticated\n        permissions.IsAuthenticated\n    ]\n\n    def post(self, request, *args, **kwargs):\n        \"\"\"\n        Add post method to create object\n\n        :param request: the request\n        :type request:  HttpRequest\n        :return:        Result of creation\n        :rtype:         HttpResponse\n        \"\"\"\n        return self.create(request, *args, **kwargs)\n\n    def get_queryset(self):\n        \"\"\"\n        Filters queryset by the authenticated user\n\n        :returns: filtered Product objects\n        :rtype:   QuerySet\n        \"\"\"\n        # distinct is needed to prevent multiple instances of product in resultset if multiple subscriptions are present\n        return self.model.objects.filter(subscription__owner=self.request.user).distinct()\n\n    def perform_destroy(self, instance):\n        \"\"\"\n        Overwrite base function to delete subscriptions, not the product itself\n\n        :param instance: the product to delete subscriptions from\n        :type instance:  Product\n        \"\"\"\n        instance.subscription_set.filter(owner=self.request.user).delete()\n"
  },
  {
    "path": "price_monitor/api/views/ProductListView.py",
    "content": "\"\"\"View for listing subscriptions\"\"\"\nfrom rest_framework import generics, permissions\n\nfrom .mixins.ProductFilteringMixin import ProductFilteringMixin\nfrom ..serializers.ProductSerializer import ProductSerializer\nfrom ...models.Product import Product\n\n\nclass ProductListView(ProductFilteringMixin, generics.ListAPIView):\n\n    \"\"\"Returns list of Products and provides endpoint to create Products, if user is authenticated.\"\"\"\n\n    model = Product\n    serializer_class = ProductSerializer\n    allow_empty = True\n    queryset = Product.objects.all()\n    permission_classes = [\n        # only return the list if user is authenticated\n        permissions.IsAuthenticated\n    ]\n"
  },
  {
    "path": "price_monitor/api/views/SubscriptionListView.py",
    "content": "\"\"\"View for listing subscriptions\"\"\"\nfrom ..serializers.SubscriptionSerializer import SubscriptionSerializer\nfrom ...models.Subscription import Subscription\n\nfrom rest_framework import generics, permissions\n\n\nclass SubscriptionListView(generics.ListAPIView):\n\n    \"\"\"Returns list of subscriptions, if user is authenticated\"\"\"\n\n    model = Subscription\n    serializer_class = SubscriptionSerializer\n    allow_empty = True\n\n    permission_classes = [\n        # only return the list if user is authenticated\n        permissions.IsAuthenticated\n    ]\n\n    def get_queryset(self):\n        \"\"\"\n        Filters queryset by the authenticated user\n\n        :returns: filtered Subscription objects\n        :rtype:   QuerySet\n        \"\"\"\n        return self.model.objects.filter(owner=self.request.user)\n"
  },
  {
    "path": "price_monitor/api/views/SubscriptionRetrieveView.py",
    "content": "\"\"\"View for retrieving a subscription\"\"\"\nfrom ..serializers.SubscriptionSerializer import SubscriptionSerializer\nfrom ...models.Subscription import Subscription\n\nfrom rest_framework import generics, permissions\n\n\nclass SubscriptionRetrieveView(generics.RetrieveAPIView):\n\n    \"\"\"Returns instance of Subscription, if user is authenticated\"\"\"\n\n    model = Subscription\n    serializer_class = SubscriptionSerializer\n    lookup_field = 'public_id'\n    permission_classes = [\n        # only return the list if user is authenticated\n        permissions.IsAuthenticated\n    ]\n\n    def get_queryset(self):\n        \"\"\"\n\n        Filters queryset by the authenticated user\n        :returns: filtered Subscription objects\n        :rtype:   QuerySet\n        \"\"\"\n        return self.model.objects.filter(owner=self.request.user)\n"
  },
  {
    "path": "price_monitor/api/views/__init__.py",
    "content": ""
  },
  {
    "path": "price_monitor/api/views/mixins/ProductFilteringMixin.py",
    "content": "\"\"\"Mixin for product filtering\"\"\"\nfrom django.db.models.query import Prefetch\n\nfrom ....models.Subscription import Subscription\n\n\nclass ProductFilteringMixin(object):\n\n    \"\"\"Mixin for filtering products of the current user and have the lowest, highest and current price included.\"\"\"\n\n    def filter_queryset(self, queryset):\n        \"\"\"\n        Filters queryset by the authenticated user\n\n        :returns: filtered Product objects\n        :rtype:   QuerySet\n        \"\"\"\n        queryset = super(ProductFilteringMixin, self).filter_queryset(queryset)\n        return queryset\\\n            .select_related('highest_price', 'lowest_price', 'current_price')\\\n            .prefetch_related(\n                Prefetch(\n                    'subscription_set',\n                    queryset=Subscription.objects.filter(\n                        owner=self.request.user\n                    ).select_related('email_notification').distinct()\n                )\n            ).filter(subscription__owner=self.request.user).distinct()\n"
  },
  {
    "path": "price_monitor/api/views/mixins/__init__.py",
    "content": ""
  },
  {
    "path": "price_monitor/app_settings.py",
    "content": "from django.conf import settings\nfrom django.utils.translation import ugettext_lazy\n\n\n# global AWS access settings\nPRICE_MONITOR_AWS_ACCESS_KEY_ID = getattr(settings, 'PRICE_MONITOR_AWS_ACCESS_KEY_ID', '')\nPRICE_MONITOR_AWS_SECRET_ACCESS_KEY = getattr(settings, 'PRICE_MONITOR_AWS_SECRET_ACCESS_KEY', '')\nPRICE_MONITOR_AMAZON_PRODUCT_API_REGION = getattr(settings, 'PRICE_MONITOR_AMAZON_PRODUCT_API_REGION', '')\nPRICE_MONITOR_AMAZON_PRODUCT_API_ASSOC_TAG = getattr(settings, 'PRICE_MONITOR_AMAZON_PRODUCT_API_ASSOC_TAG', '')\nPRICE_MONITOR_AMAZON_ASSOCIATE_NAME = getattr(settings, 'PRICE_MONITOR_AMAZON_ASSOCIATE_NAME', '<ADJUST settings.PRICE_MONITOR_AMAZON_ASSOCIATE_NAME>')\n\n# project settings\nPRICE_MONITOR_BASE_URL = getattr(settings, 'PRICE_MONITOR_BASE_URL', 'http://localhost:8000')\n\n# Amazon Disclaimers\n# Disclaimer for Product Advertising API, see https://partnernet.amazon.de/gp/advertising/api/detail/agreement.html and #12\nPRICE_MONITOR_AMAZON_PRODUCT_ADVERTISING_API_DISCLAIMER = 'CERTAIN CONTENT THAT APPEARS ON THIS SITE COMES FROM AMAZON EU S.à r.l. THIS CONTENT IS ' \\\n    'PROVIDED \\'AS IS\\' AND IS SUBJECT TO CHANGE OR REMOVAL AT ANY TIME.'\n# Disclaimer for Amazon associates, see https://partnernet.amazon.de/gp/associates/agreement (10) and #77\n# available sites for disclaimer text, just as reference, unused in code\nPRICE_MONITOR_AMAZON_ASSOCIATE_SITES = [\n    'Amazon.co.uk',\n    'Local.Amazon.co.uk',\n    'Amazon.de',\n    'de.BuyVIP.com',\n    'Amazon.fr',\n    'Amazon.it',\n    'it.BuyVIP.com',\n    'Amazon.es',\n    'es.BuyVIP.com',\n]\nPRICE_MONITOR_AMAZON_ASSOCIATE_SITE = getattr(settings, 'PRICE_MONITOR_AMAZON_ASSOCIATE_SITE', '<ADJUST settings.PRICE_MONITOR_AMAZON_ASSOCIATE_SITE>')\n# Disclaimer for Amazon associates, see https://partnernet.amazon.de/gp/associates/agreement (10) and #77\nPRICE_MONITOR_ASSOCIATE_DISCLAIMER = '{name} is a participant in the Amazon EU Associates Programme, an affiliate advertising programme designed to provide' \\\n    ' a means for sites to earn advertising fees by advertising and linking to {amazon_site_name}.'.format(\n        name=PRICE_MONITOR_AMAZON_ASSOCIATE_NAME,\n        amazon_site_name=PRICE_MONITOR_AMAZON_ASSOCIATE_SITE,\n    )\n\n# server infrastructural settings\n# serve the product images via HTTPS\nPRICE_MONITOR_IMAGES_USE_SSL = getattr(settings, 'PRICE_MONITOR_IMAGES_USE_SSL', True)\n# HTTPS host to use for getting the images. Seems to be https://images-<REGION>.ssl-images-amazon.com.\nPRICE_MONITOR_AMAZON_SSL_IMAGE_DOMAIN = getattr(settings, 'PRICE_MONITOR_AMAZON_SSL_IMAGE_DOMAIN', 'https://images-eu.ssl-images-amazon.com')\n\n# synchronization settings\n# refresh product after 12 hours\nPRICE_MONITOR_AMAZON_PRODUCT_REFRESH_THRESHOLD_MINUTES = int(getattr(settings, 'PRICE_MONITOR_AMAZON_PRODUCT_REFRESH_THRESHOLD_MINUTES', 12 * 60))\n\n# notification settings\n# time after when to notify about a subscription again\nPRICE_MONITOR_SUBSCRIPTION_RENOTIFICATION_MINUTES = int(getattr(settings, 'PRICE_MONITOR_SUBSCRIPTION_RENOTIFICATION_MINUTES', 60 * 24 * 7))\n# the email sender for notification emails\nPRICE_MONITOR_EMAIL_SENDER = getattr(settings, 'PRICE_MONITOR_EMAIL_SENDER', 'noreply@localhost')\n# default currency\nPRICE_MONITOR_DEFAULT_CURRENCY = getattr(settings, 'PRICE_MONITOR_DEFAULT_CURRENCY', 'EUR')\n# i18n for email notifications\nPRICE_MONITOR_I18N_EMAIL_NOTIFICATION_SUBJECT = getattr(\n    settings,\n    'PRICE_MONITOR_I18N_EMAIL_NOTIFICATION_SUBJECT',\n    ugettext_lazy('Price limit for %(product)s reached')\n)\nPRICE_MONITOR_I18N_EMAIL_NOTIFICATION_BODY = getattr(\n    settings,\n    'PRICE_MONITOR_I18N_EMAIL_NOTIFICATION_BODY',\n    ugettext_lazy(\n        'The price limit of {price_limit:0.2f} {currency:s} has been reached for the article \"{product_title:s}\"\\n'\n        'Current price is {price:0.2f} {currency:s} ({price_date:s}).'\n        '\\n\\n'\n        'Please support our platform by using this affiliate link for buying the product: {url_product_amazon:s}'\n        '\\n'\n        'Adjust the price limits for the products here: {url_product_detail:s}'\n        '\\n\\n'\n        '{additional_text:s}'\n        '\\n'\n        'Regards,'\n        '\\n'\n        'The Team'\n    )\n)\nPRICE_MONITOR_SITENAME = getattr(settings, 'PRICE_MONITOR_SITENAME', 'Price Monitor')\n\n# cache settings\n# key of cache (according to project config) to use for graphs. Set to none to disable caching\nPRICE_MONITOR_GRAPH_CACHE_NAME = getattr(settings, 'PRICE_MONITOR_GRAPH_CACHE_NAME', None)\n# prefix for cache key used for graphs\nPRICE_MONITOR_GRAPH_CACHE_KEY_PREFIX = getattr(settings, 'PRICE_MONITOR_GRAPH_CACHE_KEY_PREFIX', 'graph_')\n\n\n# internal settings - not to be overwritten by user\n# Regex for ASIN validation\nPRICE_MONITOR_ASIN_REGEX = r'[A-Z0-9\\-]+'\n# Product Advertising API relevant settings\n# TODO is there a possibility to only get get attributes we need? I have the feeling that 75% of the data is irrelevant for us.\nPRICE_MONITOR_PA_RESPONSE_GROUP = 'Large'\n# mapping of PRICE_MONITOR_AMAZON_PRODUCT_API_REGION to the appropriate amazon domain ending\nPRICE_MONITOR_AMAZON_REGION_DOMAINS = {\n    'CA': 'ca',\n    'DE': 'de',\n    'ES': 'es',\n    'FR': 'fr',\n    'IN': 'in',\n    'IT': 'it',\n    'JP': 'co.jp',\n    'UK': 'co.uk',\n    'US': 'com',\n}\nPRICE_MONITOR_OFFER_URL = 'http://www.amazon.{domain:s}/dp/{asin:s}/?tag={assoc_tag:s}'\n"
  },
  {
    "path": "price_monitor/forms.py",
    "content": "\"\"\"Form definitions for frontend\"\"\"\nfrom . import app_settings as settings\nfrom .models.EmailNotification import EmailNotification\nfrom .models.Product import Product\nfrom .models.Subscription import Subscription\n\nfrom django import forms\nfrom django.utils.translation import ugettext as _\n\n\nclass SubscriptionCreationForm(forms.ModelForm):\n\n    \"\"\"Form for creating an product Subscription\"\"\"\n\n    product = forms.RegexField(label=_('ASIN'), regex=settings.PRICE_MONITOR_ASIN_REGEX)\n    email_notification = forms.ModelChoiceField(queryset=EmailNotification.objects.all(), empty_label=None)\n\n    def clean_product(self):\n        \"\"\"\n        At creation, user gives an ASIN. But for saving the model, a product instance is needed.\n\n        So this product is looked up or created if not present here.\n        \"\"\"\n        asin = self.cleaned_data['product']\n        try:\n            product = Product.objects.get(asin__iexact=asin)\n        except Product.DoesNotExist:\n            product = Product.objects.create(asin=asin)\n        asin = product\n        return asin\n\n    class Meta(object):\n\n        \"\"\"Form meta stuff\"\"\"\n\n        fields = ('product', 'email_notification', 'price_limit', 'owner')\n        model = Subscription\n        widgets = {\n            'owner': forms.HiddenInput(),\n        }\n\n\nclass SubscriptionUpdateForm(forms.ModelForm):\n\n    \"\"\"Form for updating a subscription\"\"\"\n\n    class Meta(object):\n\n        \"\"\"Form meta stuff\"\"\"\n\n        fields = ('product', 'email_notification', 'price_limit', 'owner')\n        model = Subscription\n        widgets = {\n            'owner': forms.HiddenInput(),\n            'product': forms.TextInput(attrs={'readonly': True}),\n        }\n\n\nclass EmailNotificationForm(forms.ModelForm):\n\n    \"\"\"Form for giving an email notification\"\"\"\n\n    class Meta(object):\n\n        \"\"\"Form meta stuff\"\"\"\n\n        fields = ('email', 'owner')\n        model = EmailNotification\n        widgets = {\n            'owner': forms.HiddenInput(),\n        }\n"
  },
  {
    "path": "price_monitor/locale/de/LC_MESSAGES/django.po",
    "content": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER\n# This file is distributed under the same license as the PACKAGE package.\n# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.\n#\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: 1\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"POT-Creation-Date: 2015-10-28 19:33+0100\\n\"\n\"PO-Revision-Date: 2015-10-28 19:42+0100\\n\"\n\"Last-Translator: Alexander Herrmann <darignac@gmail.com>\\n\"\n\"Language-Team: Deutsch <darignac@gmail.com>\\n\"\n\"Language: Deutsch\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"X-Generator: Poedit 1.5.4\\n\"\n\"X-Poedit-SourceCharset: UTF-8\\n\"\n\n#: admin.py:27\nmsgid \"Reset to status \\\"Created\\\".\"\nmsgstr \"Auf Status \\\"Erstellt\\\" zurücksetzen.\"\n\n#: admin.py:38\nmsgid \"Resynchronize with API\"\nmsgstr \"Mit API resynchronisieren\"\n\n#: app_settings.py:32\n#, python-format\nmsgid \"Price limit for %(product)s reached\"\nmsgstr \"Preislimit für %(product)s erreicht\"\n\n#: app_settings.py:38\n#, python-format\nmsgid \"\"\n\"The price limit of %(price_limit)0.2f %(currency)s has been reached for the \"\n\"article \\\"%(product_title)s\\\" - the current price is %(price)0.2f \"\n\"%(currency)s.\\n\"\n\"\\n\"\n\"Please support our platform by using this link for buying: %(link)s\\n\"\n\"\\n\"\n\"\\n\"\n\"Regards,\\n\"\n\"The Team\"\nmsgstr \"\"\n\"Das Preislimit von %(price_limit)0.2f %(currency)s wurde für den Artikel \"\n\"\\\"%(product_title)s\\\" erreicht - der aktuelle Preis ist %(price)0.2f \"\n\"%(currency)s.\\n\"\n\"\\n\"\n\"Bitte unterstütze unsere Plattform, indem du den Artikel über diesen Link \"\n\"einkaufst: %(link)s\\n\"\n\"\\n\"\n\"\\n\"\n\"Grüße,\\n\"\n\"Das Team\"\n\n#: forms.py:14 models/Product.py:40\nmsgid \"ASIN\"\nmsgstr \"ASIN\"\n\n#: models/EmailNotification.py:14 models/Subscription.py:12\nmsgid \"Owner\"\nmsgstr \"Besitzer\"\n\n#: models/EmailNotification.py:15\nmsgid \"Email address\"\nmsgstr \"E-Mail-Adresse\"\n\n#: models/EmailNotification.py:31 models/Subscription.py:16\nmsgid \"Email Notification\"\nmsgstr \"E-Mail-Benachrichtigung\"\n\n#: models/EmailNotification.py:32\nmsgid \"Email Notifications\"\nmsgstr \"E-Mail-Benachrichtigungen\"\n\n#: models/Price.py:9 models/Price.py:29\nmsgid \"Price\"\nmsgstr \"Preis\"\n\n#: models/Price.py:10\nmsgid \"Currency\"\nmsgstr \"Währung\"\n\n#: models/Price.py:11\nmsgid \"Date of price\"\nmsgstr \"Datum des Preises\"\n\n#: models/Price.py:12 models/Product.py:127 models/Subscription.py:13\nmsgid \"Product\"\nmsgstr \"Produkt\"\n\n#: models/Price.py:30\nmsgid \"Prices\"\nmsgstr \"Preise\"\n\n#: models/Product.py:23\nmsgid \"Created\"\nmsgstr \"Erstellt\"\n\n#: models/Product.py:24\nmsgid \"Synced over API\"\nmsgstr \"über API synchronisiert\"\n\n#: models/Product.py:25\nmsgid \"Unsynchable\"\nmsgstr \"nicht synchronisierbar\"\n\n#: models/Product.py:29\nmsgid \"Date of creation\"\nmsgstr \"Erstellungsdatum\"\n\n#: models/Product.py:30\nmsgid \"Date of last update\"\nmsgstr \"Datum der letzten Aktualisierung\"\n\n#: models/Product.py:31\nmsgid \"Date of last synchronization\"\nmsgstr \"Datum der letzten Synchronisierung\"\n\n#: models/Product.py:34\nmsgid \"Status\"\nmsgstr \"Status\"\n\n#: models/Product.py:37\nmsgid \"Subscribers\"\nmsgstr \"Abonnenten\"\n\n#: models/Product.py:41\nmsgid \"Title\"\nmsgstr \"Titel\"\n\n#: models/Product.py:42\nmsgid \"Artist\"\nmsgstr \"Künstler\"\n\n#: models/Product.py:43\nmsgid \"ISBN\"\nmsgstr \"ISBN\"\n\n#: models/Product.py:44\nmsgid \"E-ISBN\"\nmsgstr \"E-ISBN\"\n\n#: models/Product.py:45\nmsgid \"Binding\"\nmsgstr \"Bindung\"\n\n#: models/Product.py:46\nmsgid \"Publication date\"\nmsgstr \"Erscheinungsdatum\"\n\n#: models/Product.py:47\nmsgid \"Release date\"\nmsgstr \"Freigabedatum\"\n\n#: models/Product.py:48\nmsgid \"Audience rating\"\nmsgstr \"Zielgruppe\"\n\n#: models/Product.py:49\nmsgid \"URL to large product image\"\nmsgstr \"URL zum großen Produktbild\"\n\n#: models/Product.py:50\nmsgid \"URL to medium product image\"\nmsgstr \"URL zum mittleren Produktbild\"\n\n#: models/Product.py:51\nmsgid \"URL to small product image\"\nmsgstr \"URL zum kleinen Produktbild\"\n\n#: models/Product.py:52\nmsgid \"URL to the offer\"\nmsgstr \"URL zum Angebot\"\n\n#: models/Product.py:54\n#| msgid \"Currency\"\nmsgid \"Current price\"\nmsgstr \"Aktueller Preis\"\n\n#: models/Product.py:55\nmsgid \"Highest price ever\"\nmsgstr \"Höchster Preis\"\n\n#: models/Product.py:56\nmsgid \"Lowest price ever\"\nmsgstr \"Niedrigster Preis\"\n\n#: models/Product.py:114\nmsgid \"Unsynchronized Product\"\nmsgstr \"Nicht synchronisiertes Produkt\"\n\n#: models/Product.py:128\nmsgid \"Products\"\nmsgstr \"Produkte\"\n\n#: models/Subscription.py:14\nmsgid \"Price limit\"\nmsgstr \"Preislimit\"\n\n#: models/Subscription.py:15\nmsgid \"Date of last sent notification\"\nmsgstr \"Datum der zuletzt gesendeten Benachrichtigung\"\n\n#: models/Subscription.py:24\nmsgid \"Notification email\"\nmsgstr \"Benachrichtigungs-E-Mail\"\n\n#: models/Subscription.py:39\nmsgid \"Subscription\"\nmsgstr \"Abonnement\"\n\n#: models/Subscription.py:40\nmsgid \"Subscriptions\"\nmsgstr \"Abonnements\"\n\n#: models/mixins/PublicIDMixin.py:17\nmsgid \"Public-ID\"\nmsgstr \"Public-ID\"\n\n#~ msgid \"Email notifications\"\n#~ msgstr \"E-Mail-Benachrichtigungen\"\n\n#~ msgid \"Add new email notifications\"\n#~ msgstr \"Neue E-Mail-Benachrichtigungen hinzufügen\"\n\n#~ msgid \"Already monitored products\"\n#~ msgstr \"Bereits überwachte Produkte\"\n\n#~ msgid \"\"\n#~ \"Product prices and availability are accurate as of the date/time \"\n#~ \"indicated and are subject to change. Any price and availability \"\n#~ \"information displayed on %(site_name)s at the time of purchase will apply \"\n#~ \"to the purchase of this product.\"\n#~ msgstr \"\"\n#~ \"Produktpreise und -verfügbarkeit gelten zur angezeigten Zeit und können \"\n#~ \"sich ändern. Jede Preis- und Verügbarkeitsinformation, die auf \"\n#~ \"%(site_name)s zum Zeitpunkt der Bestellung angezeigt werden, treffen auch \"\n#~ \"für den Kauf des Produktes zu.\"\n\n#~ msgid \"Details\"\n#~ msgstr \"Details\"\n\n#~ msgid \"No price information available.\"\n#~ msgstr \"Keine Preisinformationen verfügbar.\"\n\n#~ msgid \"Limit\"\n#~ msgstr \"Limit\"\n\n#~ msgid \"Remove\"\n#~ msgstr \"Entfernen\"\n\n#~ msgid \"Add new products\"\n#~ msgstr \"Neue Produkte hinzufügen\"\n\n#~ msgid \"Monitor ASINs\"\n#~ msgstr \"ASINs überwachen\"\n\n#~ msgid \"No price data available.\"\n#~ msgstr \"Keine Preisdaten verfügbar.\"\n\n#~ msgid \"at\"\n#~ msgstr \"um\"\n\n#~ msgid \"Please specify a list of ASINs as only argument, separated by comma!\"\n#~ msgstr \"\"\n#~ \"Bitte gib eine Liste von kommaseparierten ASINs als einziges Argument an!\"\n\n#~ msgid \"Please specify a single ASIN as only argument!\"\n#~ msgstr \"Bitte gib eine einzige ASIN als einziges Argument an!\"\n"
  },
  {
    "path": "price_monitor/management/__init__.py",
    "content": ""
  },
  {
    "path": "price_monitor/management/commands/__init__.py",
    "content": ""
  },
  {
    "path": "price_monitor/management/commands/price_monitor_batch_create_products.py",
    "content": "\"\"\"Management command for batch reation of products\"\"\"\nfrom django.core.management.base import BaseCommand\n\nfrom price_monitor.models import Product\n\n\nclass Command(BaseCommand):\n\n    \"\"\"Command for batch creating of products.\"\"\"\n\n    help = 'Creates multiple products from the given ASIN list. Skips products already in database.'\n\n    def add_arguments(self, parser):\n        \"\"\"\n        Adds the positional argument for ASINs\n\n        :param parser: the argument parser\n        \"\"\"\n        parser.add_argument('asins', nargs='+', type=str)\n\n    def handle(self, *args, **options):\n        \"\"\"Batch create products from given ASIN list.\"\"\"\n        # get all products with given asins\n        product_asins = [p.asin for p in Product.objects.filter(asin__in=options['asins'])]\n\n        # remove the asins that are already there\n        asins = [a for a in options['asins'] if a not in product_asins]\n\n        # create some products\n        for asin in asins:\n            Product.objects.create(asin=asin)\n\n        print('created {0:d} products'.format(len(asins)))\n"
  },
  {
    "path": "price_monitor/management/commands/price_monitor_clean_db.py",
    "content": "\"\"\"Management command for removing invalid data from database\"\"\"\nfrom django.core.management.base import BaseCommand\n\nfrom price_monitor.models import (\n    Price,\n    Product,\n)\n\n\nclass Command(BaseCommand):\n\n    \"\"\"Command for cleaning the database. Deletes all products without subscriptions.\"\"\"\n\n    help = 'Deletes all products without subscriptions'\n\n    def handle(self, *args, **options):\n        \"\"\"Deletes the products without subscriptions.\"\"\"\n        products_without_subscribers = Product.objects.filter(subscribers__isnull=True)\n        prices_without_subscribers = Price.objects.filter(product__subscribers__isnull=True)\n\n        print('=== PRE-CLEANUP ==================================')\n        print('Product count:                {0:20d}'.format(Product.objects.count()))\n        print('Products with subscribers:    {0:20d}'.format(Product.objects.filter(subscribers__isnull=False).count()))\n        print('Products without subscribers: {0:20d}'.format(products_without_subscribers.count()))\n        print('Prices count:                 {0:20d}'.format(Price.objects.count()))\n        print('==================================================')\n        print('')\n\n        choice = input(\n            '{0:d} products with {1:d} prices will be deleted, continue? [y/N]'.format(\n                products_without_subscribers.count(),\n                prices_without_subscribers.count()\n            )\n        )\n\n        if choice in ['y', 'Y']:\n            products_without_subscribers.delete()\n            prices_without_subscribers.delete()\n            print('')\n            print('DONE')\n"
  },
  {
    "path": "price_monitor/management/commands/price_monitor_recreate_product.py",
    "content": "\"\"\"Management command for recreating a product\"\"\"\nfrom django.core.management.base import BaseCommand\n\nfrom price_monitor.models import Product\n\n\nclass Command(BaseCommand):\n    help = 'Recreates a product with the given asin. If product already exists, it is deleted.'\n\n    def add_arguments(self, parser):\n        \"\"\"\n        Adds the positional argument for ASIN\n\n        :param parser: the argument parser\n        \"\"\"\n        parser.add_argument('asin', nargs=1, type=str)\n\n    def handle(self, *args, **options):\n        \"\"\"Recreates the product with given ASIN\"\"\"\n        asin = options['asin'][0]\n        product, created = Product.objects.get_or_create(asin=asin)\n        if not created:\n            product.delete()\n            Product.objects.create(asin=asin)\n"
  },
  {
    "path": "price_monitor/management/commands/price_monitor_search.py",
    "content": "\"\"\"Management command for searching Amazon\"\"\"\nfrom django.core.management.base import BaseCommand\n\nfrom price_monitor.product_advertising_api.api import ProductAdvertisingAPI\n\nfrom pprint import pprint\n\n\nclass Command(BaseCommand):\n\n    \"\"\"Command for searching ASINs and displaying their return value\"\"\"\n\n    help = 'Searches for products at Amazon (not within the database!) with the given ASINs and prints out their details.'\n\n    def add_arguments(self, parser):\n        \"\"\"\n        Adds the positional argument for ASINs.\n\n        :param parser: the argument parser\n        \"\"\"\n        parser.add_argument('asins', nargs='+', type=str)\n\n    def handle(self, *args, **options):\n        \"\"\"Searches for a product with the given ASIN.\"\"\"\n        asins = options['asins']\n        api = ProductAdvertisingAPI()\n        pprint(api.item_lookup(asins), indent=4)\n"
  },
  {
    "path": "price_monitor/management/commands/price_monitor_send_test_mail.py",
    "content": "\"\"\"Management command for sending a pricemonitor specific test email\"\"\"\nfrom datetime import datetime\n\nfrom django.contrib.auth.models import User\nfrom django.core.management.base import BaseCommand\n\nfrom price_monitor.models import (\n    EmailNotification,\n    Price,\n    Product,\n    Subscription,\n)\nfrom price_monitor.utils import send_mail\n\n\nclass Command(BaseCommand):\n\n    \"\"\"Command for sending a pricemonitor specific test email\"\"\"\n\n    help = 'Sends a pricemonitor specific test email'\n\n    def add_arguments(self, parser):\n        \"\"\"\n        Adds the positional argument for the email address.\n\n        :param parser: the argument parser\n        \"\"\"\n        parser.add_argument('email', nargs='+', type=str)\n\n    def handle(self, *args, **options):\n        \"\"\"Sends an email.\"\"\"\n\n        u = User()\n\n        e = EmailNotification()\n        e.owner = u\n        e.email = options['email'][0]\n\n        p = Product()\n        p.asin = 'ASIN123'\n        p.title = 'Dummy Product'\n        p.offer_url = 'http://localhost/offer'\n\n        s = Subscription()\n        s.price_limit = 9.99\n        s.email_notification = e\n\n        r = Price()\n        r.value = 8.00\n        r.currency = 'EUR'\n        r.date_seen = datetime.now()\n        r.product = p\n\n        send_mail(\n            p,\n            s,\n            r,\n            additional_text='This is a test email.'\n        )\n"
  },
  {
    "path": "price_monitor/migrations/0001_initial.py",
    "content": "# -*- coding: utf-8 -*-\nfrom __future__ import unicode_literals\n\nfrom django.db import models, migrations\nfrom django.conf import settings\n\n\nclass Migration(migrations.Migration):\n\n    dependencies = [\n        migrations.swappable_dependency(settings.AUTH_USER_MODEL),\n    ]\n\n    operations = [\n        migrations.CreateModel(\n            name='EmailNotification',\n            fields=[\n                ('id', models.AutoField(primary_key=True, verbose_name='ID', auto_created=True, serialize=False)),\n                ('public_id', models.CharField(db_index=True, unique=True, editable=False, verbose_name='Public-ID', max_length=36)),\n                ('email', models.EmailField(verbose_name='Email address', max_length=254)),\n                ('owner', models.ForeignKey(to=settings.AUTH_USER_MODEL, verbose_name='Owner')),\n            ],\n            options={\n                'ordering': ('email',),\n                'verbose_name': 'Email Notification',\n                'verbose_name_plural': 'Email Notifications',\n            },\n            bases=(models.Model,),\n        ),\n        migrations.CreateModel(\n            name='Price',\n            fields=[\n                ('id', models.AutoField(primary_key=True, verbose_name='ID', auto_created=True, serialize=False)),\n                ('value', models.FloatField(verbose_name='Price')),\n                ('currency', models.CharField(verbose_name='Currency', max_length=3)),\n                ('date_seen', models.DateTimeField(verbose_name='Date of price')),\n            ],\n            options={\n                'ordering': ('date_seen',),\n                'get_latest_by': 'date_seen',\n                'verbose_name': 'Price',\n                'verbose_name_plural': 'Prices',\n            },\n            bases=(models.Model,),\n        ),\n        migrations.CreateModel(\n            name='Product',\n            fields=[\n                ('id', models.AutoField(primary_key=True, verbose_name='ID', auto_created=True, serialize=False)),\n                ('date_creation', models.DateTimeField(verbose_name='Date of creation', auto_now_add=True)),\n                ('date_updated', models.DateTimeField(verbose_name='Date of last update', auto_now=True)),\n                ('date_last_synced', models.DateTimeField(null=True, verbose_name='Date of last synchronization', blank=True)),\n                ('status', models.SmallIntegerField(verbose_name='Status', choices=[(0, 'Created'), (1, 'Synced over API'), (2, 'Unsynchable')], default=0)),\n                ('asin', models.CharField(unique=True, verbose_name='ASIN', max_length=100)),\n                ('title', models.CharField(null=True, verbose_name='Title', blank=True, max_length=255)),\n                ('isbn', models.CharField(null=True, verbose_name='ISBN', blank=True, max_length=10)),\n                ('eisbn', models.CharField(null=True, verbose_name='E-ISBN', blank=True, max_length=13)),\n                ('binding', models.CharField(null=True, verbose_name='Binding', blank=True, max_length=255)),\n                ('date_publication', models.DateField(null=True, verbose_name='Publication date', blank=True)),\n                ('date_release', models.DateField(null=True, verbose_name='Release date', blank=True)),\n                ('audience_rating', models.CharField(null=True, verbose_name='Audience rating', blank=True, max_length=255)),\n                ('large_image_url', models.URLField(null=True, verbose_name='URL to large product image', blank=True)),\n                ('medium_image_url', models.URLField(null=True, verbose_name='URL to medium product image', blank=True)),\n                ('small_image_url', models.URLField(null=True, verbose_name='URL to small product image', blank=True)),\n                ('offer_url', models.URLField(null=True, verbose_name='URL to the offer', blank=True)),\n            ],\n            options={\n                'ordering': ('title', 'asin'),\n                'verbose_name': 'Product',\n                'verbose_name_plural': 'Products',\n            },\n            bases=(models.Model,),\n        ),\n        migrations.CreateModel(\n            name='Subscription',\n            fields=[\n                ('id', models.AutoField(primary_key=True, verbose_name='ID', auto_created=True, serialize=False)),\n                ('public_id', models.CharField(db_index=True, unique=True, editable=False, verbose_name='Public-ID', max_length=36)),\n                ('price_limit', models.FloatField(verbose_name='Price limit')),\n                ('date_last_notification', models.DateTimeField(null=True, verbose_name='Date of last sent notification', blank=True)),\n                ('email_notification', models.ForeignKey(to='price_monitor.EmailNotification', verbose_name='Email Notification')),\n                ('owner', models.ForeignKey(to=settings.AUTH_USER_MODEL, verbose_name='Owner')),\n                ('product', models.ForeignKey(to='price_monitor.Product', verbose_name='Product')),\n            ],\n            options={\n                'ordering': ('product__title', 'price_limit', 'email_notification__email'),\n                'verbose_name': 'Subscription',\n                'verbose_name_plural': 'Subscriptions',\n            },\n            bases=(models.Model,),\n        ),\n        migrations.AddField(\n            model_name='product',\n            name='subscribers',\n            field=models.ManyToManyField(to=settings.AUTH_USER_MODEL, verbose_name='Subscribers', through='price_monitor.Subscription'),\n            preserve_default=True,\n        ),\n        migrations.AddField(\n            model_name='price',\n            name='product',\n            field=models.ForeignKey(to='price_monitor.Product', verbose_name='Product'),\n            preserve_default=True,\n        ),\n    ]\n"
  },
  {
    "path": "price_monitor/migrations/0002_add_min_max_fk_to_product.py",
    "content": "# -*- coding: utf-8 -*-\nfrom __future__ import unicode_literals\n\nfrom django.db import models, migrations\n\n\nclass Migration(migrations.Migration):\n\n    dependencies = [\n        ('price_monitor', '0001_initial'),\n    ]\n\n    operations = [\n        migrations.AddField(\n            model_name='product',\n            name='highest_price',\n            field=models.ForeignKey(related_name='product_highest', to='price_monitor.Price', blank=True, verbose_name='Highest price ever', null=True),\n        ),\n        migrations.AddField(\n            model_name='product',\n            name='lowest_price',\n            field=models.ForeignKey(related_name='product_lowest', to='price_monitor.Price', blank=True, verbose_name='Lowest price ever', null=True),\n        ),\n        migrations.AddField(\n            model_name='product',\n            name='current_price',\n            field=models.ForeignKey(to='price_monitor.Price', null=True, blank=True, verbose_name='Current price', related_name='product_current'),\n        ),\n    ]\n"
  },
  {
    "path": "price_monitor/migrations/0003_datamigration_for_min_max_cur_fks.py",
    "content": "# -*- coding: utf-8 -*-\nfrom __future__ import unicode_literals\n\nfrom django.db import migrations\n\n\ndef set_prices(apps, schema_editor):\n    \"\"\"\n    Sets min, max and current price\n    \"\"\"\n    for product in apps.get_model('price_monitor', 'Product').objects.all():\n        if product.price_set.count() > 0:\n            product.current_price = product.price_set.latest('date_seen')\n            product.highest_price = product.price_set.latest('value')\n            product.lowest_price = product.price_set.earliest('value')\n            product.save()\n\n\nclass Migration(migrations.Migration):\n\n    dependencies = [\n        ('price_monitor', '0002_add_min_max_fk_to_product'),\n    ]\n\n    operations = [\n        migrations.RunPython(set_prices, reverse_code=migrations.RunPython.noop),\n    ]\n"
  },
  {
    "path": "price_monitor/migrations/0004_make_price_and_currency_nullable.py",
    "content": "# -*- coding: utf-8 -*-\nfrom __future__ import unicode_literals\n\nfrom django.db import models, migrations\n\n\nclass Migration(migrations.Migration):\n\n    dependencies = [\n        ('price_monitor', '0003_datamigration_for_min_max_cur_fks'),\n    ]\n\n    operations = [\n        migrations.AlterField(\n            model_name='price',\n            name='currency',\n            field=models.CharField(null=True, verbose_name='Currency', blank=True, max_length=3),\n        ),\n        migrations.AlterField(\n            model_name='price',\n            name='value',\n            field=models.FloatField(null=True, verbose_name='Price', blank=True),\n        ),\n    ]\n"
  },
  {
    "path": "price_monitor/migrations/0005_product_artist.py",
    "content": "# -*- coding: utf-8 -*-\nfrom __future__ import unicode_literals\n\nfrom django.db import models, migrations\n\n\nclass Migration(migrations.Migration):\n\n    dependencies = [\n        ('price_monitor', '0004_make_price_and_currency_nullable'),\n    ]\n\n    operations = [\n        migrations.AddField(\n            model_name='product',\n            name='artist',\n            field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Artist'),\n        ),\n    ]\n"
  },
  {
    "path": "price_monitor/migrations/__init__.py",
    "content": ""
  },
  {
    "path": "price_monitor/models/EmailNotification.py",
    "content": "\"\"\"Model for an email based notification\"\"\"\nfrom .mixins.PublicIDMixin import PublicIDMixin\n\nfrom django.conf import settings\nfrom django.db import models\nfrom django.utils.translation import ugettext as _, ugettext_lazy\n\nfrom six import text_type\n\n\nclass EmailNotification(PublicIDMixin, models.Model):\n\n    \"\"\"An email notification.\"\"\"\n\n    owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, verbose_name=_('Owner'))\n    email = models.EmailField(verbose_name=_('Email address'))\n\n    def __str__(self):\n        \"\"\"\n        Returns the unicode representation of the EmailNotification.\n\n        :return: the unicode representation\n        :rtype: unicode\n        \"\"\"\n        return text_type(\n            ' {email!s}'.format(**{\n                'email': self.email,\n            })\n        )\n\n    class Meta(object):\n\n        \"\"\"Meta Peter or how to configure your Django model\"\"\"\n\n        app_label = 'price_monitor'\n        verbose_name = ugettext_lazy('Email Notification')\n        verbose_name_plural = ugettext_lazy('Email Notifications')\n        ordering = ('email',)\n"
  },
  {
    "path": "price_monitor/models/Price.py",
    "content": "\"\"\"Definition of a model for prices\"\"\"\nfrom django.db import models\nfrom django.utils.translation import ugettext as _, ugettext_lazy\n\n\nclass Price(models.Model):\n\n    \"\"\"Representing fetched price for a product\"\"\"\n\n    value = models.FloatField(verbose_name=_('Price'), blank=True, null=True)\n    currency = models.CharField(max_length=3, verbose_name=_('Currency'), blank=True, null=True)\n    date_seen = models.DateTimeField(verbose_name=_('Date of price'))\n    product = models.ForeignKey('Product', on_delete=models.CASCADE, verbose_name=_('Product'))\n\n    def __str__(self):\n        \"\"\"\n        Returns the string representation of the Product.\n\n        :return: the unicode representation\n        :rtype: unicode\n        \"\"\"\n        return '{value!s} {currency!s} on {date_seen!s}'.format(**dict(\n            value='{0:0.2f}'.format(self.value) if self.value else 'No price',\n            currency=self.currency if self.currency else '',\n            date_seen=self.date_seen\n        ))\n\n    class Meta(object):\n        app_label = 'price_monitor'\n        get_latest_by = 'date_seen'\n        verbose_name = ugettext_lazy('Price')\n        verbose_name_plural = ugettext_lazy('Prices')\n        ordering = ('date_seen',)\n"
  },
  {
    "path": "price_monitor/models/Product.py",
    "content": "\"\"\"Model for an Amazon product\"\"\"\nfrom django.conf import settings\nfrom django.db import models\nfrom django.utils import formats\nfrom django.utils.translation import (\n    ugettext as _,\n    ugettext_lazy,\n)\n\nfrom price_monitor import app_settings\nfrom price_monitor.models.Price import Price\n\nfrom urllib.parse import (\n    urljoin,\n    urlparse,\n)\n\n\nclass Product(models.Model):\n\n    \"\"\"Product to be monitored.\"\"\"\n\n    STATUS_CHOICES = (\n        (0, _('Created'),),\n        (1, _('Synced over API'),),\n        (2, _('Unsynchable'),),\n    )\n\n    # date values\n    date_creation = models.DateTimeField(auto_now_add=True, verbose_name=_('Date of creation'))\n    date_updated = models.DateTimeField(auto_now=True, verbose_name=_('Date of last update'))\n    date_last_synced = models.DateTimeField(blank=True, null=True, verbose_name=_('Date of last synchronization'))\n\n    # synchronization status\n    status = models.SmallIntegerField(choices=STATUS_CHOICES, default=0, verbose_name=_('Status'))\n\n    # relations\n    subscribers = models.ManyToManyField(settings.AUTH_USER_MODEL, through='Subscription', verbose_name=_('Subscribers'))\n\n    # amazon specific fields\n    asin = models.CharField(max_length=100, unique=True, verbose_name=_('ASIN'))\n    title = models.CharField(blank=True, null=True, max_length=255, verbose_name=_('Title'))\n    artist = models.CharField(blank=True, null=True, max_length=255, verbose_name=_('Artist'))\n    isbn = models.CharField(blank=True, null=True, max_length=10, verbose_name=_('ISBN'))\n    eisbn = models.CharField(blank=True, null=True, max_length=13, verbose_name=_('E-ISBN'))\n    binding = models.CharField(blank=True, null=True, max_length=255, verbose_name=_('Binding'))\n    date_publication = models.DateField(blank=True, null=True, verbose_name=_('Publication date'))\n    date_release = models.DateField(blank=True, null=True, verbose_name=_('Release date'))\n    audience_rating = models.CharField(blank=True, null=True, max_length=255, verbose_name=_('Audience rating'))\n    large_image_url = models.URLField(blank=True, null=True, verbose_name=_('URL to large product image'))\n    medium_image_url = models.URLField(blank=True, null=True, verbose_name=_('URL to medium product image'))\n    small_image_url = models.URLField(blank=True, null=True, verbose_name=_('URL to small product image'))\n    offer_url = models.URLField(blank=True, null=True, verbose_name=_('URL to the offer'))\n\n    current_price = models.ForeignKey(Price, on_delete=models.CASCADE, blank=True, null=True, related_name='product_current', verbose_name=_('Current price'))\n    highest_price = models.ForeignKey(Price, on_delete=models.CASCADE, blank=True, null=True, related_name='product_highest',\n                                      verbose_name=_('Highest price ever'))\n    lowest_price = models.ForeignKey(Price, on_delete=models.CASCADE, blank=True, null=True, related_name='product_lowest', verbose_name=_('Lowest price ever'))\n\n    def get_prices_for_chart(self):\n        \"\"\"\n        Returns all prices of the product.\n\n        :return: list\n        \"\"\"\n        # TODO: be able to specify a range, like last 100 days\n        # TODO: don't select all prices, but a representative representation, like each 5th price aso\n        return [{'x': str(formats.date_format(p.date_seen, 'SHORT_DATETIME_FORMAT')), 'y': p.value} for p in self.price_set.all().order_by('date_seen')]\n\n    def set_failed_to_sync(self):\n        \"\"\"Marks the product as failed to sync. This happens if the Amazon API request for this product fails.\"\"\"\n        self.status = 2\n        self.save()\n\n    def get_image_urls(self):\n        \"\"\"\n        Returns all image urls as dictionary. The size is the key.\n\n        Respects HTTP/HTTPS configuration.\n        :return: image dict\n        :rtype: dict\n        \"\"\"\n        return {\n            'small': self.__get_image_url(self.small_image_url),\n            'medium': self.__get_image_url(self.medium_image_url),\n            'large': self.__get_image_url(self.large_image_url),\n        }\n\n    def __get_image_url(self, url):\n        \"\"\"\n        Returns the correct image url depending on the settings. Will either be a HTTP or HTTPS host.\n\n        :param url: the original (HTTP) image url\n        :return: the adjusted image url if SSL is enabled\n        \"\"\"\n        if app_settings.PRICE_MONITOR_IMAGES_USE_SSL:\n            return urljoin(app_settings.PRICE_MONITOR_AMAZON_SSL_IMAGE_DOMAIN, urlparse(url).path)\n\n        return url\n\n    def get_graph_cache_key(self):\n        \"\"\"\n        Returns cache key used for caching the price graph\n\n        :return: the cache key\n        :rtype:  str\n        \"\"\"\n        return 'graph-{0!s}-{1!s}'.format(self.asin, self.date_last_synced.isoformat() if self.date_last_synced is not None else '')\n\n    def get_title(self):\n        \"\"\"\n        Returns the title of the product.\n\n        :return: the title\n        :rtype: str\n        \"\"\"\n        return '{0}{1}'.format(\n            '{0}: '.format(self.artist) if self.artist is not None and len(self.artist) > 0 else '',\n            self.title if self.title is not None and len(self.title) > 0 else _('Unsynchronized Product'),\n        )\n\n    def get_detail_url(self):\n        \"\"\"\n        Returns the url to a product detail view.\n\n        As the frontend is AngularJS, we cannot use any Django reverse functionality.\n        :return: the link\n        \"\"\"\n        return '{base_url:s}/#/products/{asin:s}'.format(\n            base_url=app_settings.PRICE_MONITOR_BASE_URL,\n            asin=self.asin,\n        )\n\n    def __str__(self):\n        \"\"\"\n\n        Returns the unicode representation of the Product.\n        :return: the unicode representation\n        :rtype: unicode\n        \"\"\"\n        return '{0} (ASIN: {1})'.format(self.get_title(), self.asin)\n\n    class Meta(object):\n\n        \"\"\"Django meta config\"\"\"\n\n        app_label = 'price_monitor'\n        verbose_name = ugettext_lazy('Product')\n        verbose_name_plural = ugettext_lazy('Products')\n        ordering = ('title', 'asin',)\n"
  },
  {
    "path": "price_monitor/models/Subscription.py",
    "content": "\"\"\"The subscription model\"\"\"\nfrom .mixins.PublicIDMixin import PublicIDMixin\n\nfrom django.conf import settings\nfrom django.db import models\nfrom django.utils.translation import ugettext as _, ugettext_lazy\n\n\nclass Subscription(PublicIDMixin, models.Model):\n\n    \"\"\"Model for a user being able to subscribe to a product and be notified if the price_limit is reached.\"\"\"\n\n    owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, verbose_name=_('Owner'))\n    product = models.ForeignKey('Product', on_delete=models.CASCADE, verbose_name=_('Product'))\n    price_limit = models.FloatField(verbose_name=_('Price limit'))\n    date_last_notification = models.DateTimeField(null=True, blank=True, verbose_name=_('Date of last sent notification'))\n    email_notification = models.ForeignKey('EmailNotification', on_delete=models.CASCADE, verbose_name=_('Email Notification'))\n\n    def get_email_address(self):\n        \"\"\"\n        Returns the email address of the notification.\n\n        :return: string\n        \"\"\"\n        return self.email_notification.email\n\n    get_email_address.short_description = ugettext_lazy('Notification email')\n\n    def __str__(self):\n        \"\"\"\n        Returns the string representation of the Subscription.\n\n        :return: the unicode representation\n        :rtype: unicode\n        \"\"\"\n        return 'Subscription of \"{product!s}\" for {user!s}'.format(**{\n            'product': self.product.title,\n            'user': self.owner.username,\n        })\n\n    class Meta(object):\n\n        \"\"\"Meta stuff - you know what...\"\"\"\n\n        app_label = 'price_monitor'\n        verbose_name = ugettext_lazy('Subscription')\n        verbose_name_plural = ugettext_lazy('Subscriptions')\n        ordering = ('product__title', 'price_limit', 'email_notification__email',)\n"
  },
  {
    "path": "price_monitor/models/__init__.py",
    "content": "\"\"\"Base module for models that are in module entities. Sets all signal handlers\"\"\"\nimport os\n\nfrom django.db.models.signals import (\n    post_delete,\n    post_save,\n)\nfrom django.dispatch import receiver\n\nfrom price_monitor.models.EmailNotification import EmailNotification  # noqa\nfrom price_monitor.models.Price import Price  # noqa\nfrom price_monitor.models.Product import Product  # noqa\nfrom price_monitor.models.Subscription import Subscription  # noqa\n\n\n@receiver(post_save, sender=Product)\ndef synchronize_product_after_creation(sender, instance, created, **kwargs):  # pylint:disable=unused-argument\n    \"\"\"\n    Directly start synchronization of a Product after its creation.\n\n    :param sender: class calling the signal\n    :type sender: ModelBase\n    :param instance: the Product instance\n    :type instance: Product\n    :param created: if the Product was created\n    :type created: bool\n    :param kwargs: additional keywords arguments, see https://docs.djangoproject.com/en/dev/ref/signals/#django.db.models.signals.post_save\n    :type kwargs: dict\n    \"\"\"\n    if created and os.environ.get('STAGE', 'Live') != 'TravisCI':\n        from price_monitor.product_advertising_api.tasks import SynchronizeProductsTask\n        # have to delay the creation a when using angular via API somehow the product is not fully saved when the task is run thus it does not find the product\n        SynchronizeProductsTask.apply_async(([instance.asin],), countdown=1)\n\n\n@receiver(post_delete, sender=Subscription)\ndef cleanup_products_after_subscription_removal(sender, instance, using, **kwargs):  # pylint:disable=unused-argument\n    \"\"\"\n    Queues the execution of the ProductCleanupTask after a subscription was deleted.\n\n    :param sender: class calling the signal\n    :type sender: ModelBase\n    :param instance: the Subscription instance\n    :type instance: price_monitor.models.Subscription\n    :param using: database alias being used\n    :type using: str\n    :param kwargs: additional keywords arguments, see https://docs.djangoproject.com/en/dev/ref/signals/#django.db.models.signals.post_delete\n    :type kwargs: dict\n    \"\"\"\n    if os.environ.get('STAGE', 'Live') != 'TravisCI':\n        from price_monitor.tasks import ProductCleanupTask\n        ProductCleanupTask.delay(instance.product.asin)\n"
  },
  {
    "path": "price_monitor/models/mixins/PublicIDMixin.py",
    "content": "\"\"\"Mixin for having a public id.\"\"\"\nfrom django.db import models\nfrom django.utils.translation import ugettext as _\n\nfrom uuid import uuid4\n\n\nclass PublicIDMixin(models.Model):\n\n    \"\"\"Mixin for adding a public id to models to prevent revealing database ids via API\"\"\"\n\n    public_id = models.CharField(\n        max_length=36,\n        unique=True,\n        editable=False,\n        null=False,\n        db_index=True,\n        verbose_name=_('Public-ID')\n    )\n\n    def save(self, *args, **kwargs):\n        \"\"\"\n        Sets public id on new instances\n\n        :param args: positional arguments\n        :type args: list\n        :param kwargs: keyword arguments\n        :type kwargs: dict\n        :returns: what parent returns\n        :rtype: see parent\n        \"\"\"\n        if self.pk is None:\n            self.public_id = str(uuid4())\n        return super(PublicIDMixin, self).save(*args, **kwargs)\n\n    class Meta(object):\n\n        \"\"\"Meta stuff\"\"\"\n\n        abstract = True\n        app_label = 'price_monitor'\n"
  },
  {
    "path": "price_monitor/models/mixins/__init__.py",
    "content": ""
  },
  {
    "path": "price_monitor/product_advertising_api/__init__.py",
    "content": ""
  },
  {
    "path": "price_monitor/product_advertising_api/api.py",
    "content": "import bottlenose\nimport logging\nimport random\nimport time\n\nfrom bs4 import BeautifulSoup\n\nfrom dateutil import parser\n\nfrom price_monitor import (\n    app_settings,\n    utils,\n)\n\nfrom urllib.error import HTTPError\n\n\nlogger = logging.getLogger('price_monitor.product_advertising_api')\n\n\nclass ProductAdvertisingAPI(object):\n    \"\"\"\n    A wrapper class for the necessary Amazon Product Advertising API calls.\n    See the API reference here: http://docs.aws.amazon.com/AWSECommerceService/latest/DG/CHAP_ApiReference.html\n    See bottlenose here: https://github.com/lionheart/bottlenose\n    \"\"\"\n\n    def __init__(self):\n        self.__amazon = bottlenose.Amazon(\n            AWSAccessKeyId=app_settings.PRICE_MONITOR_AWS_ACCESS_KEY_ID,\n            AWSSecretAccessKey=app_settings.PRICE_MONITOR_AWS_SECRET_ACCESS_KEY,\n            AssociateTag=app_settings.PRICE_MONITOR_AMAZON_PRODUCT_API_ASSOC_TAG,\n            Region=app_settings.PRICE_MONITOR_AMAZON_PRODUCT_API_REGION,\n            Parser=lambda response_text: BeautifulSoup(response_text, 'lxml'),\n            ErrorHandler=ProductAdvertisingAPI.handle_error,\n        )\n\n    @staticmethod\n    def __get_item_attribute(item, attribute):\n        \"\"\"\n        Returns the attribute value from a bs4 parsed item.\n        :param item: bs4 item returned from PA API upon item lookup\n        :param attribute: the attribute to search for\n        :return: the value if found, else None\n        :rtype: basestring\n        \"\"\"\n        value = item.itemattributes.find_all(attribute, recursive=False)\n        return value[0].string if len(value) == 1 else None\n\n    @staticmethod\n    def format_datetime(value):\n        \"\"\"\n        Formats the given value if it is not None in the given format.\n        :param value: the value to format\n        :type value: basestring\n        :return: formatted datetime\n        :rtype: basestring\n        \"\"\"\n        if value is not None:\n            try:\n                return parser.parse(value)\n            except ValueError:\n                logger.error('Unable to parse %s to a datetime', value)\n                return None\n\n    @staticmethod\n    def handle_error(error):\n        \"\"\"\n        Generic error handler for bottlenose requests.\n        @see https://github.com/lionheart/bottlenose#error-handling\n        :param error: error information\n        :type error: dict\n        :return: if to retry the request\n        :rtype: bool\n        :\n        \"\"\"\n        ex = error['exception']\n\n        logger.error('Error upon requesting Amazon URL %s (Code: %s, Cache-URL: %s): %r', error['api_url'], error['cache_url'], ex, ex.code)\n\n        # try reconnect\n        if isinstance(ex, HTTPError) and ex.code == 503:\n            time.sleep(random.expovariate(0.1))\n            return True\n\n        return False\n\n    def lookup_at_amazon(self, item_ids):\n        \"\"\"\n        Outsourced this call to better mock in tests.\n        :param item_ids: the item ids\n        :type item_ids: list\n        :return: parsed xml\n        :rtype: bs4.BeautifulSoup\n        \"\"\"\n        return self.__amazon.ItemLookup(ItemId=','.join(item_ids), ResponseGroup=app_settings.PRICE_MONITOR_PA_RESPONSE_GROUP)\n\n    def item_lookup(self, item_ids):\n        \"\"\"\n        Lookup of the item with the given id on Amazon. Returns it values or None if something went wrong.\n        :param item_ids: the item ids\n        :type item_ids: list\n        :return: the values of the item\n        :rtype: dict\n        \"\"\"\n        logger.info('starting lookup for ASINs %s', ', '.join(item_ids))\n        item_response = self.lookup_at_amazon(item_ids)\n\n        if getattr(item_response, 'items') is None:\n            logger.error(\n                'Request for item lookup (ResponseGroup: %s, ASINs: %s) returned nothing',\n                app_settings.PRICE_MONITOR_PA_RESPONSE_GROUP,\n                ', '.join(item_ids),\n            )\n            return dict()\n\n        if item_response.items.request.isvalid.string == 'True':\n\n            # the dict that will contain a key for every ASIN and as value the parsed values\n            product_values = dict()\n\n            for item_node in item_response.find_all(['item']):\n\n                # parse the values\n                try:\n                    isbn = self.__get_item_attribute(item_node, 'isbn')\n                    eisbn = self.__get_item_attribute(item_node, 'eisbn')\n                    if eisbn is None and isbn is not None:\n                        if len(isbn) == 13:\n                            eisbn = isbn\n                            isbn = None\n\n                    item_values = {\n                        'asin': item_node.asin.string,\n                        'title': item_node.itemattributes.title.string,\n                        'artist': item_node.itemattributes.artist.string if item_node.itemattributes.artist is not None else None,\n                        'isbn': isbn,\n                        'eisbn': eisbn,\n                        'binding': item_node.itemattributes.binding.string,\n                        'date_publication': self.format_datetime(self.__get_item_attribute(item_node, 'publicationdate')),\n                        'date_release': self.format_datetime(self.__get_item_attribute(item_node, 'releasedate')),\n                        'large_image_url': item_node.largeimage.url.string if item_node.largeimage.url is not None else None,\n                        'medium_image_url': item_node.mediumimage.url.string if item_node.mediumimage.url is not None else None,\n                        'small_image_url': item_node.smallimage.url.string if item_node.smallimage.url is not None else None,\n                        'offer_url': utils.get_offer_url(item_node.asin.string),\n                        'audience_rating': self.__get_item_attribute(item_node, 'audiencerating'),\n                    }\n\n                    # check if there are offers, if so add price\n                    if item_node.offers is not None and int(item_node.offers.totaloffers.string) > 0:\n                        item_values['price'] = float(int(item_node.offers.offer.offerlisting.price.amount.string) / 100)\n                        item_values['currency'] = item_node.offers.offer.offerlisting.price.currencycode.string\n\n                    # insert into main dict\n                    product_values[item_values['asin']] = item_values\n                except AttributeError:\n                    raise\n                    logger.error('fetching item values from returned XML for ASIN %s failed', item_node.asin)\n\n            # check if all ASINs are included, if not write error message to log\n            failed_asins = []\n            for asin in item_ids:\n                if asin not in product_values.keys():\n                    failed_asins.append(asin)\n\n            if failed_asins:\n                logger.error('Lookup for the following ASINs failed: %s', ', '.join(failed_asins))\n\n            # if there is at least a single ASIN in the list, return the list, else None\n            return dict() if len(product_values) == 0 else product_values\n\n        else:\n            logger.error(\n                'Request for item lookup (ResponseGroup: %s, ASINs: %s) was not valid',\n                app_settings.PRICE_MONITOR_PA_RESPONSE_GROUP,\n                ', '.join(item_ids),\n            )\n            return dict()\n"
  },
  {
    "path": "price_monitor/product_advertising_api/tasks.py",
    "content": "# pylint: disable=unused-argument, arguments-differ\n\"\"\"Celery tasks for the Amazon Product Advertising API\"\"\"\nimport logging\n\nfrom celery import chord\nfrom celery.signals import celeryd_after_setup\nfrom celery.task import (\n    PeriodicTask,\n    Task,\n)\nfrom celery.task.control import (\n    inspect,\n    revoke,\n)\n\nfrom collections import (\n    Counter,\n    namedtuple,\n)\n\nfrom datetime import (\n    datetime,\n    timedelta,\n)\n\nfrom django.db.models import (\n    Min,\n    Q,\n)\nfrom django.utils import timezone\n\n# from django.utils.translation import ugettext\n\nfrom price_monitor import app_settings\nfrom price_monitor.models import (\n    Price,\n    Product,\n    Subscription,\n)\nfrom price_monitor.product_advertising_api.api import ProductAdvertisingAPI\nfrom price_monitor.utils import (\n    chunk_list,\n    send_mail,\n)\n\nfrom smtplib import SMTPServerDisconnected\n\n\nlogger = logging.getLogger('price_monitor.product_advertising_api')\n\n\n@celeryd_after_setup.connect\ndef celeryd_after_setup(*args, **kwargs):\n    \"\"\"\n    Called after the worker instances are set up.\n\n    Starts the StartupTask to get the whole synchronization started.\n    \"\"\"\n    StartupTask().apply_async(countdown=5)\n\n\nclass StartupTask(Task):\n\n    \"\"\"The task for getting the machinery up and running. As we do not use celery beat, we have to start somewhere.\"\"\"\n\n    ignore_result = True\n\n    def run(self):\n        logger.info('StartupTask was called')\n\n        # that's better than an simple tuple\n        task_repr = namedtuple('TaskRepresentation', 'id, name')\n\n        # fetch all currently queued task, map them to the TaskRepresentation tuple\n        scheduled_tasks = [task_repr(x['request']['id'], x['request']['name']) for x in list(inspect().scheduled().values())[0]]\n\n        # count how many FindProductsToSynchronizeTask are scheduled\n        count = dict(Counter([x.name for x in scheduled_tasks]).most_common())\n\n        # check if the FindProductsToSynchronizeTask is in and how often\n        if count and FindProductsToSynchronizeTask.name in count:\n            c = count[FindProductsToSynchronizeTask.name]\n        else:\n            c = 0\n\n        # if the task is not scheduled, do so\n        if c == 0:\n            logger.info('no FindProductsToSynchronizeTask is scheduled, now scheduling it')\n            FindProductsToSynchronizeTask().apply_async(countdown=5)\n\n        # put out logging info if the task is already scheduled\n        if c == 1:\n            logger.info('FindProductsToSynchronizeTask is already scheduled, skipping additional run')\n\n        # if the task is there more than once, remove it\n        # this has the potential to remove ALL scheduled FindProductsToSynchronizeTasks if the timing is \"bad\"\n        # however, the JumpStartTask will re-schedule the task if this happens (a workaround for a workaround - bad design by me btw.)\n        if c > 1:\n            logger.info('FindProductsToSynchronizeTask is already scheduled %d times, revoking %d', c, c - 1)\n\n            # revoke c-1 tasks - that means the task still stays in schedule but is removed and not executed when execution time is reached\n            for t in scheduled_tasks[1:]:\n                logger.info('revoking FindProductsToSynchronizeTask with id %s', t.id)\n                revoke(t.id)\n\n\nclass JumpStartTask(PeriodicTask):\n\n    \"\"\"\n    Task providing jump start to the FindProductsToSynchronizeTask.\n\n    It can happen that the FindProductsToSynchronizeTask does not reschedule itself. I don't know why. We do a workaround with this periodic tasks providing a\n    jumper cable to reschedule the task by queuing the StartupTask.\n    \"\"\"\n\n    run_every = timedelta(minutes=60)\n\n    def run(self):\n        \"\"\"\n        Does no more that to call the StartupTask in 5 seconds.\n\n        :return: always True\n        \"\"\"\n        logger.info('JumpStartTask was called')\n        StartupTask().apply_async(countdown=5)\n        return True\n\n\nclass FindProductsToSynchronizeTask(Task):\n\n    \"\"\"The tasks that finds the products that shall be updated through the api.\"\"\"\n\n    def run(self):\n        \"\"\"\n        Fetches the products to update via api. Queues a SynchronizeProductsTask and calls a new instance of itself after all\n        tasks are done. If no products found for update, sleeps until the next update time is reached.\n\n        :return: the result is always true\n        :rtype: bool\n        \"\"\"\n        logger.info('FindProductsToSynchronizeTask was called')\n\n        # get all products that shall be updated\n        products = self.__get_products_to_sync()\n\n        if products:\n            # chunk the products into 10 products each\n            products_chunked = list(chunk_list(list(products), 10))\n\n            logger.info('Starting chord for synchronization of %d products in %d chunks', len(products), len(products_chunked))\n\n            # after all single product synchronize tasks are done recall the FindProductsToSynchronizeTask. That is because we do not know how long it takes to\n            # synchronize the products and there can be new ones meanwhile. If the newly called task finds no products, it will handle the new callback to the\n            # correct time.\n            chord(\n                SynchronizeProductsTask().s([product.asin for product in product_list]) for product_list in products_chunked\n            )(\n                FindProductsToSynchronizeTask().si()\n            )\n        else:\n            logger.info('No products found to update now')\n            # One might think this may interfere with newly created products and their synchronization if they are added before the\n            # FindProductsToSynchronizeTask is called again, but it doesn't. The new product is updated on creation and the next synchronization is always\n            # after the next task call.\n            oldest_synchronization = Product.objects.filter(subscription__isnull=False, status__in=[0, 1]).aggregate(\n                Min('date_last_synced')\n            )['date_last_synced__min'] or datetime.now()\n            next_synchronization = oldest_synchronization + timedelta(minutes=app_settings.PRICE_MONITOR_AMAZON_PRODUCT_REFRESH_THRESHOLD_MINUTES)\n            logger.info('Eta for next FindProductsToSynchronizeTask run is %s', next_synchronization)\n            FindProductsToSynchronizeTask().apply_async(eta=next_synchronization)\n\n        return True\n\n    def __get_products_to_sync(self):\n        \"\"\"\n        Returns the products to synchronize.\n\n        These are newly created products with status \"0\" or products that are older than settings.PRICE_MONITOR_AMAZON_PRODUCT_REFRESH_THRESHOLD_MINUTES.\n        :return: list of products\n        :rtype: django.db.models.query.QuerySet\n        \"\"\"\n        # prefer already synced products over newly created\n        return Product.objects.select_related().filter(\n            subscription__isnull=False,\n            date_last_synced__lte=(timezone.now() - timedelta(minutes=app_settings.PRICE_MONITOR_AMAZON_PRODUCT_REFRESH_THRESHOLD_MINUTES)),\n            # issue #21 don't sync products that are  not existent\n            status__in=[0, 1]\n        )\n\n\nclass SynchronizeProductsTask(Task):\n\n    \"\"\"Task for synchronizing a single product.\"\"\"\n\n    # limit to one task per second, limited by Amazon API\n    rate_limit = '1/s'\n\n    # if we use the product instances instead of asins we get an EncodeError(RuntimeError('maximum recursion depth exceeded',),) resulting in a\n    # billiard.exceptions.WorkerLostError:\n    def run(self, asin_list):\n        \"\"\"\n        Called by celery if task is being delayed.\n\n        :param asin_list: list of asins of the products to be sycnhronized with Amazon\n        :type  asin_list: list\n        \"\"\"\n        products = dict()\n        # fetch the product instances\n        for asin in asin_list:\n            try:\n                # do select_related for price values for reducing db queries\n                product = Product.objects.select_related('highest_price', 'lowest_price', 'current_price').get(asin=asin)\n            except Product.DoesNotExist:\n                logger.error('Product with ASIN %s could not be found.', asin)\n                continue\n\n            products[asin] = product\n\n        if not products:\n            logger.error('For the given ASINs %s no products where found!', ','.join(asin_list))\n            return True\n\n        logger.info('Synchronizing products with ItemIds %s', ', '.join(products.keys()))\n\n        # query Amazon and iterate over results to update values\n        for asin, amazon_data in ProductAdvertisingAPI().item_lookup(item_ids=list(products.keys())).items():\n            self.__sync_product(products[asin], amazon_data)\n\n        return True\n\n    def __sync_product(self, product, amazon_data):\n        \"\"\"\n        Synchronizes the given price_monitor.model.Product with the Amazon data.\n        :param product: the product to update\n        :type product: price_monitor.models.Product\n        :param amazon_data: the date from the amazon api\n        :type amazon_data: dict\n        \"\"\"\n        now = timezone.now()\n\n        # create the price\n        price = Price.objects.create(\n            value=amazon_data['price'] if 'price' in amazon_data else None,\n            currency=amazon_data['currency'] if 'currency' in amazon_data else None,\n            date_seen=now,\n            product=product,\n        )\n\n        product.current_price = price\n\n        if product.lowest_price is None or (price.value is not None and price.value <= product.lowest_price.value):\n            product.lowest_price = price\n\n        if product.highest_price is None or (price.value is not None and price.value >= product.highest_price.value):\n            product.highest_price = price\n\n        # remove the elements that are not a field in Product model\n        if 'price' in amazon_data:\n            amazon_data.pop('price')\n        if 'currency' in amazon_data:\n            amazon_data.pop('currency')\n\n        # update and save the product\n        product.__dict__.update(amazon_data)\n        product.status = 1\n        product.date_last_synced = now\n        product.save()\n\n        if price.value is not None:\n            # get all subscriptions of product that are subscribed to the current price or a higher one and\n            # whose owners have not been notified about that particular subscription price since before\n            # settings.PRICE_MONITOR_SUBSCRIPTION_RENOTIFICATION_MINUTES.\n            for sub in Subscription.objects.filter(\n                Q(\n                    product=product,\n                    price_limit__gte=price.value,\n                    date_last_notification__lte=(timezone.now() - timedelta(minutes=app_settings.PRICE_MONITOR_SUBSCRIPTION_RENOTIFICATION_MINUTES))\n                ) | Q(\n                    product=product,\n                    price_limit__gte=price.value,\n                    date_last_notification__isnull=True\n                )\n            ):\n                # FIXME: how to handle failed notifications?\n                NotifySubscriberTask().apply_async((product.pk, price.pk, sub.pk), countdown=5)\n\n\nclass NotifySubscriberTask(Task):\n\n    \"\"\"Task for notifying a single user about a product that has reached the desired price.\"\"\"\n\n    def run(self, product_pk, price_pk, subscription_pk, **kwargs):\n        \"\"\"\n        Sends an email to the subscriber.\n\n        :param product_pk: the id of product to notify about\n        :type product_pk: int\n        :param price_pk: the id of current price of the product\n        :type price_pk: int\n        :param subscription_pk: the id of Subscription class connecting subscriber and product\n        :type subscription_pk: int\n        \"\"\"\n        try:\n            product = Product.objects.get(pk=product_pk)\n        except Product.DoesNotExist:\n            logger.error('Product with PK %d could not be found.', product_pk)\n            return False\n\n        try:\n            price = Price.objects.get(pk=price_pk)\n        except Price.DoesNotExist:\n            logger.error('Price with PK %d could not be found.', price_pk)\n            return False\n\n        try:\n            subscription = Subscription.objects.get(pk=subscription_pk)\n        except Subscription.DoesNotExist:\n            logger.error('Subscription with PK %d could not be found.', subscription_pk)\n            return False\n\n        logger.info('Trying to send notification email to %s...', subscription.email_notification.email)\n        try:\n            send_mail(product, subscription, price, self.get_audience_rating_info(product))\n        except SMTPServerDisconnected:\n            logger.exception('SMTP server was disconnected.')\n        else:\n            logger.info('Notification email to %s was sent!', subscription.email_notification.email)\n            subscription.date_last_notification = timezone.now()\n            subscription.save()\n            return True\n\n        return False\n\n    # TODO move to Product\n    def get_audience_rating_info(self, product):\n        \"\"\"\n        Checks, if the product matches specific audience rating and includes additional information.\n\n        If the region is DE and the product is a FSK 18 one, additionally get all other FSK 18 products and put them into a mailable list.\n        see https://github.com/ponyriders/django-amazon-price-monitor/issues/92\n\n        As we do not currently have any use cases that could be generalized to something using the audience rating this is a country specific implementation.\n        :param product: the product to check\n        :type product:  price_monitor.models.Product\n        :return: an additional mail text or empty string if product and installation do not match prerequisites.\n        :rtype: str\n        \"\"\"\n        # age_identifiers = ['Freigegeben ab 18 Jahren', 'Ages 18 and over']\n        # if app_settings.PRICE_MONITOR_AMAZON_PRODUCT_API_REGION == 'DE' and product.audience_rating in age_identifiers:\n        #     # mail text\n        #     mail_text = ''\n        #\n        #     # fetch all other products with FSK 18\n        #     for p in Product.objects.filter(audience_rating__in=age_identifiers).exclude(pk=product.pk).order_by('current_price'):\n        #         mail_text += '{title:s}\\n'.format(title=p.get_title())\n        #         mail_text += '{price:0.2f} {currency:s} ({price_date:s})\\n'.format(\n        #             price=p.current_price.value,\n        #             currency=p.current_price.currency,\n        #             price_date=p.current_price.date_seen.strftime('%b %d, %Y %H:%M %p %Z'),\n        #         )\n        #         mail_text += '{offer_url:s}\\n'.format(offer_url=p.offer_url)\n        #         mail_text += '{product_detail_url:s}\\n\\n'.format(product_detail_url=product.get_detail_url())\n        #\n        #     # prepend introduction if there were results\n        #     if mail_text:\n        #         mail_text = '\\n{intro:s}\\n\\n'.format(\n        #             intro=ugettext('As this is a FSK 18 article, here are your other subscribed FSK 18 articles:')\n        #         ) + mail_text\n        #\n        #     # return\n        #     return mail_text\n        return ''\n"
  },
  {
    "path": "price_monitor/static/price_monitor/angular/angular-django-rest-resource.js",
    "content": "'use strict';\n\n//Portions of this file:\n//Copyright (c) 2010-2012 Google, Inc. http://angularjs.org\n//Those portions modified, used, or copied under permissions granted by the MIT license. See:\n// https://raw.github.com/angular/angular.js/9480136d9f062ec4b8df0a35914b48c0d61e0002/LICENSE\n\n/**\n * @ngdoc overview\n * @name djangoRESTResources\n * @description\n */\n\n/**\n * @ngdoc object\n * @name djangoRESTResources.djResource\n * @requires $http\n *\n * @description\n * A factory for generating classes that interact with a Django REST Framework backend.\n *\n * Identical in operation to AngularJS' ngResource module's $resource object except for the following:\n *  - If an isArray=True request receives a JSON _object_ containing a `count` field (instead of a JS array), assume\n *  that the REST endpoint has `paginate_by` set. The results are then streamed a page at a time into the promise object\n *  and any success callbacks are deferred until the last page returns successfully.\n *  - URLs are assumed to have the trailing slashes, as is the Django way of doing things.\n *\n * # Installation\n * Include `angular-django-rest-resource.js`\n *\n * Load the module:\n *\n *        angular.module('app', ['djangoRESTResources']);\n *\n * now you inject djResource into any of your Angular things.\n *\n * @param {string} url A parametrized URL template with parameters prefixed by `:` as in\n *   `/user/:username`. If you are using a URL with a port number (e.g.\n *   `http://example.com:8080/api`), you'll need to escape the colon character before the port\n *   number, like this: `djResource('http://example.com\\\\:8080/api')`.\n *\n * @param {Object=} paramDefaults Default values for `url` parameters. These can be overridden in\n *   `actions` methods. If any of the parameter value is a function, it will be executed every time\n *   when a param value needs to be obtained for a request (unless the param was overridden).\n *\n *   Each key value in the parameter object is first bound to url template if present and then any\n *   excess keys are appended to the url search query after the `?`.\n *\n *   Given a template `/path/:verb` and parameter `{verb:'greet', salutation:'Hello'}` results in\n *   URL `/path/greet?salutation=Hello`.\n *\n *   If the parameter value is prefixed with `@` then the value of that parameter is extracted from\n *   the data object (useful for non-GET operations).\n *\n * @param {Object.<Object>=} actions Hash with declaration of custom action that should extend the\n *   default set of resource actions. The declaration should be created in the format of $http.config\n *\n *       {action1: {method:?, params:?, isArray:?, headers:?, ...},\n *        action2: {method:?, params:?, isArray:?, headers:?, ...},\n *        ...}\n *\n *   Where:\n *\n *   - **`action`** – {string} – The name of action. This name becomes the name of the method on your\n *     resource object.\n *   - **`method`** – {string} – HTTP request method. Valid methods are: `GET`, `POST`, `PUT`, `DELETE`,\n *     and `JSONP`.\n *   - **`params`** – {Object=} – Optional set of pre-bound parameters for this action. If any of the\n *     parameter value is a function, it will be executed every time when a param value needs to be\n *     obtained for a request (unless the param was overridden).\n *   - **`url`** – {string} – action specific `url` override. The url templating is supported just like\n *     for the resource-level urls.\n *   - **`isArray`** – {boolean=} – If true then the returned object for this action is an array, see\n *     `returns` section.\n *   - **`transformRequest`** – `{function(data, headersGetter)|Array.<function(data, headersGetter)>}` –\n *     transform function or an array of such functions. The transform function takes the http\n *     request body and headers and returns its transformed (typically serialized) version.\n *   - **`transformResponse`** – `{function(data, headersGetter)|Array.<function(data, headersGetter)>}` –\n *     transform function or an array of such functions. The transform function takes the http\n *     response body and headers and returns its transformed (typically deserialized) version.\n *   - **`cache`** – `{boolean|Cache}` – If true, a default $http cache will be used to cache the\n *     GET request, otherwise if a cache instance built with\n *     {@link http://docs.angularjs.org/api/ng.$cacheFactory $cacheFactory}, this cache will be used for\n *     caching.\n *   - **`timeout`** – `{number}` – timeout in milliseconds.\n *   - **`withCredentials`** - `{boolean}` - whether to to set the `withCredentials` flag on the\n *     XHR object. See {@link https://developer.mozilla.org/en/http_access_control#section_5\n *     requests with credentials} for more information.\n *   - **`responseType`** - `{string}` - see\n *   {@link https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#responseType requestType}.\n *\n * @returns {Object} A resource \"class\" object with methods for the default set of resource actions\n *   optionally extended with custom `actions`. The default set contains these actions:\n *\n *       { 'get':    {method:'GET'},\n *         'save':   {method:'POST', method_if_field_has_value:['id', 'PUT']},\n *         'update': {method:'PUT'},\n *         'query':  {method:'GET', isArray:true},\n *         'remove': {method:'DELETE'},\n *         'delete': {method:'DELETE'} };\n *\n *   Calling these methods invoke an {@link http://docs.angularjs.org/api/ng.$http $http} with the specified http\n *   method, destination and parameters. When the data is returned from the server then the object is an\n *   instance of the resource class. The actions `save`, `remove` and `delete` are available on it\n *   as  methods with the `$` prefix. This allows you to easily perform CRUD operations (create,\n *   read, update, delete) on server-side data like this:\n *   <pre>\n        var User = djResource('/user/:userId', {userId:'@id'});\n        var user = User.get({userId:123}, function() {\n          user.abc = true;\n          user.$save();\n        });\n     </pre>\n *\n *   Invoking a djResource object method immediately returns an empty reference (object or array depending\n *   on `isArray`). Once the data is returned from the server the existing reference is populated with the actual data.\n *\n *   The action methods on the class object or instance object can be invoked with the following\n *   parameters:\n *\n *   - HTTP GET \"class\" actions: `DjangoRESTResource.action([parameters], [success], [error])`\n *   - non-GET \"class\" actions: `DjangoRESTResource.action([parameters], postData, [success], [error])`\n *   - non-GET instance actions:  `instance.$action([parameters], [success], [error])`\n *\n *\n *   The DjangoRESTResource instances and collection have these additional properties:\n *\n *   - `$then`: the `then` method of a {@link http://docs.angularjs.org/api/ng.$q promise} derived from the underlying\n *     {@link http://docs.angularjs.org/api/ng.$http $http} call.\n *\n *     The success callback for the `$then` method will be resolved if the underlying `$http` requests\n *     succeeds.\n *\n *     The success callback is called with a single object which is the\n *     {@link http://docs.angularjs.org/api/ng.$http http response}\n *     object extended with a new property `resource`. This `resource` property is a reference to the\n *     result of the resource action — resource object or array of resources.\n *\n *     The error callback is called with the {@link http://docs.angularjs.org/api/ng.$http http response} object when\n *     an http error occurs.\n *\n *   - `$resolved`: true if the promise has been resolved (either with success or rejection);\n *     Knowing if the DjangoRESTResource has been resolved is useful in data-binding.\n */\nangular.module('djangoRESTResources', ['ng']).\n  factory('djResource', ['$http', '$parse', function($http, $parse) {\n    var DEFAULT_ACTIONS = {\n      'get':    {method:'GET'},\n      'save':   {method:'POST', method_if_field_has_value: ['id','PUT']},\n      'update': {method:'PUT'},\n      'query':  {method:'GET', isArray:true},\n      'remove': {method:'DELETE'},\n      'delete': {method:'DELETE'}\n    };\n    var noop = angular.noop,\n        forEach = angular.forEach,\n        extend = angular.extend,\n        copy = angular.copy,\n        isFunction = angular.isFunction,\n        getter = function(obj, path) {\n          return $parse(path)(obj);\n        };\n\n    /**\n     * We need our custom method because encodeURIComponent is too aggressive and doesn't follow\n     * http://www.ietf.org/rfc/rfc3986.txt with regards to the character set (pchar) allowed in path\n     * segments:\n     *    segment       = *pchar\n     *    pchar         = unreserved / pct-encoded / sub-delims / \":\" / \"@\"\n     *    pct-encoded   = \"%\" HEXDIG HEXDIG\n     *    unreserved    = ALPHA / DIGIT / \"-\" / \".\" / \"_\" / \"~\"\n     *    sub-delims    = \"!\" / \"$\" / \"&\" / \"'\" / \"(\" / \")\"\n     *                     / \"*\" / \"+\" / \",\" / \";\" / \"=\"\n     */\n    function encodeUriSegment(val) {\n      return encodeUriQuery(val, true).\n        replace(/%26/gi, '&').\n        replace(/%3D/gi, '=').\n        replace(/%2B/gi, '+');\n    }\n\n\n    /**\n     * This method is intended for encoding *key* or *value* parts of query component. We need a custom\n     * method because encodeURIComponent is too aggressive and encodes stuff that doesn't have to be\n     * encoded per http://tools.ietf.org/html/rfc3986:\n     *    query       = *( pchar / \"/\" / \"?\" )\n     *    pchar         = unreserved / pct-encoded / sub-delims / \":\" / \"@\"\n     *    unreserved    = ALPHA / DIGIT / \"-\" / \".\" / \"_\" / \"~\"\n     *    pct-encoded   = \"%\" HEXDIG HEXDIG\n     *    sub-delims    = \"!\" / \"$\" / \"&\" / \"'\" / \"(\" / \")\"\n     *                     / \"*\" / \"+\" / \",\" / \";\" / \"=\"\n     */\n    function encodeUriQuery(val, pctEncodeSpaces) {\n      return encodeURIComponent(val).\n        replace(/%40/gi, '@').\n        replace(/%3A/gi, ':').\n        replace(/%24/g, '$').\n        replace(/%2C/gi, ',').\n        replace(/%20/g, (pctEncodeSpaces ? '%20' : '+'));\n    }\n\n    function Route(template, defaults) {\n      this.template = template = template + '#';\n      this.defaults = defaults || {};\n      this.urlParams = {};\n    }\n\n    Route.prototype = {\n      setUrlParams: function(config, params, actionUrl) {\n        var self = this,\n            url = actionUrl || self.template,\n            val,\n            encodedVal;\n\n        var urlParams = self.urlParams = {};\n        forEach(url.split(/\\W/), function(param){\n          if (param && (new RegExp(\"(^|[^\\\\\\\\]):\" + param + \"(\\\\W|$)\").test(url))) {\n              urlParams[param] = true;\n          }\n        });\n        url = url.replace(/\\\\:/g, ':');\n\n        params = params || {};\n        forEach(self.urlParams, function(_, urlParam){\n          val = params.hasOwnProperty(urlParam) ? params[urlParam] : self.defaults[urlParam];\n          if (angular.isDefined(val) && val !== null) {\n            encodedVal = encodeUriSegment(val);\n            url = url.replace(new RegExp(\":\" + urlParam + \"(\\\\W|$)\", \"g\"), encodedVal + \"$1\");\n          } else {\n            url = url.replace(new RegExp(\"(\\/?):\" + urlParam + \"(\\\\W|$)\", \"g\"), function(match,\n                leadingSlashes, tail) {\n              if (tail.charAt(0) == '/') {\n                return tail;\n              } else {\n                return leadingSlashes + tail;\n              }\n            });\n          }\n        });\n\n        // set the url\n        config.url = url.replace(/#$/, '');\n\n        // set params - delegate param encoding to $http\n        forEach(params, function(value, key){\n          if (!self.urlParams[key]) {\n            config.params = config.params || {};\n            config.params[key] = value;\n          }\n        });\n      }\n    };\n\n\n    function DjangoRESTResourceFactory(url, paramDefaults, actions) {\n      var route = new Route(url);\n\n      actions = extend({}, DEFAULT_ACTIONS, actions);\n\n      function extractParams(data, actionParams){\n        var ids = {};\n        actionParams = extend({}, paramDefaults, actionParams);\n        forEach(actionParams, function(value, key){\n          if (isFunction(value)) { value = value(); }\n          ids[key] = value.charAt && value.charAt(0) == '@' ? getter(data, value.substr(1)) : value;\n        });\n        return ids;\n      }\n\n      function DjangoRESTResource(value){\n        copy(value || {}, this);\n      }\n\n      forEach(actions, function(action, name) {\n        action.method = angular.uppercase(action.method);\n        var hasBody = action.method == 'POST' || action.method == 'PUT' || action.method == 'PATCH';\n        DjangoRESTResource[name] = function(a1, a2, a3, a4) {\n          var params = {};\n          var data;\n          var success = noop;\n          var error = null;\n          var promise;\n\n          switch(arguments.length) {\n          case 4:\n            error = a4;\n            success = a3;\n            //fallthrough\n          case 3:\n          case 2:\n            if (isFunction(a2)) {\n              if (isFunction(a1)) {\n                success = a1;\n                error = a2;\n                break;\n              }\n\n              success = a2;\n              error = a3;\n              //fallthrough\n            } else {\n              params = a1;\n              data = a2;\n              success = a3;\n              break;\n            }\n          case 1:\n            if (isFunction(a1)) success = a1;\n            else if (hasBody) data = a1;\n            else params = a1;\n            break;\n          case 0: break;\n          default:\n            throw \"Expected between 0-4 arguments [params, data, success, error], got \" +\n              arguments.length + \" arguments.\";\n          }\n\n          var value = this instanceof DjangoRESTResource ? this : (action.isArray ? [] : new DjangoRESTResource(data));\n          var httpConfig = {},\n              promise;\n\n          forEach(action, function(value, key) {\n            if (key == 'method' && action.hasOwnProperty('method_if_field_has_value')) {\n              // Check if the action's HTTP method is dependent on a field holding a value ('id' for example)\n              var field = action.method_if_field_has_value[0];\n              var fieldDependentMethod = action.method_if_field_has_value[1];\n              httpConfig.method =\n                (data.hasOwnProperty(field) && data[field] !== null) ? fieldDependentMethod : action.method;\n            } else if (key != 'params' && key != 'isArray' ) {\n              httpConfig[key] = copy(value);\n            }\n          });\n          httpConfig.data = data;\n          route.setUrlParams(httpConfig, extend({}, extractParams(data, action.params || {}), params), action.url);\n\n          function markResolved() { value.$resolved = true; }\n\n          promise = $http(httpConfig);\n          value.$resolved = false;\n\n          promise.then(markResolved, markResolved);\n          value.$then = promise.then(function(response) {\n            // Success wrapper\n\n            var data = response.data;\n            var then = value.$then, resolved = value.$resolved;\n\n            var deferSuccess = false;\n\n            if (data) {\n              if (action.isArray) {\n                value.length = 0;\n\n                // If it's an object with count and results, it's a pagination container, not an array:\n                if (data.hasOwnProperty(\"count\") && data.hasOwnProperty(\"results\")) {\n                  // Don't call success callback until the last page has been accepted:\n                  deferSuccess = true;\n\n                  var paginator = function recursivePaginator(data) {\n                    // If there is a next page, go ahead and request it before parsing our results. Less wasted time.\n                    if (data.next !== null) {\n                      var next_config = copy(httpConfig);\n                      next_config.params = {};\n                      next_config.url = data.next;\n                      $http(next_config).success(function(next_data) { recursivePaginator(next_data); }).error(error);\n                    }\n                    // Ok, now load this page's results:\n                    forEach(data.results, function(item) {\n                      value.push(new DjangoRESTResource(item));\n                    });\n                    if (data.next == null) {\n                      // We've reached the last page, call the original success callback with the concatenated pages of data.\n                      (success||noop)(value, response.headers);\n                    }\n                  };\n                  paginator(data);\n                } else {\n                  //Not paginated, push into array as normal.\n                  forEach(data, function(item) {\n                    value.push(new DjangoRESTResource(item));\n                  });\n                }\n              } else {\n                // Not an isArray action\n                copy(data, value);\n\n                // Copy operation destroys value's original properties, so restore some of the old ones:\n                value.$then = then;\n                value.$resolved = resolved;\n                value.$promise = promise;\n              }\n            }\n\n            if (!deferSuccess) {\n              (success||noop)(value, response.headers);\n            }\n\n            response.resource = value;\n            return response;\n          }, error).then;\n\n          return value;\n        };\n\n\n        DjangoRESTResource.prototype['$' + name] = function(a1, a2, a3) {\n          var params = extractParams(this),\n              success = noop,\n              error;\n\n          switch(arguments.length) {\n          case 3: params = a1; success = a2; error = a3; break;\n          case 2:\n          case 1:\n            if (isFunction(a1)) {\n              success = a1;\n              error = a2;\n            } else {\n              params = a1;\n              success = a2 || noop;\n            }\n          case 0: break;\n          default:\n            throw \"Expected between 1-3 arguments [params, success, error], got \" +\n              arguments.length + \" arguments.\";\n          }\n          var data = hasBody ? this : undefined;\n          return DjangoRESTResource[name].call(this, params, data, success, error);\n        };\n      });\n\n      DjangoRESTResource.bind = function(additionalParamDefaults){\n        return DjangoRESTResourceFactory(url, extend({}, paramDefaults, additionalParamDefaults), actions);\n      };\n\n      return DjangoRESTResource;\n    }\n\n    return DjangoRESTResourceFactory;\n  }]);\n"
  },
  {
    "path": "price_monitor/static/price_monitor/angular/angular-responsive-images.js",
    "content": "/**\n * Angular responsive images\n * @version v0.0.0-dev-2013-06-19\n * @link https://github.com/c0bra/angular-res-img.git\n * @license MIT License, http://www.opensource.org/licenses/MIT\n */(function(){\n\nvar app = angular.module('ngResponsiveImages', []);\n\n// Default queries (stolen from Zurb Foundation)\napp.value('presetMediaQueries', {\n  'default':   'only screen and (min-width: 1px)',\n  'small':     'only screen and (min-width: 768px)',\n  'medium':    'only screen and (min-width: 1280px)',\n  'large':     'only screen and (min-width: 1440px)',\n  'landscape': 'only screen and (orientation: landscape)',\n  'portrait':  'only screen and (orientation: portrait)',\n  'retina':    'only screen and (-webkit-min-device-pixel-ratio: 2), ' +\n               'only screen and (min--moz-device-pixel-ratio: 2), ' +\n               'only screen and (-o-min-device-pixel-ratio: 2/1), ' +\n               'only screen and (min-device-pixel-ratio: 2), ' +\n               'only screen and (min-resolution: 192dpi), ' +\n               'only screen and (min-resolution: 2dppx)'\n});\n\napp.directive('ngSrcResponsive', ['presetMediaQueries', '$timeout', function(presetMediaQueries, $timeout) {\n  return {\n    restrict: 'A',\n    priority: 100,\n    link: function(scope, elm, attrs) {\n      // Double-check that the matchMedia function matchMedia exists\n      if (typeof(matchMedia) !== 'function') {\n        throw \"Function 'matchMedia' does not exist\";\n      }\n\n      // Array of media query and listener sets\n      // \n      // {\n      //    mql: <MediaQueryList object>\n      //    listener: function () { ... } \n      // }\n      // \n      var listenerSets = [];\n\n      // Query that gets run on link, whenever the directive attr changes, and whenever \n      var waiting = false;\n      function updateFromQuery(querySets) {\n        // Throttle calling this function so that multiple media query change handlers don't try to run concurrently\n        if (!waiting) {\n          $timeout(function() { \n            // Destroy registered listeners, we will re-register them below\n            angular.forEach(listenerSets, function(set) {\n              set.mql.removeListener(set.listener);\n            });\n\n            // Clear the deregistration functions\n            listenerSets = [];\n            var lastTrueQuerySet;\n\n            // for (var query in querySets) {\n            angular.forEach(querySets, function(set) {\n              // if (querySets.hasOwnProperty(query)) {\n\n              var queryText = set[0];\n\n              // If we were passed a preset query, use its value instead\n              var query = queryText;\n              if (presetMediaQueries.hasOwnProperty(queryText)) {\n                query = presetMediaQueries[queryText];\n              }\n\n              var mq = matchMedia(query);\n\n              if (mq.matches) {\n                lastTrueQuerySet = set;\n              }\n\n              // Listener function for this query\n              var queryListener = function(mql) {\n                // TODO: add throttling or a debounce here (or somewhere) to prevent this function from being called a ton of times\n                updateFromQuery(querySets);\n              };\n\n              // Add a listener for when this query's match changes\n              mq.addListener(queryListener);\n\n              listenerSets.push({\n                mql: mq,\n                listener: queryListener\n              });\n            });\n\n            if (lastTrueQuerySet) {\n              setSrc( lastTrueQuerySet[1] );\n            }\n\n            waiting = false;\n          }, 0);\n          \n          waiting = true;\n        }\n      }\n\n      \n      function setSrc(src) {\n        elm.attr('src', src);\n      }\n\n      var updaterDereg;\n      attrs.$observe('ngSrcResponsive', function(value) {\n        var querySets = scope.$eval(value);\n        \n        if (querySets instanceof Array === false) {\n          throw \"Expected evaluate ng-src-responsive to evaluate to an Array, instead got: \" + querySets;\n        }\n\n        updateFromQuery(querySets);\n\n        // Remove the previous matchMedia listener\n        if (typeof(updaterDereg) === 'function') { updaterDereg(); }\n\n        // Add a global match-media listener back\n        // var mq = matchMedia('only screen and (min-width: 1px)');\n        // console.log('mq', mq);\n        // updaterDereg = mq.addListener(function(){\n        //   console.log('updating!');\n        //   updateFromQuery(querySets);\n        // });\n      });\n    }\n  };\n}]);\n\n})();\n"
  },
  {
    "path": "price_monitor/static/price_monitor/app/css/app.css",
    "content": ".content {\n    margin-top: 55px;\n}\n\n.media:first-child {\n    /* This is the default */\n    margin-top: 15px;\n}\n\n.media.list a, .media.list a:visited, .media.list a:hover, .media.list a:active {\n    text-decoration: none;\n    color: #000;\n}\n\n.media.list a.pull-left {\n    height: 75px;\n    width: 75px;\n}\n\n.media.list .media-body {\n    margin-left: 85px;\n}\n\n.media.list .media-body img.sparkline {\n    height: 40px;\n    width: 400px;\n}\n\n#product-form .row {\n    margin-top: 5px;\n}\n\n#product-form input,\n#emailnotification-form .form-group,\n#emailnotification-form .form-group input[type=\"email\"] {\n    width: 100%;\n}\n\n#product-form select {\n    display: inline-block;\n    width: 84%;\n}\n\n#product-form span.glyphicon {\n    cursor: pointer;\n}\n\n.responsive-chart {\n    margin-top: 15px;\n}\n"
  },
  {
    "path": "price_monitor/static/price_monitor/app/css/xeditable.css",
    "content": "/*!\nangular-xeditable - 0.1.8\nEdit-in-place for angular.js\nBuild date: 2014-01-10 \n*/\n\n.editable-wrap{display:inline-block;white-space:nowrap;margin:0}.editable-wrap .editable-controls,.editable-wrap .editable-error{margin-bottom:0}.editable-wrap .editable-controls>input,.editable-wrap .editable-controls>select,.editable-wrap .editable-controls>textarea{margin-bottom:0}.editable-wrap .editable-input{display:inline-block}.editable-buttons{display:inline-block;vertical-align:top}.editable-buttons button{margin-left:5px}.editable-input.editable-has-buttons{width:auto}.editable-bstime .editable-input input[type=text]{width:46px}.editable-bstime .well-small{margin-bottom:0;padding:10px}.editable-range output{display:inline-block;min-width:30px;vertical-align:top;text-align:center}.editable-color input[type=color]{width:50px}.editable-checkbox label span,.editable-checklist label span,.editable-radiolist label span{margin-left:7px;margin-right:10px}.editable-hide{display:none!important}.editable-click,a.editable-click{text-decoration:none;color:#428bca;border-bottom:dashed 1px #428bca}.editable-click:hover,a.editable-click:hover{text-decoration:none;color:#2a6496;border-bottom-color:#2a6496}.editable-empty,.editable-empty:hover,.editable-empty:focus,a.editable-empty,a.editable-empty:hover,a.editable-empty:focus{font-style:italic;color:#D14;text-decoration:none}"
  },
  {
    "path": "price_monitor/static/price_monitor/app/js/app.js",
    "content": "'use strict';\n\nvar PriceMonitorApp = angular.module(\n    'PriceMonitorApp', \n    [\n        'ngCookies',\n        'ngRoute',\n        'ngResource',\n        'ui.bootstrap',\n        'djangoRESTResources',\n        'ngResponsiveImages',\n        'xeditable',\n        'PriceMonitorServerConnector'\n    ]\n);\n\nPriceMonitorApp.config(function ($routeProvider) {\n    $routeProvider\n        .when('/products', {\n            controller: 'ProductListCtrl',\n            templateUrl: SETTINGS.uris.static + 'price_monitor/app/partials/product-list.html'\n        })\n        .when('/products/:asin', {\n            controller: 'ProductDetailCtrl',\n            templateUrl: SETTINGS.uris.static + 'price_monitor/app/partials/product-detail.html'\n        })\n        .otherwise({redirectTo: '/products'});\n});\n\n/**\n * Setting X-Requested-With header to enable Django to identify the request as asyncronous.\n */\n//PriceMonitorApp.config('$httpProvider', function($httpProvider) {\n//    $httpProvider.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';\n//});\n\n/**\n * Adding value of CSRF cookie to request headers\n */\nPriceMonitorApp.run(function($http, $cookies, editableOptions) {\n    $http.defaults.headers.post['X-CSRFToken'] = $cookies.csrftoken;\n    // Add the following two lines\n    $http.defaults.xsrfCookieName = 'csrftoken';\n    $http.defaults.xsrfHeaderName = 'X-CSRFToken';\n    editableOptions.theme = 'bs3';\n});\n"
  },
  {
    "path": "price_monitor/static/price_monitor/app/js/controller/emailnotification-create-ctrl.js",
    "content": "PriceMonitorApp.controller('EmailNotificationCreateCtrl', function ($scope, $modalInstance, EmailNotification) {\n    $scope.email_notification = {};\n\n    $scope.ok = function (email_notification) {\n        EmailNotification.save(email_notification, function() {\n            $modalInstance.close();\n        });\n    };\n    $scope.cancel = function () {\n        $modalInstance.dismiss('cancel');\n    };\n});\n"
  },
  {
    "path": "price_monitor/static/price_monitor/app/js/controller/main-ctrl.js",
    "content": "PriceMonitorApp.controller('MainCtrl', function ($scope, $location) {\n    $scope.URIS = window.URIS;\n    $scope.isActive = function (route) {\n        return route === $location.path();\n    }\n});\n"
  },
  {
    "path": "price_monitor/static/price_monitor/app/js/controller/product-delete-ctrl.js",
    "content": "PriceMonitorApp.controller('ProductDeleteCtrl', function ($scope, $modalInstance, product) {\n    $scope.product = product;\n    $scope.ok = function () {\n        $scope.product.$delete();\n        $modalInstance.close();\n    };\n    $scope.cancel = function () {\n        $modalInstance.dismiss('cancel');\n    };\n});\n"
  },
  {
    "path": "price_monitor/static/price_monitor/app/js/controller/product-detail-ctrl.js",
    "content": "PriceMonitorApp.controller('ProductDetailCtrl', function ($scope, $routeParams, $location, $modal, Product) {\n    $scope.siteName = SETTINGS.siteName;\n    $scope.product = Product.get(\n        {asin: $routeParams.asin},\n        // called when product can be retrieved\n        function () {\n            $scope.open = function () {\n                var modalInstance = $modal.open({\n                    templateUrl: SETTINGS.uris.static + '/price_monitor/app/partials/product-delete.html',\n                    controller: 'ProductDeleteCtrl',\n                    resolve: {\n                        product: function () {\n                            return $scope.product;\n                        }\n                    }\n                });\n\n                modalInstance.result.then(function () {\n                    $location.path('#products');\n                });\n            };\n        },\n        // called if asin is not found\n        function () {\n            $location.path('#products');\n        }\n    );\n\n\n});\n"
  },
  {
    "path": "price_monitor/static/price_monitor/app/js/controller/product-list-ctrl.js",
    "content": "PriceMonitorApp.controller('ProductListCtrl', function($scope, $modal, Product, EmailNotification) {\n    $scope.products = Product.query(function() {\n        $scope.emailNotifications = EmailNotification.query(function() {\n            $scope.productCount = $scope.products.length;\n            $scope.currentPage = 1;\n            $scope.maxPageCount = SETTINGS.pagination.maxPageCount;\n            $scope.itemsPerPage = SETTINGS.pagination.itemsPerPage;\n            $scope.paginationBoundaryLinks = SETTINGS.pagination.paginationBoundaryLinks;\n            $scope.paginationRotate = SETTINGS.pagination.paginationRotate;\n            $scope.pagesTotal = 0;\n            $scope.siteName = SETTINGS.siteName;\n\n            var emptyProduct = {\n                asin: null,\n                subscription_set: [{\n                    price_limit: null,\n                    email_notification: {\n                        email: $scope.emailNotifications.length > 0 ? $scope.emailNotifications[0].email : ''\n                    }\n                }]\n            };\n\n            $scope.newProducts = [angular.copy(emptyProduct)];\n\n            $scope.addNewProduct = function() {\n                $scope.newProducts.push(emptyProduct);\n            };\n\n            $scope.removeFormLine = function(product) {\n                var index = $scope.newProducts.indexOf(product);\n                if (index != -1) {\n                    $scope.newProducts.splice(index, 1);\n                }\n            };\n\n            $scope.saveNewProducts = function() {\n                angular.forEach($scope.newProducts, function(newProduct) {\n                    Product.save(newProduct, function() {\n                        $scope.products = Product.query();\n                        $scope.newProducts = [angular.copy(emptyProduct)];\n                    });\n                });\n            };\n\n            $scope.openProductDelete = function (product) {\n                var modalInstance = $modal.open({\n                    templateUrl: SETTINGS.uris.static + '/price_monitor/app/partials/product-delete.html',\n                    controller: 'ProductDeleteCtrl',\n                    resolve: {\n                        product: function () {\n                            return product;\n                        }\n                    }\n                });\n\n                modalInstance.result.then(function () {\n                    $scope.products = Product.query();\n                });\n            };\n\n            $scope.openEmailNotificationCreate = function() {\n                var modalInstance = $modal.open({\n                    templateUrl: SETTINGS.uris.static + '/price_monitor/app/partials/emailnotification-create.html',\n                    controller: 'EmailNotificationCreateCtrl'\n                });\n\n                modalInstance.result.then(function () {\n                    $scope.emailNotifications = EmailNotification.query();\n                });\n            };\n        });\n    });\n});\n"
  },
  {
    "path": "price_monitor/static/price_monitor/app/js/filters.js",
    "content": "//We already have a limitTo filter built-in to angular,\n//let's make a startFrom filter\nPriceMonitorApp.filter('startFrom', function() {\n    return function(input, start) {\n        start = parseInt(start); //parse to int\n        return input.slice(start);\n    }\n});\n"
  },
  {
    "path": "price_monitor/static/price_monitor/app/js/server-connector.js",
    "content": "'use strict';\n\nvar PriceMonitorServerConnector = angular.module('PriceMonitorServerConnector', ['ngResource', 'djangoRESTResources']);\n\nPriceMonitorServerConnector.factory('Product', ['djResource', function(djResource) {\n    var Product = djResource(SETTINGS.uris.product, {'asin': '@asin'}, {\n        'update': {\n            method:'PUT'\n        }\n    });\n    \n    Product.prototype.getSparklineUrl = function() {\n        return SETTINGS.uris.sparkline.replace(':asin', this.asin);\n    };\n    \n    Product.prototype.getChartUrl = function(size) {\n        if (SETTINGS.uris.chart[size]) {\n            return SETTINGS.uris.chart[size].replace(':asin', this.asin)\n        }\n        return '';\n    };\n\n    Product.prototype.removeSubscription = function(index) {\n        this.subscription_set.splice(index, 1);\n    };\n    \n    return Product;\n}]);\n\nPriceMonitorServerConnector.factory('Subscription', ['djResource', 'Product', function(djResource) {\n    return djResource(SETTINGS.uris.subscription, {'public_id': '@public_id'}, {});\n}]);\n\nPriceMonitorServerConnector.factory('Price', ['djResource', function(djResource) {\n    return djResource(SETTINGS.uris.price, {'asin': '@asin'}, {});\n}]);\n\nPriceMonitorServerConnector.factory('EmailNotification', ['djResource', function(djResource) {\n    return djResource(SETTINGS.uris.emailNotification, {}, {});\n}]);\n"
  },
  {
    "path": "price_monitor/static/price_monitor/app/partials/emailnotification-create.html",
    "content": "<form novalidate id=\"emailnotification-form\" name=\"emailnotification_form\" class=\"form-inline\">\n    <div class=\"modal-header\">\n        <h3 class=\"modal-title\">Add email address</h3>\n    </div>\n    <div class=\"modal-body\">\n        <div class=\"row\">\n            <div class=\"col-xs-12\">\n                <div class=\"form-group\" ng-class=\"{ 'has-error' : email_notification.email.$invalid && email_notification.email.$dirty}\">\n                    <input class=\"form-control\" type=\"email\" ng-model=\"email_notification.email\" placeholder=\"E-Mail\" required />\n                </div>\n            </div>\n        </div>\n    </div>\n    <div class=\"modal-footer\">\n        <input type=\"submit\" class=\"btn btn-danger\" ng-disabled=\"!emailnotification_form.$valid\" ng-click=\"ok(email_notification)\" value=\"OK\" />\n        <button class=\"btn\" ng-click=\"cancel()\">Cancel</button>\n    </div>\n</form>\n"
  },
  {
    "path": "price_monitor/static/price_monitor/app/partials/product-delete.html",
    "content": "<div class=\"modal-header\">\n    <h3 class=\"modal-title\">Delete \"{{ product.title }}\"</h3>\n</div>\n<div class=\"modal-body\">\n    Are you sure you want to delete {{ product.title }}?\n</div>\n<div class=\"modal-footer\">\n    <button class=\"btn btn-danger\" ng-click=\"ok()\">OK</button>\n    <button class=\"btn\" ng-click=\"cancel()\">Cancel</button>\n</div>\n"
  },
  {
    "path": "price_monitor/static/price_monitor/app/partials/product-detail.html",
    "content": "<div class=\"media\">\n    <a class=\"pull-left\" href=\"{{ product.offer_url }}\">\n        <img class=\"media-object\" ng-src=\"{{ product.image_urls.medium }}\"\n        >\n    </a>\n    <div class=\"media-body\">\n        <div class=\"row\">\n            <div class=\"col-xs-12\">\n                <h1>\n                    <button class=\"btn btn-danger pull-right\" ng-click=\"open()\" type=\"button\" aria-label=\"Close\"><span aria-hidden=\"true\">&times;</span></button>\n                    <span ng-if=\"product.artist\">{{ product.artist }}: </span>{{ product.title }}\n                </h1>\n            </div>\n        </div>\n        <div class=\"row\">\n            <div class=\"col-xs-12\">\n                <div class=\"row\">\n                    <div class=\"col-xs-3 col-md-2\">Current price:</div>\n                    <div class=\"col-xs-9 col-md-10\">\n                        <span ng-if=\"product.current_price.value\">{{ product.current_price.currency }} {{ product.current_price.value | number: 2 }}</span>\n                        <span ng-if=\"!product.current_price.value\">No current price available</span>  (as of {{ product.current_price.date_seen | date: 'MMMM d, y hh:mm a Z' }} - <span data-toggle=\"tooltip\" data-placement=\"top\" title=\"Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on {{ siteName }} at the time of purchase will apply to the purchase of this product.\">Details</span>)</div>\n                </div>\n                <div class=\"row\">\n                    <div class=\"col-xs-3 col-md-2\">Highest price:</div>\n                    <div class=\"col-xs-9 col-md-10\" ng-show=\"product.highest_price\">{{ product.highest_price.currency }} {{ product.highest_price.value | number: 2 }} (as of {{ product.highest_price.date_seen | date: 'MMMM d, y hh:mm a Z' }} - <span data-toggle=\"tooltip\" data-placement=\"top\" title=\"Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on {{ siteName }} at the time of purchase will apply to the purchase of this product.\">Details</span>)</div>\n                    <div class=\"col-xs-9 col-md-10\" ng-hide=\"product.highest_price\">No highest price available.</div>\n                </div>\n                <div class=\"row\">\n                    <div class=\"col-xs-3 col-md-2\">Lowest price:</div>\n                    <div class=\"col-xs-9 col-md-10\" ng-show=\"product.lowest_price\">{{ product.lowest_price.currency }} {{ product.lowest_price.value | number: 2 }} (as of {{ product.lowest_price.date_seen | date: 'MMMM d, y hh:mm a Z' }} - <span data-toggle=\"tooltip\" data-placement=\"top\" title=\"Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on {{ siteName }} at the time of purchase will apply to the purchase of this product.\">Details</span>)</div>\n                    <div class=\"col-xs-9 col-md-10\" ng-hide=\"product.lowest_price\">No lowest price available.</div>\n                </div>\n                <div class=\"row\" ng-repeat=\"subscription in product.subscription_set\">\n                    <div class=\"col-xs-2 col-md-1\">Limit:</div>\n                    <div class=\"col-xs-9 col-md-10\">{{ product.current_price.currency }} <span editable-number=\"subscription.price_limit\" e-step=\"0.01\" onaftersave=\"product.$update()\">{{ subscription.price_limit | number: 2 }}</span>  (<span editable-email=\"subscription.email_notification.email\" onaftersave=\"product.$update()\">{{ subscription.email_notification.email }}</span>)</div>\n                    <div class=\"col-xs-1\">\n                        <button class=\"close\" ng-if=\"product.subscription_set.length > 1\" ng-click=\"product.removeSubscription($index); product.$update()\" aria-label=\"Close\">\n                            <span aria-hidden=\"true\">&times;</span>\n                        </button>\n                    </div>\n                </div>\n            </div>\n        </div>\n    </div>\n</div>\n\n<div class=\"row responsive-chart\">\n    <div class=\"col-xs-12\">\n        <img ng-src-responsive=\"[ [ 'default', '{{ product.getChartUrl('default') }}'], ['small', '{{ product.getChartUrl('small') }}' ], ['medium', '{{ product.getChartUrl('medium') }}' ], ['large', '{{ product.getChartUrl('large') }}' ] ]\">\n    </div>\n</div>\n"
  },
  {
    "path": "price_monitor/static/price_monitor/app/partials/product-list.html",
    "content": "<center ng-hide=\"products.length == 0\">\n    <pagination total-items=\"productCount\" ng-model=\"currentPage\" max-size=\"maxPageCount\" class=\"pagination-sm\" boundary-links=\"true\" rotate=\"false\" num-pages=\"pagesTotal\" items-per-page=\"itemsPerPage\"></pagination>\n</center>\n<div class=\"media list\" ng-repeat=\"product in products | startFrom:(currentPage-1)*itemsPerPage | limitTo:itemsPerPage\">\n    <a class=\"pull-left\" href=\"{{ product.offer_url }}\"\n       ng-if=\"product.image_urls.small\">\n        <img class=\"media-object\" ng-src=\"{{ product.image_urls.small }}\"\n             alt=\"{{ product.title }}\">\n    </a>\n\n        <div class=\"media-body\">\n            <h4 class=\"media-heading\">\n                <button class=\"btn btn-xs btn-danger pull-right\" ng-click=\"openProductDelete(product)\" type=\"button\" aria-label=\"Close\"><span aria-hidden=\"true\">&times;</span></button>\n                <a href=\"#/products/{{ product.asin }}\">\n                    <span ng-if=\"product.artist\">{{ product.artist }}: </span>{{ product.title }}\n                </a>\n            </h4>\n            <div class=\"row\">\n                <div class=\"col-xs-12 col-md-6\">\n                    <div class=\"row\">\n                        <div class=\"col-xs-2 col-md-1\">Price:</div>\n                        <div class=\"col-xs-10 col-md-11\">\n                            <span ng-if=\"product.current_price.value\">{{ product.current_price.currency }} {{ product.current_price.value | number: 2 }}</span>\n                            <span ng-if=\"!product.current_price.value\">No price available</span> (as of {{ product.current_price.date_seen | date: 'MMMM d, y hh:mm a Z' }} - <span data-toggle=\"tooltip\" data-placement=\"top\" title=\"Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on {{ siteName }} at the time of purchase will apply to the purchase of this product.\">Details</span>)\n                        </div>\n                    </div>\n                    <div class=\"row\">\n                        <div class=\"col-xs-2 col-md-1\">Limit:</div>\n                        <div class=\"col-xs-10 col-md-11\">{{ product.current_price.currency || product.highest_price.currency || product.lowest_price.currency }} <span ng-repeat=\"subscription in product.subscription_set\"><span editable-number=\"subscription.price_limit\" e-step=\"0.01\" onaftersave=\"product.$update()\">{{ subscription.price_limit | number: 2 }}</span>{{ $last ? '' : ', ' }}</div>\n                    </div>\n                </div>\n                <div class=\"col-xs-12 col-md-6\">\n                    <a href=\"#/products/{{ product.asin }}\">\n                        <img class=\"sparkline\" ng-src=\"{{ product.getSparklineUrl() }}\" />\n                    </a>\n                </div>\n            </div>\n        </div>\n</div>\n<center ng-hide=\"products.length == 0\">\n    <pagination total-items=\"productCount\" ng-model=\"currentPage\" max-size=\"maxPageCount\" class=\"pagination-sm\" boundary-links=\"true\" rotate=\"false\" num-pages=\"pagesTotal\" items-per-page=\"itemsPerPage\"></pagination>\n</center>\n\n<form novalidate name=\"product_form\" id=\"product-form\" class=\"form-inline\">\n    <div ng-repeat=\"product in newProducts\" class=\"row\">\n        <ng-form name=\"innerForm\">\n        <div class=\"col-xs-4 form-group\" ng-class=\"{ 'has-error' : innerForm.asin.$invalid && innerForm.asin.$dirty}\">\n                <input class=\"form-control\" type=\"text\" name=\"asin\" ng-model=\"product.asin\" placeholder=\"ASIN\" required ng-pattern=\"/[A-Z0-9]/\" />\n            </div>\n            <div class=\"col-xs-4 form-group\" ng-class=\"{ 'has-error' : innerForm.subscription_set[0].email_notification.email.$invalid && innerForm.subscription_set[0].email_notification.email.$dirty}\">\n                <select ng-if=\"emailNotifications.length > 0\" class=\"form-control\" name=\"email\" ng-model=\"product.subscription_set[0].email_notification.email\">\n                    <option ng-repeat=\"emailNotification in emailNotifications\" ng-selected=\"$first\">{{ emailNotification.email }}</option>\n                </select>\n                <input ng-if=\"emailNotifications.length == 0\" class=\"form-control\" type=\"email\" name=\"email\" ng-model=\"product.subscription_set[0].email_notification.email\" placeholder=\"E-Mail\" required />\n                <span ng-if=\"emailNotifications.length > 0\" ng-click=\"openEmailNotificationCreate()\" aria-hidden=\"true\" class=\"glyphicon glyphicon-plus\"></span>\n            </div>\n            <div class=\"col-xs-3 form-group\" ng-class=\"{ 'has-error' : innerForm.subscription_set[0].price_limit.$invalid && innerForm.subscription_set[0].price_limit.$dirty}\">\n                <input class=\"form-control\" type=\"number\" name=\"price\" ng-model=\"product.subscription_set[0].price_limit\" required step=\"0.01\" />\n            </div>\n            <div class=\"col-xs-1\">\n                <button class=\"close\" ng-if=\"newProducts.length > 1\" ng-click=\"removeFormLine(product)\" aria-label=\"Close\">\n                    <span aria-hidden=\"true\">&times;</span>\n                </button>\n            </div>\n        </ng-form>\n    </div>\n    <div class=\"row\">\n        <div class=\"col-xs-12\">\n            <button class=\"btn\" ng-click=\"addNewProduct()\" type=\"button\">Add another</button>\n            <button class=\"btn btn-primary pull-right\" ng-disabled=\"product_form.$invalid\" ng-click=\"saveNewProducts()\" type=\"submit\">Save</button>\n        </div>\n    </div>\n</form>\n\n"
  },
  {
    "path": "price_monitor/static/price_monitor/bootstrap/css/bootstrap-theme.css",
    "content": "/*!\n * Bootstrap v3.1.1 (http://getbootstrap.com)\n * Copyright 2011-2014 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n */\n\n.btn-default,\n.btn-primary,\n.btn-success,\n.btn-info,\n.btn-warning,\n.btn-danger {\n  text-shadow: 0 -1px 0 rgba(0, 0, 0, .2);\n  -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075);\n          box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075);\n}\n.btn-default:active,\n.btn-primary:active,\n.btn-success:active,\n.btn-info:active,\n.btn-warning:active,\n.btn-danger:active,\n.btn-default.active,\n.btn-primary.active,\n.btn-success.active,\n.btn-info.active,\n.btn-warning.active,\n.btn-danger.active {\n  -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);\n          box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);\n}\n.btn:active,\n.btn.active {\n  background-image: none;\n}\n.btn-default {\n  text-shadow: 0 1px 0 #fff;\n  background-image: -webkit-linear-gradient(top, #fff 0%, #e0e0e0 100%);\n  background-image:         linear-gradient(to bottom, #fff 0%, #e0e0e0 100%);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);\n  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n  background-repeat: repeat-x;\n  border-color: #dbdbdb;\n  border-color: #ccc;\n}\n.btn-default:hover,\n.btn-default:focus {\n  background-color: #e0e0e0;\n  background-position: 0 -15px;\n}\n.btn-default:active,\n.btn-default.active {\n  background-color: #e0e0e0;\n  border-color: #dbdbdb;\n}\n.btn-primary {\n  background-image: -webkit-linear-gradient(top, #428bca 0%, #2d6ca2 100%);\n  background-image:         linear-gradient(to bottom, #428bca 0%, #2d6ca2 100%);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff2d6ca2', GradientType=0);\n  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n  background-repeat: repeat-x;\n  border-color: #2b669a;\n}\n.btn-primary:hover,\n.btn-primary:focus {\n  background-color: #2d6ca2;\n  background-position: 0 -15px;\n}\n.btn-primary:active,\n.btn-primary.active {\n  background-color: #2d6ca2;\n  border-color: #2b669a;\n}\n.btn-success {\n  background-image: -webkit-linear-gradient(top, #5cb85c 0%, #419641 100%);\n  background-image:         linear-gradient(to bottom, #5cb85c 0%, #419641 100%);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);\n  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n  background-repeat: repeat-x;\n  border-color: #3e8f3e;\n}\n.btn-success:hover,\n.btn-success:focus {\n  background-color: #419641;\n  background-position: 0 -15px;\n}\n.btn-success:active,\n.btn-success.active {\n  background-color: #419641;\n  border-color: #3e8f3e;\n}\n.btn-info {\n  background-image: -webkit-linear-gradient(top, #5bc0de 0%, #2aabd2 100%);\n  background-image:         linear-gradient(to bottom, #5bc0de 0%, #2aabd2 100%);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);\n  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n  background-repeat: repeat-x;\n  border-color: #28a4c9;\n}\n.btn-info:hover,\n.btn-info:focus {\n  background-color: #2aabd2;\n  background-position: 0 -15px;\n}\n.btn-info:active,\n.btn-info.active {\n  background-color: #2aabd2;\n  border-color: #28a4c9;\n}\n.btn-warning {\n  background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #eb9316 100%);\n  background-image:         linear-gradient(to bottom, #f0ad4e 0%, #eb9316 100%);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);\n  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n  background-repeat: repeat-x;\n  border-color: #e38d13;\n}\n.btn-warning:hover,\n.btn-warning:focus {\n  background-color: #eb9316;\n  background-position: 0 -15px;\n}\n.btn-warning:active,\n.btn-warning.active {\n  background-color: #eb9316;\n  border-color: #e38d13;\n}\n.btn-danger {\n  background-image: -webkit-linear-gradient(top, #d9534f 0%, #c12e2a 100%);\n  background-image:         linear-gradient(to bottom, #d9534f 0%, #c12e2a 100%);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);\n  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n  background-repeat: repeat-x;\n  border-color: #b92c28;\n}\n.btn-danger:hover,\n.btn-danger:focus {\n  background-color: #c12e2a;\n  background-position: 0 -15px;\n}\n.btn-danger:active,\n.btn-danger.active {\n  background-color: #c12e2a;\n  border-color: #b92c28;\n}\n.thumbnail,\n.img-thumbnail {\n  -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075);\n          box-shadow: 0 1px 2px rgba(0, 0, 0, .075);\n}\n.dropdown-menu > li > a:hover,\n.dropdown-menu > li > a:focus {\n  background-color: #e8e8e8;\n  background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);\n  background-image:         linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);\n  background-repeat: repeat-x;\n}\n.dropdown-menu > .active > a,\n.dropdown-menu > .active > a:hover,\n.dropdown-menu > .active > a:focus {\n  background-color: #357ebd;\n  background-image: -webkit-linear-gradient(top, #428bca 0%, #357ebd 100%);\n  background-image:         linear-gradient(to bottom, #428bca 0%, #357ebd 100%);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0);\n  background-repeat: repeat-x;\n}\n.navbar-default {\n  background-image: -webkit-linear-gradient(top, #fff 0%, #f8f8f8 100%);\n  background-image:         linear-gradient(to bottom, #fff 0%, #f8f8f8 100%);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);\n  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n  background-repeat: repeat-x;\n  border-radius: 4px;\n  -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075);\n          box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075);\n}\n.navbar-default .navbar-nav > .active > a {\n  background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f3f3f3 100%);\n  background-image:         linear-gradient(to bottom, #ebebeb 0%, #f3f3f3 100%);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff3f3f3', GradientType=0);\n  background-repeat: repeat-x;\n  -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075);\n          box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075);\n}\n.navbar-brand,\n.navbar-nav > li > a {\n  text-shadow: 0 1px 0 rgba(255, 255, 255, .25);\n}\n.navbar-inverse {\n  background-image: -webkit-linear-gradient(top, #3c3c3c 0%, #222 100%);\n  background-image:         linear-gradient(to bottom, #3c3c3c 0%, #222 100%);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);\n  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n  background-repeat: repeat-x;\n}\n.navbar-inverse .navbar-nav > .active > a {\n  background-image: -webkit-linear-gradient(top, #222 0%, #282828 100%);\n  background-image:         linear-gradient(to bottom, #222 0%, #282828 100%);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222', endColorstr='#ff282828', GradientType=0);\n  background-repeat: repeat-x;\n  -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25);\n          box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25);\n}\n.navbar-inverse .navbar-brand,\n.navbar-inverse .navbar-nav > li > a {\n  text-shadow: 0 -1px 0 rgba(0, 0, 0, .25);\n}\n.navbar-static-top,\n.navbar-fixed-top,\n.navbar-fixed-bottom {\n  border-radius: 0;\n}\n.alert {\n  text-shadow: 0 1px 0 rgba(255, 255, 255, .2);\n  -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05);\n          box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05);\n}\n.alert-success {\n  background-image: -webkit-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%);\n  background-image:         linear-gradient(to bottom, #dff0d8 0%, #c8e5bc 100%);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);\n  background-repeat: repeat-x;\n  border-color: #b2dba1;\n}\n.alert-info {\n  background-image: -webkit-linear-gradient(top, #d9edf7 0%, #b9def0 100%);\n  background-image:         linear-gradient(to bottom, #d9edf7 0%, #b9def0 100%);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);\n  background-repeat: repeat-x;\n  border-color: #9acfea;\n}\n.alert-warning {\n  background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%);\n  background-image:         linear-gradient(to bottom, #fcf8e3 0%, #f8efc0 100%);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);\n  background-repeat: repeat-x;\n  border-color: #f5e79e;\n}\n.alert-danger {\n  background-image: -webkit-linear-gradient(top, #f2dede 0%, #e7c3c3 100%);\n  background-image:         linear-gradient(to bottom, #f2dede 0%, #e7c3c3 100%);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);\n  background-repeat: repeat-x;\n  border-color: #dca7a7;\n}\n.progress {\n  background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%);\n  background-image:         linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);\n  background-repeat: repeat-x;\n}\n.progress-bar {\n  background-image: -webkit-linear-gradient(top, #428bca 0%, #3071a9 100%);\n  background-image:         linear-gradient(to bottom, #428bca 0%, #3071a9 100%);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3071a9', GradientType=0);\n  background-repeat: repeat-x;\n}\n.progress-bar-success {\n  background-image: -webkit-linear-gradient(top, #5cb85c 0%, #449d44 100%);\n  background-image:         linear-gradient(to bottom, #5cb85c 0%, #449d44 100%);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);\n  background-repeat: repeat-x;\n}\n.progress-bar-info {\n  background-image: -webkit-linear-gradient(top, #5bc0de 0%, #31b0d5 100%);\n  background-image:         linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);\n  background-repeat: repeat-x;\n}\n.progress-bar-warning {\n  background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #ec971f 100%);\n  background-image:         linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);\n  background-repeat: repeat-x;\n}\n.progress-bar-danger {\n  background-image: -webkit-linear-gradient(top, #d9534f 0%, #c9302c 100%);\n  background-image:         linear-gradient(to bottom, #d9534f 0%, #c9302c 100%);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);\n  background-repeat: repeat-x;\n}\n.list-group {\n  border-radius: 4px;\n  -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075);\n          box-shadow: 0 1px 2px rgba(0, 0, 0, .075);\n}\n.list-group-item.active,\n.list-group-item.active:hover,\n.list-group-item.active:focus {\n  text-shadow: 0 -1px 0 #3071a9;\n  background-image: -webkit-linear-gradient(top, #428bca 0%, #3278b3 100%);\n  background-image:         linear-gradient(to bottom, #428bca 0%, #3278b3 100%);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3278b3', GradientType=0);\n  background-repeat: repeat-x;\n  border-color: #3278b3;\n}\n.panel {\n  -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .05);\n          box-shadow: 0 1px 2px rgba(0, 0, 0, .05);\n}\n.panel-default > .panel-heading {\n  background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);\n  background-image:         linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);\n  background-repeat: repeat-x;\n}\n.panel-primary > .panel-heading {\n  background-image: -webkit-linear-gradient(top, #428bca 0%, #357ebd 100%);\n  background-image:         linear-gradient(to bottom, #428bca 0%, #357ebd 100%);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0);\n  background-repeat: repeat-x;\n}\n.panel-success > .panel-heading {\n  background-image: -webkit-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%);\n  background-image:         linear-gradient(to bottom, #dff0d8 0%, #d0e9c6 100%);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);\n  background-repeat: repeat-x;\n}\n.panel-info > .panel-heading {\n  background-image: -webkit-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%);\n  background-image:         linear-gradient(to bottom, #d9edf7 0%, #c4e3f3 100%);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);\n  background-repeat: repeat-x;\n}\n.panel-warning > .panel-heading {\n  background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%);\n  background-image:         linear-gradient(to bottom, #fcf8e3 0%, #faf2cc 100%);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);\n  background-repeat: repeat-x;\n}\n.panel-danger > .panel-heading {\n  background-image: -webkit-linear-gradient(top, #f2dede 0%, #ebcccc 100%);\n  background-image:         linear-gradient(to bottom, #f2dede 0%, #ebcccc 100%);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);\n  background-repeat: repeat-x;\n}\n.well {\n  background-image: -webkit-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%);\n  background-image:         linear-gradient(to bottom, #e8e8e8 0%, #f5f5f5 100%);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);\n  background-repeat: repeat-x;\n  border-color: #dcdcdc;\n  -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1);\n          box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1);\n}\n/*# sourceMappingURL=bootstrap-theme.css.map */\n"
  },
  {
    "path": "price_monitor/static/price_monitor/bootstrap/css/bootstrap.css",
    "content": "/*!\n * Bootstrap v3.1.1 (http://getbootstrap.com)\n * Copyright 2011-2014 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n */\n\n/*! normalize.css v3.0.0 | MIT License | git.io/normalize */\nhtml {\n  font-family: sans-serif;\n  -webkit-text-size-adjust: 100%;\n      -ms-text-size-adjust: 100%;\n}\nbody {\n  margin: 0;\n}\narticle,\naside,\ndetails,\nfigcaption,\nfigure,\nfooter,\nheader,\nhgroup,\nmain,\nnav,\nsection,\nsummary {\n  display: block;\n}\naudio,\ncanvas,\nprogress,\nvideo {\n  display: inline-block;\n  vertical-align: baseline;\n}\naudio:not([controls]) {\n  display: none;\n  height: 0;\n}\n[hidden],\ntemplate {\n  display: none;\n}\na {\n  background: transparent;\n}\na:active,\na:hover {\n  outline: 0;\n}\nabbr[title] {\n  border-bottom: 1px dotted;\n}\nb,\nstrong {\n  font-weight: bold;\n}\ndfn {\n  font-style: italic;\n}\nh1 {\n  margin: .67em 0;\n  font-size: 2em;\n}\nmark {\n  color: #000;\n  background: #ff0;\n}\nsmall {\n  font-size: 80%;\n}\nsub,\nsup {\n  position: relative;\n  font-size: 75%;\n  line-height: 0;\n  vertical-align: baseline;\n}\nsup {\n  top: -.5em;\n}\nsub {\n  bottom: -.25em;\n}\nimg {\n  border: 0;\n}\nsvg:not(:root) {\n  overflow: hidden;\n}\nfigure {\n  margin: 1em 40px;\n}\nhr {\n  height: 0;\n  -moz-box-sizing: content-box;\n       box-sizing: content-box;\n}\npre {\n  overflow: auto;\n}\ncode,\nkbd,\npre,\nsamp {\n  font-family: monospace, monospace;\n  font-size: 1em;\n}\nbutton,\ninput,\noptgroup,\nselect,\ntextarea {\n  margin: 0;\n  font: inherit;\n  color: inherit;\n}\nbutton {\n  overflow: visible;\n}\nbutton,\nselect {\n  text-transform: none;\n}\nbutton,\nhtml input[type=\"button\"],\ninput[type=\"reset\"],\ninput[type=\"submit\"] {\n  -webkit-appearance: button;\n  cursor: pointer;\n}\nbutton[disabled],\nhtml input[disabled] {\n  cursor: default;\n}\nbutton::-moz-focus-inner,\ninput::-moz-focus-inner {\n  padding: 0;\n  border: 0;\n}\ninput {\n  line-height: normal;\n}\ninput[type=\"checkbox\"],\ninput[type=\"radio\"] {\n  box-sizing: border-box;\n  padding: 0;\n}\ninput[type=\"number\"]::-webkit-inner-spin-button,\ninput[type=\"number\"]::-webkit-outer-spin-button {\n  height: auto;\n}\ninput[type=\"search\"] {\n  -webkit-box-sizing: content-box;\n     -moz-box-sizing: content-box;\n          box-sizing: content-box;\n  -webkit-appearance: textfield;\n}\ninput[type=\"search\"]::-webkit-search-cancel-button,\ninput[type=\"search\"]::-webkit-search-decoration {\n  -webkit-appearance: none;\n}\nfieldset {\n  padding: .35em .625em .75em;\n  margin: 0 2px;\n  border: 1px solid #c0c0c0;\n}\nlegend {\n  padding: 0;\n  border: 0;\n}\ntextarea {\n  overflow: auto;\n}\noptgroup {\n  font-weight: bold;\n}\ntable {\n  border-spacing: 0;\n  border-collapse: collapse;\n}\ntd,\nth {\n  padding: 0;\n}\n@media print {\n  * {\n    color: #000 !important;\n    text-shadow: none !important;\n    background: transparent !important;\n    box-shadow: none !important;\n  }\n  a,\n  a:visited {\n    text-decoration: underline;\n  }\n  a[href]:after {\n    content: \" (\" attr(href) \")\";\n  }\n  abbr[title]:after {\n    content: \" (\" attr(title) \")\";\n  }\n  a[href^=\"javascript:\"]:after,\n  a[href^=\"#\"]:after {\n    content: \"\";\n  }\n  pre,\n  blockquote {\n    border: 1px solid #999;\n\n    page-break-inside: avoid;\n  }\n  thead {\n    display: table-header-group;\n  }\n  tr,\n  img {\n    page-break-inside: avoid;\n  }\n  img {\n    max-width: 100% !important;\n  }\n  p,\n  h2,\n  h3 {\n    orphans: 3;\n    widows: 3;\n  }\n  h2,\n  h3 {\n    page-break-after: avoid;\n  }\n  select {\n    background: #fff !important;\n  }\n  .navbar {\n    display: none;\n  }\n  .table td,\n  .table th {\n    background-color: #fff !important;\n  }\n  .btn > .caret,\n  .dropup > .btn > .caret {\n    border-top-color: #000 !important;\n  }\n  .label {\n    border: 1px solid #000;\n  }\n  .table {\n    border-collapse: collapse !important;\n  }\n  .table-bordered th,\n  .table-bordered td {\n    border: 1px solid #ddd !important;\n  }\n}\n* {\n  -webkit-box-sizing: border-box;\n     -moz-box-sizing: border-box;\n          box-sizing: border-box;\n}\n*:before,\n*:after {\n  -webkit-box-sizing: border-box;\n     -moz-box-sizing: border-box;\n          box-sizing: border-box;\n}\nhtml {\n  font-size: 62.5%;\n\n  -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n}\nbody {\n  font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n  font-size: 14px;\n  line-height: 1.42857143;\n  color: #333;\n  background-color: #fff;\n}\ninput,\nbutton,\nselect,\ntextarea {\n  font-family: inherit;\n  font-size: inherit;\n  line-height: inherit;\n}\na {\n  color: #428bca;\n  text-decoration: none;\n}\na:hover,\na:focus {\n  color: #2a6496;\n  text-decoration: underline;\n}\na:focus {\n  outline: thin dotted;\n  outline: 5px auto -webkit-focus-ring-color;\n  outline-offset: -2px;\n}\nfigure {\n  margin: 0;\n}\nimg {\n  vertical-align: middle;\n}\n.img-responsive,\n.thumbnail > img,\n.thumbnail a > img,\n.carousel-inner > .item > img,\n.carousel-inner > .item > a > img {\n  display: block;\n  max-width: 100%;\n  height: auto;\n}\n.img-rounded {\n  border-radius: 6px;\n}\n.img-thumbnail {\n  display: inline-block;\n  max-width: 100%;\n  height: auto;\n  padding: 4px;\n  line-height: 1.42857143;\n  background-color: #fff;\n  border: 1px solid #ddd;\n  border-radius: 4px;\n  -webkit-transition: all .2s ease-in-out;\n          transition: all .2s ease-in-out;\n}\n.img-circle {\n  border-radius: 50%;\n}\nhr {\n  margin-top: 20px;\n  margin-bottom: 20px;\n  border: 0;\n  border-top: 1px solid #eee;\n}\n.sr-only {\n  position: absolute;\n  width: 1px;\n  height: 1px;\n  padding: 0;\n  margin: -1px;\n  overflow: hidden;\n  clip: rect(0, 0, 0, 0);\n  border: 0;\n}\nh1,\nh2,\nh3,\nh4,\nh5,\nh6,\n.h1,\n.h2,\n.h3,\n.h4,\n.h5,\n.h6 {\n  font-family: inherit;\n  font-weight: 500;\n  line-height: 1.1;\n  color: inherit;\n}\nh1 small,\nh2 small,\nh3 small,\nh4 small,\nh5 small,\nh6 small,\n.h1 small,\n.h2 small,\n.h3 small,\n.h4 small,\n.h5 small,\n.h6 small,\nh1 .small,\nh2 .small,\nh3 .small,\nh4 .small,\nh5 .small,\nh6 .small,\n.h1 .small,\n.h2 .small,\n.h3 .small,\n.h4 .small,\n.h5 .small,\n.h6 .small {\n  font-weight: normal;\n  line-height: 1;\n  color: #999;\n}\nh1,\n.h1,\nh2,\n.h2,\nh3,\n.h3 {\n  margin-top: 20px;\n  margin-bottom: 10px;\n}\nh1 small,\n.h1 small,\nh2 small,\n.h2 small,\nh3 small,\n.h3 small,\nh1 .small,\n.h1 .small,\nh2 .small,\n.h2 .small,\nh3 .small,\n.h3 .small {\n  font-size: 65%;\n}\nh4,\n.h4,\nh5,\n.h5,\nh6,\n.h6 {\n  margin-top: 10px;\n  margin-bottom: 10px;\n}\nh4 small,\n.h4 small,\nh5 small,\n.h5 small,\nh6 small,\n.h6 small,\nh4 .small,\n.h4 .small,\nh5 .small,\n.h5 .small,\nh6 .small,\n.h6 .small {\n  font-size: 75%;\n}\nh1,\n.h1 {\n  font-size: 36px;\n}\nh2,\n.h2 {\n  font-size: 30px;\n}\nh3,\n.h3 {\n  font-size: 24px;\n}\nh4,\n.h4 {\n  font-size: 18px;\n}\nh5,\n.h5 {\n  font-size: 14px;\n}\nh6,\n.h6 {\n  font-size: 12px;\n}\np {\n  margin: 0 0 10px;\n}\n.lead {\n  margin-bottom: 20px;\n  font-size: 16px;\n  font-weight: 200;\n  line-height: 1.4;\n}\n@media (min-width: 768px) {\n  .lead {\n    font-size: 21px;\n  }\n}\nsmall,\n.small {\n  font-size: 85%;\n}\ncite {\n  font-style: normal;\n}\n.text-left {\n  text-align: left;\n}\n.text-right {\n  text-align: right;\n}\n.text-center {\n  text-align: center;\n}\n.text-justify {\n  text-align: justify;\n}\n.text-muted {\n  color: #999;\n}\n.text-primary {\n  color: #428bca;\n}\na.text-primary:hover {\n  color: #3071a9;\n}\n.text-success {\n  color: #3c763d;\n}\na.text-success:hover {\n  color: #2b542c;\n}\n.text-info {\n  color: #31708f;\n}\na.text-info:hover {\n  color: #245269;\n}\n.text-warning {\n  color: #8a6d3b;\n}\na.text-warning:hover {\n  color: #66512c;\n}\n.text-danger {\n  color: #a94442;\n}\na.text-danger:hover {\n  color: #843534;\n}\n.bg-primary {\n  color: #fff;\n  background-color: #428bca;\n}\na.bg-primary:hover {\n  background-color: #3071a9;\n}\n.bg-success {\n  background-color: #dff0d8;\n}\na.bg-success:hover {\n  background-color: #c1e2b3;\n}\n.bg-info {\n  background-color: #d9edf7;\n}\na.bg-info:hover {\n  background-color: #afd9ee;\n}\n.bg-warning {\n  background-color: #fcf8e3;\n}\na.bg-warning:hover {\n  background-color: #f7ecb5;\n}\n.bg-danger {\n  background-color: #f2dede;\n}\na.bg-danger:hover {\n  background-color: #e4b9b9;\n}\n.page-header {\n  padding-bottom: 9px;\n  margin: 40px 0 20px;\n  border-bottom: 1px solid #eee;\n}\nul,\nol {\n  margin-top: 0;\n  margin-bottom: 10px;\n}\nul ul,\nol ul,\nul ol,\nol ol {\n  margin-bottom: 0;\n}\n.list-unstyled {\n  padding-left: 0;\n  list-style: none;\n}\n.list-inline {\n  padding-left: 0;\n  margin-left: -5px;\n  list-style: none;\n}\n.list-inline > li {\n  display: inline-block;\n  padding-right: 5px;\n  padding-left: 5px;\n}\ndl {\n  margin-top: 0;\n  margin-bottom: 20px;\n}\ndt,\ndd {\n  line-height: 1.42857143;\n}\ndt {\n  font-weight: bold;\n}\ndd {\n  margin-left: 0;\n}\n@media (min-width: 768px) {\n  .dl-horizontal dt {\n    float: left;\n    width: 160px;\n    overflow: hidden;\n    clear: left;\n    text-align: right;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n  }\n  .dl-horizontal dd {\n    margin-left: 180px;\n  }\n}\nabbr[title],\nabbr[data-original-title] {\n  cursor: help;\n  border-bottom: 1px dotted #999;\n}\n.initialism {\n  font-size: 90%;\n  text-transform: uppercase;\n}\nblockquote {\n  padding: 10px 20px;\n  margin: 0 0 20px;\n  font-size: 17.5px;\n  border-left: 5px solid #eee;\n}\nblockquote p:last-child,\nblockquote ul:last-child,\nblockquote ol:last-child {\n  margin-bottom: 0;\n}\nblockquote footer,\nblockquote small,\nblockquote .small {\n  display: block;\n  font-size: 80%;\n  line-height: 1.42857143;\n  color: #999;\n}\nblockquote footer:before,\nblockquote small:before,\nblockquote .small:before {\n  content: '\\2014 \\00A0';\n}\n.blockquote-reverse,\nblockquote.pull-right {\n  padding-right: 15px;\n  padding-left: 0;\n  text-align: right;\n  border-right: 5px solid #eee;\n  border-left: 0;\n}\n.blockquote-reverse footer:before,\nblockquote.pull-right footer:before,\n.blockquote-reverse small:before,\nblockquote.pull-right small:before,\n.blockquote-reverse .small:before,\nblockquote.pull-right .small:before {\n  content: '';\n}\n.blockquote-reverse footer:after,\nblockquote.pull-right footer:after,\n.blockquote-reverse small:after,\nblockquote.pull-right small:after,\n.blockquote-reverse .small:after,\nblockquote.pull-right .small:after {\n  content: '\\00A0 \\2014';\n}\nblockquote:before,\nblockquote:after {\n  content: \"\";\n}\naddress {\n  margin-bottom: 20px;\n  font-style: normal;\n  line-height: 1.42857143;\n}\ncode,\nkbd,\npre,\nsamp {\n  font-family: Menlo, Monaco, Consolas, \"Courier New\", monospace;\n}\ncode {\n  padding: 2px 4px;\n  font-size: 90%;\n  color: #c7254e;\n  white-space: nowrap;\n  background-color: #f9f2f4;\n  border-radius: 4px;\n}\nkbd {\n  padding: 2px 4px;\n  font-size: 90%;\n  color: #fff;\n  background-color: #333;\n  border-radius: 3px;\n  box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .25);\n}\npre {\n  display: block;\n  padding: 9.5px;\n  margin: 0 0 10px;\n  font-size: 13px;\n  line-height: 1.42857143;\n  color: #333;\n  word-break: break-all;\n  word-wrap: break-word;\n  background-color: #f5f5f5;\n  border: 1px solid #ccc;\n  border-radius: 4px;\n}\npre code {\n  padding: 0;\n  font-size: inherit;\n  color: inherit;\n  white-space: pre-wrap;\n  background-color: transparent;\n  border-radius: 0;\n}\n.pre-scrollable {\n  max-height: 340px;\n  overflow-y: scroll;\n}\n.container {\n  padding-right: 15px;\n  padding-left: 15px;\n  margin-right: auto;\n  margin-left: auto;\n}\n@media (min-width: 768px) {\n  .container {\n    width: 750px;\n  }\n}\n@media (min-width: 992px) {\n  .container {\n    width: 970px;\n  }\n}\n@media (min-width: 1200px) {\n  .container {\n    width: 1170px;\n  }\n}\n.container-fluid {\n  padding-right: 15px;\n  padding-left: 15px;\n  margin-right: auto;\n  margin-left: auto;\n}\n.row {\n  margin-right: -15px;\n  margin-left: -15px;\n}\n.col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12 {\n  position: relative;\n  min-height: 1px;\n  padding-right: 15px;\n  padding-left: 15px;\n}\n.col-xs-1, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9, .col-xs-10, .col-xs-11, .col-xs-12 {\n  float: left;\n}\n.col-xs-12 {\n  width: 100%;\n}\n.col-xs-11 {\n  width: 91.66666667%;\n}\n.col-xs-10 {\n  width: 83.33333333%;\n}\n.col-xs-9 {\n  width: 75%;\n}\n.col-xs-8 {\n  width: 66.66666667%;\n}\n.col-xs-7 {\n  width: 58.33333333%;\n}\n.col-xs-6 {\n  width: 50%;\n}\n.col-xs-5 {\n  width: 41.66666667%;\n}\n.col-xs-4 {\n  width: 33.33333333%;\n}\n.col-xs-3 {\n  width: 25%;\n}\n.col-xs-2 {\n  width: 16.66666667%;\n}\n.col-xs-1 {\n  width: 8.33333333%;\n}\n.col-xs-pull-12 {\n  right: 100%;\n}\n.col-xs-pull-11 {\n  right: 91.66666667%;\n}\n.col-xs-pull-10 {\n  right: 83.33333333%;\n}\n.col-xs-pull-9 {\n  right: 75%;\n}\n.col-xs-pull-8 {\n  right: 66.66666667%;\n}\n.col-xs-pull-7 {\n  right: 58.33333333%;\n}\n.col-xs-pull-6 {\n  right: 50%;\n}\n.col-xs-pull-5 {\n  right: 41.66666667%;\n}\n.col-xs-pull-4 {\n  right: 33.33333333%;\n}\n.col-xs-pull-3 {\n  right: 25%;\n}\n.col-xs-pull-2 {\n  right: 16.66666667%;\n}\n.col-xs-pull-1 {\n  right: 8.33333333%;\n}\n.col-xs-pull-0 {\n  right: 0;\n}\n.col-xs-push-12 {\n  left: 100%;\n}\n.col-xs-push-11 {\n  left: 91.66666667%;\n}\n.col-xs-push-10 {\n  left: 83.33333333%;\n}\n.col-xs-push-9 {\n  left: 75%;\n}\n.col-xs-push-8 {\n  left: 66.66666667%;\n}\n.col-xs-push-7 {\n  left: 58.33333333%;\n}\n.col-xs-push-6 {\n  left: 50%;\n}\n.col-xs-push-5 {\n  left: 41.66666667%;\n}\n.col-xs-push-4 {\n  left: 33.33333333%;\n}\n.col-xs-push-3 {\n  left: 25%;\n}\n.col-xs-push-2 {\n  left: 16.66666667%;\n}\n.col-xs-push-1 {\n  left: 8.33333333%;\n}\n.col-xs-push-0 {\n  left: 0;\n}\n.col-xs-offset-12 {\n  margin-left: 100%;\n}\n.col-xs-offset-11 {\n  margin-left: 91.66666667%;\n}\n.col-xs-offset-10 {\n  margin-left: 83.33333333%;\n}\n.col-xs-offset-9 {\n  margin-left: 75%;\n}\n.col-xs-offset-8 {\n  margin-left: 66.66666667%;\n}\n.col-xs-offset-7 {\n  margin-left: 58.33333333%;\n}\n.col-xs-offset-6 {\n  margin-left: 50%;\n}\n.col-xs-offset-5 {\n  margin-left: 41.66666667%;\n}\n.col-xs-offset-4 {\n  margin-left: 33.33333333%;\n}\n.col-xs-offset-3 {\n  margin-left: 25%;\n}\n.col-xs-offset-2 {\n  margin-left: 16.66666667%;\n}\n.col-xs-offset-1 {\n  margin-left: 8.33333333%;\n}\n.col-xs-offset-0 {\n  margin-left: 0;\n}\n@media (min-width: 768px) {\n  .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12 {\n    float: left;\n  }\n  .col-sm-12 {\n    width: 100%;\n  }\n  .col-sm-11 {\n    width: 91.66666667%;\n  }\n  .col-sm-10 {\n    width: 83.33333333%;\n  }\n  .col-sm-9 {\n    width: 75%;\n  }\n  .col-sm-8 {\n    width: 66.66666667%;\n  }\n  .col-sm-7 {\n    width: 58.33333333%;\n  }\n  .col-sm-6 {\n    width: 50%;\n  }\n  .col-sm-5 {\n    width: 41.66666667%;\n  }\n  .col-sm-4 {\n    width: 33.33333333%;\n  }\n  .col-sm-3 {\n    width: 25%;\n  }\n  .col-sm-2 {\n    width: 16.66666667%;\n  }\n  .col-sm-1 {\n    width: 8.33333333%;\n  }\n  .col-sm-pull-12 {\n    right: 100%;\n  }\n  .col-sm-pull-11 {\n    right: 91.66666667%;\n  }\n  .col-sm-pull-10 {\n    right: 83.33333333%;\n  }\n  .col-sm-pull-9 {\n    right: 75%;\n  }\n  .col-sm-pull-8 {\n    right: 66.66666667%;\n  }\n  .col-sm-pull-7 {\n    right: 58.33333333%;\n  }\n  .col-sm-pull-6 {\n    right: 50%;\n  }\n  .col-sm-pull-5 {\n    right: 41.66666667%;\n  }\n  .col-sm-pull-4 {\n    right: 33.33333333%;\n  }\n  .col-sm-pull-3 {\n    right: 25%;\n  }\n  .col-sm-pull-2 {\n    right: 16.66666667%;\n  }\n  .col-sm-pull-1 {\n    right: 8.33333333%;\n  }\n  .col-sm-pull-0 {\n    right: 0;\n  }\n  .col-sm-push-12 {\n    left: 100%;\n  }\n  .col-sm-push-11 {\n    left: 91.66666667%;\n  }\n  .col-sm-push-10 {\n    left: 83.33333333%;\n  }\n  .col-sm-push-9 {\n    left: 75%;\n  }\n  .col-sm-push-8 {\n    left: 66.66666667%;\n  }\n  .col-sm-push-7 {\n    left: 58.33333333%;\n  }\n  .col-sm-push-6 {\n    left: 50%;\n  }\n  .col-sm-push-5 {\n    left: 41.66666667%;\n  }\n  .col-sm-push-4 {\n    left: 33.33333333%;\n  }\n  .col-sm-push-3 {\n    left: 25%;\n  }\n  .col-sm-push-2 {\n    left: 16.66666667%;\n  }\n  .col-sm-push-1 {\n    left: 8.33333333%;\n  }\n  .col-sm-push-0 {\n    left: 0;\n  }\n  .col-sm-offset-12 {\n    margin-left: 100%;\n  }\n  .col-sm-offset-11 {\n    margin-left: 91.66666667%;\n  }\n  .col-sm-offset-10 {\n    margin-left: 83.33333333%;\n  }\n  .col-sm-offset-9 {\n    margin-left: 75%;\n  }\n  .col-sm-offset-8 {\n    margin-left: 66.66666667%;\n  }\n  .col-sm-offset-7 {\n    margin-left: 58.33333333%;\n  }\n  .col-sm-offset-6 {\n    margin-left: 50%;\n  }\n  .col-sm-offset-5 {\n    margin-left: 41.66666667%;\n  }\n  .col-sm-offset-4 {\n    margin-left: 33.33333333%;\n  }\n  .col-sm-offset-3 {\n    margin-left: 25%;\n  }\n  .col-sm-offset-2 {\n    margin-left: 16.66666667%;\n  }\n  .col-sm-offset-1 {\n    margin-left: 8.33333333%;\n  }\n  .col-sm-offset-0 {\n    margin-left: 0;\n  }\n}\n@media (min-width: 992px) {\n  .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12 {\n    float: left;\n  }\n  .col-md-12 {\n    width: 100%;\n  }\n  .col-md-11 {\n    width: 91.66666667%;\n  }\n  .col-md-10 {\n    width: 83.33333333%;\n  }\n  .col-md-9 {\n    width: 75%;\n  }\n  .col-md-8 {\n    width: 66.66666667%;\n  }\n  .col-md-7 {\n    width: 58.33333333%;\n  }\n  .col-md-6 {\n    width: 50%;\n  }\n  .col-md-5 {\n    width: 41.66666667%;\n  }\n  .col-md-4 {\n    width: 33.33333333%;\n  }\n  .col-md-3 {\n    width: 25%;\n  }\n  .col-md-2 {\n    width: 16.66666667%;\n  }\n  .col-md-1 {\n    width: 8.33333333%;\n  }\n  .col-md-pull-12 {\n    right: 100%;\n  }\n  .col-md-pull-11 {\n    right: 91.66666667%;\n  }\n  .col-md-pull-10 {\n    right: 83.33333333%;\n  }\n  .col-md-pull-9 {\n    right: 75%;\n  }\n  .col-md-pull-8 {\n    right: 66.66666667%;\n  }\n  .col-md-pull-7 {\n    right: 58.33333333%;\n  }\n  .col-md-pull-6 {\n    right: 50%;\n  }\n  .col-md-pull-5 {\n    right: 41.66666667%;\n  }\n  .col-md-pull-4 {\n    right: 33.33333333%;\n  }\n  .col-md-pull-3 {\n    right: 25%;\n  }\n  .col-md-pull-2 {\n    right: 16.66666667%;\n  }\n  .col-md-pull-1 {\n    right: 8.33333333%;\n  }\n  .col-md-pull-0 {\n    right: 0;\n  }\n  .col-md-push-12 {\n    left: 100%;\n  }\n  .col-md-push-11 {\n    left: 91.66666667%;\n  }\n  .col-md-push-10 {\n    left: 83.33333333%;\n  }\n  .col-md-push-9 {\n    left: 75%;\n  }\n  .col-md-push-8 {\n    left: 66.66666667%;\n  }\n  .col-md-push-7 {\n    left: 58.33333333%;\n  }\n  .col-md-push-6 {\n    left: 50%;\n  }\n  .col-md-push-5 {\n    left: 41.66666667%;\n  }\n  .col-md-push-4 {\n    left: 33.33333333%;\n  }\n  .col-md-push-3 {\n    left: 25%;\n  }\n  .col-md-push-2 {\n    left: 16.66666667%;\n  }\n  .col-md-push-1 {\n    left: 8.33333333%;\n  }\n  .col-md-push-0 {\n    left: 0;\n  }\n  .col-md-offset-12 {\n    margin-left: 100%;\n  }\n  .col-md-offset-11 {\n    margin-left: 91.66666667%;\n  }\n  .col-md-offset-10 {\n    margin-left: 83.33333333%;\n  }\n  .col-md-offset-9 {\n    margin-left: 75%;\n  }\n  .col-md-offset-8 {\n    margin-left: 66.66666667%;\n  }\n  .col-md-offset-7 {\n    margin-left: 58.33333333%;\n  }\n  .col-md-offset-6 {\n    margin-left: 50%;\n  }\n  .col-md-offset-5 {\n    margin-left: 41.66666667%;\n  }\n  .col-md-offset-4 {\n    margin-left: 33.33333333%;\n  }\n  .col-md-offset-3 {\n    margin-left: 25%;\n  }\n  .col-md-offset-2 {\n    margin-left: 16.66666667%;\n  }\n  .col-md-offset-1 {\n    margin-left: 8.33333333%;\n  }\n  .col-md-offset-0 {\n    margin-left: 0;\n  }\n}\n@media (min-width: 1200px) {\n  .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12 {\n    float: left;\n  }\n  .col-lg-12 {\n    width: 100%;\n  }\n  .col-lg-11 {\n    width: 91.66666667%;\n  }\n  .col-lg-10 {\n    width: 83.33333333%;\n  }\n  .col-lg-9 {\n    width: 75%;\n  }\n  .col-lg-8 {\n    width: 66.66666667%;\n  }\n  .col-lg-7 {\n    width: 58.33333333%;\n  }\n  .col-lg-6 {\n    width: 50%;\n  }\n  .col-lg-5 {\n    width: 41.66666667%;\n  }\n  .col-lg-4 {\n    width: 33.33333333%;\n  }\n  .col-lg-3 {\n    width: 25%;\n  }\n  .col-lg-2 {\n    width: 16.66666667%;\n  }\n  .col-lg-1 {\n    width: 8.33333333%;\n  }\n  .col-lg-pull-12 {\n    right: 100%;\n  }\n  .col-lg-pull-11 {\n    right: 91.66666667%;\n  }\n  .col-lg-pull-10 {\n    right: 83.33333333%;\n  }\n  .col-lg-pull-9 {\n    right: 75%;\n  }\n  .col-lg-pull-8 {\n    right: 66.66666667%;\n  }\n  .col-lg-pull-7 {\n    right: 58.33333333%;\n  }\n  .col-lg-pull-6 {\n    right: 50%;\n  }\n  .col-lg-pull-5 {\n    right: 41.66666667%;\n  }\n  .col-lg-pull-4 {\n    right: 33.33333333%;\n  }\n  .col-lg-pull-3 {\n    right: 25%;\n  }\n  .col-lg-pull-2 {\n    right: 16.66666667%;\n  }\n  .col-lg-pull-1 {\n    right: 8.33333333%;\n  }\n  .col-lg-pull-0 {\n    right: 0;\n  }\n  .col-lg-push-12 {\n    left: 100%;\n  }\n  .col-lg-push-11 {\n    left: 91.66666667%;\n  }\n  .col-lg-push-10 {\n    left: 83.33333333%;\n  }\n  .col-lg-push-9 {\n    left: 75%;\n  }\n  .col-lg-push-8 {\n    left: 66.66666667%;\n  }\n  .col-lg-push-7 {\n    left: 58.33333333%;\n  }\n  .col-lg-push-6 {\n    left: 50%;\n  }\n  .col-lg-push-5 {\n    left: 41.66666667%;\n  }\n  .col-lg-push-4 {\n    left: 33.33333333%;\n  }\n  .col-lg-push-3 {\n    left: 25%;\n  }\n  .col-lg-push-2 {\n    left: 16.66666667%;\n  }\n  .col-lg-push-1 {\n    left: 8.33333333%;\n  }\n  .col-lg-push-0 {\n    left: 0;\n  }\n  .col-lg-offset-12 {\n    margin-left: 100%;\n  }\n  .col-lg-offset-11 {\n    margin-left: 91.66666667%;\n  }\n  .col-lg-offset-10 {\n    margin-left: 83.33333333%;\n  }\n  .col-lg-offset-9 {\n    margin-left: 75%;\n  }\n  .col-lg-offset-8 {\n    margin-left: 66.66666667%;\n  }\n  .col-lg-offset-7 {\n    margin-left: 58.33333333%;\n  }\n  .col-lg-offset-6 {\n    margin-left: 50%;\n  }\n  .col-lg-offset-5 {\n    margin-left: 41.66666667%;\n  }\n  .col-lg-offset-4 {\n    margin-left: 33.33333333%;\n  }\n  .col-lg-offset-3 {\n    margin-left: 25%;\n  }\n  .col-lg-offset-2 {\n    margin-left: 16.66666667%;\n  }\n  .col-lg-offset-1 {\n    margin-left: 8.33333333%;\n  }\n  .col-lg-offset-0 {\n    margin-left: 0;\n  }\n}\ntable {\n  max-width: 100%;\n  background-color: transparent;\n}\nth {\n  text-align: left;\n}\n.table {\n  width: 100%;\n  margin-bottom: 20px;\n}\n.table > thead > tr > th,\n.table > tbody > tr > th,\n.table > tfoot > tr > th,\n.table > thead > tr > td,\n.table > tbody > tr > td,\n.table > tfoot > tr > td {\n  padding: 8px;\n  line-height: 1.42857143;\n  vertical-align: top;\n  border-top: 1px solid #ddd;\n}\n.table > thead > tr > th {\n  vertical-align: bottom;\n  border-bottom: 2px solid #ddd;\n}\n.table > caption + thead > tr:first-child > th,\n.table > colgroup + thead > tr:first-child > th,\n.table > thead:first-child > tr:first-child > th,\n.table > caption + thead > tr:first-child > td,\n.table > colgroup + thead > tr:first-child > td,\n.table > thead:first-child > tr:first-child > td {\n  border-top: 0;\n}\n.table > tbody + tbody {\n  border-top: 2px solid #ddd;\n}\n.table .table {\n  background-color: #fff;\n}\n.table-condensed > thead > tr > th,\n.table-condensed > tbody > tr > th,\n.table-condensed > tfoot > tr > th,\n.table-condensed > thead > tr > td,\n.table-condensed > tbody > tr > td,\n.table-condensed > tfoot > tr > td {\n  padding: 5px;\n}\n.table-bordered {\n  border: 1px solid #ddd;\n}\n.table-bordered > thead > tr > th,\n.table-bordered > tbody > tr > th,\n.table-bordered > tfoot > tr > th,\n.table-bordered > thead > tr > td,\n.table-bordered > tbody > tr > td,\n.table-bordered > tfoot > tr > td {\n  border: 1px solid #ddd;\n}\n.table-bordered > thead > tr > th,\n.table-bordered > thead > tr > td {\n  border-bottom-width: 2px;\n}\n.table-striped > tbody > tr:nth-child(odd) > td,\n.table-striped > tbody > tr:nth-child(odd) > th {\n  background-color: #f9f9f9;\n}\n.table-hover > tbody > tr:hover > td,\n.table-hover > tbody > tr:hover > th {\n  background-color: #f5f5f5;\n}\ntable col[class*=\"col-\"] {\n  position: static;\n  display: table-column;\n  float: none;\n}\ntable td[class*=\"col-\"],\ntable th[class*=\"col-\"] {\n  position: static;\n  display: table-cell;\n  float: none;\n}\n.table > thead > tr > td.active,\n.table > tbody > tr > td.active,\n.table > tfoot > tr > td.active,\n.table > thead > tr > th.active,\n.table > tbody > tr > th.active,\n.table > tfoot > tr > th.active,\n.table > thead > tr.active > td,\n.table > tbody > tr.active > td,\n.table > tfoot > tr.active > td,\n.table > thead > tr.active > th,\n.table > tbody > tr.active > th,\n.table > tfoot > tr.active > th {\n  background-color: #f5f5f5;\n}\n.table-hover > tbody > tr > td.active:hover,\n.table-hover > tbody > tr > th.active:hover,\n.table-hover > tbody > tr.active:hover > td,\n.table-hover > tbody > tr.active:hover > th {\n  background-color: #e8e8e8;\n}\n.table > thead > tr > td.success,\n.table > tbody > tr > td.success,\n.table > tfoot > tr > td.success,\n.table > thead > tr > th.success,\n.table > tbody > tr > th.success,\n.table > tfoot > tr > th.success,\n.table > thead > tr.success > td,\n.table > tbody > tr.success > td,\n.table > tfoot > tr.success > td,\n.table > thead > tr.success > th,\n.table > tbody > tr.success > th,\n.table > tfoot > tr.success > th {\n  background-color: #dff0d8;\n}\n.table-hover > tbody > tr > td.success:hover,\n.table-hover > tbody > tr > th.success:hover,\n.table-hover > tbody > tr.success:hover > td,\n.table-hover > tbody > tr.success:hover > th {\n  background-color: #d0e9c6;\n}\n.table > thead > tr > td.info,\n.table > tbody > tr > td.info,\n.table > tfoot > tr > td.info,\n.table > thead > tr > th.info,\n.table > tbody > tr > th.info,\n.table > tfoot > tr > th.info,\n.table > thead > tr.info > td,\n.table > tbody > tr.info > td,\n.table > tfoot > tr.info > td,\n.table > thead > tr.info > th,\n.table > tbody > tr.info > th,\n.table > tfoot > tr.info > th {\n  background-color: #d9edf7;\n}\n.table-hover > tbody > tr > td.info:hover,\n.table-hover > tbody > tr > th.info:hover,\n.table-hover > tbody > tr.info:hover > td,\n.table-hover > tbody > tr.info:hover > th {\n  background-color: #c4e3f3;\n}\n.table > thead > tr > td.warning,\n.table > tbody > tr > td.warning,\n.table > tfoot > tr > td.warning,\n.table > thead > tr > th.warning,\n.table > tbody > tr > th.warning,\n.table > tfoot > tr > th.warning,\n.table > thead > tr.warning > td,\n.table > tbody > tr.warning > td,\n.table > tfoot > tr.warning > td,\n.table > thead > tr.warning > th,\n.table > tbody > tr.warning > th,\n.table > tfoot > tr.warning > th {\n  background-color: #fcf8e3;\n}\n.table-hover > tbody > tr > td.warning:hover,\n.table-hover > tbody > tr > th.warning:hover,\n.table-hover > tbody > tr.warning:hover > td,\n.table-hover > tbody > tr.warning:hover > th {\n  background-color: #faf2cc;\n}\n.table > thead > tr > td.danger,\n.table > tbody > tr > td.danger,\n.table > tfoot > tr > td.danger,\n.table > thead > tr > th.danger,\n.table > tbody > tr > th.danger,\n.table > tfoot > tr > th.danger,\n.table > thead > tr.danger > td,\n.table > tbody > tr.danger > td,\n.table > tfoot > tr.danger > td,\n.table > thead > tr.danger > th,\n.table > tbody > tr.danger > th,\n.table > tfoot > tr.danger > th {\n  background-color: #f2dede;\n}\n.table-hover > tbody > tr > td.danger:hover,\n.table-hover > tbody > tr > th.danger:hover,\n.table-hover > tbody > tr.danger:hover > td,\n.table-hover > tbody > tr.danger:hover > th {\n  background-color: #ebcccc;\n}\n@media (max-width: 767px) {\n  .table-responsive {\n    width: 100%;\n    margin-bottom: 15px;\n    overflow-x: scroll;\n    overflow-y: hidden;\n    -webkit-overflow-scrolling: touch;\n    -ms-overflow-style: -ms-autohiding-scrollbar;\n    border: 1px solid #ddd;\n  }\n  .table-responsive > .table {\n    margin-bottom: 0;\n  }\n  .table-responsive > .table > thead > tr > th,\n  .table-responsive > .table > tbody > tr > th,\n  .table-responsive > .table > tfoot > tr > th,\n  .table-responsive > .table > thead > tr > td,\n  .table-responsive > .table > tbody > tr > td,\n  .table-responsive > .table > tfoot > tr > td {\n    white-space: nowrap;\n  }\n  .table-responsive > .table-bordered {\n    border: 0;\n  }\n  .table-responsive > .table-bordered > thead > tr > th:first-child,\n  .table-responsive > .table-bordered > tbody > tr > th:first-child,\n  .table-responsive > .table-bordered > tfoot > tr > th:first-child,\n  .table-responsive > .table-bordered > thead > tr > td:first-child,\n  .table-responsive > .table-bordered > tbody > tr > td:first-child,\n  .table-responsive > .table-bordered > tfoot > tr > td:first-child {\n    border-left: 0;\n  }\n  .table-responsive > .table-bordered > thead > tr > th:last-child,\n  .table-responsive > .table-bordered > tbody > tr > th:last-child,\n  .table-responsive > .table-bordered > tfoot > tr > th:last-child,\n  .table-responsive > .table-bordered > thead > tr > td:last-child,\n  .table-responsive > .table-bordered > tbody > tr > td:last-child,\n  .table-responsive > .table-bordered > tfoot > tr > td:last-child {\n    border-right: 0;\n  }\n  .table-responsive > .table-bordered > tbody > tr:last-child > th,\n  .table-responsive > .table-bordered > tfoot > tr:last-child > th,\n  .table-responsive > .table-bordered > tbody > tr:last-child > td,\n  .table-responsive > .table-bordered > tfoot > tr:last-child > td {\n    border-bottom: 0;\n  }\n}\nfieldset {\n  min-width: 0;\n  padding: 0;\n  margin: 0;\n  border: 0;\n}\nlegend {\n  display: block;\n  width: 100%;\n  padding: 0;\n  margin-bottom: 20px;\n  font-size: 21px;\n  line-height: inherit;\n  color: #333;\n  border: 0;\n  border-bottom: 1px solid #e5e5e5;\n}\nlabel {\n  display: inline-block;\n  margin-bottom: 5px;\n  font-weight: bold;\n}\ninput[type=\"search\"] {\n  -webkit-box-sizing: border-box;\n     -moz-box-sizing: border-box;\n          box-sizing: border-box;\n}\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n  margin: 4px 0 0;\n  margin-top: 1px \\9;\n  /* IE8-9 */\n  line-height: normal;\n}\ninput[type=\"file\"] {\n  display: block;\n}\ninput[type=\"range\"] {\n  display: block;\n  width: 100%;\n}\nselect[multiple],\nselect[size] {\n  height: auto;\n}\ninput[type=\"file\"]:focus,\ninput[type=\"radio\"]:focus,\ninput[type=\"checkbox\"]:focus {\n  outline: thin dotted;\n  outline: 5px auto -webkit-focus-ring-color;\n  outline-offset: -2px;\n}\noutput {\n  display: block;\n  padding-top: 7px;\n  font-size: 14px;\n  line-height: 1.42857143;\n  color: #555;\n}\n.form-control {\n  display: block;\n  width: 100%;\n  height: 34px;\n  padding: 6px 12px;\n  font-size: 14px;\n  line-height: 1.42857143;\n  color: #555;\n  background-color: #fff;\n  background-image: none;\n  border: 1px solid #ccc;\n  border-radius: 4px;\n  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);\n          box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);\n  -webkit-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;\n          transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;\n}\n.form-control:focus {\n  border-color: #66afe9;\n  outline: 0;\n  -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, .6);\n          box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, .6);\n}\n.form-control::-moz-placeholder {\n  color: #999;\n  opacity: 1;\n}\n.form-control:-ms-input-placeholder {\n  color: #999;\n}\n.form-control::-webkit-input-placeholder {\n  color: #999;\n}\n.form-control[disabled],\n.form-control[readonly],\nfieldset[disabled] .form-control {\n  cursor: not-allowed;\n  background-color: #eee;\n  opacity: 1;\n}\ntextarea.form-control {\n  height: auto;\n}\ninput[type=\"search\"] {\n  -webkit-appearance: none;\n}\ninput[type=\"date\"] {\n  line-height: 34px;\n}\n.form-group {\n  margin-bottom: 15px;\n}\n.radio,\n.checkbox {\n  display: block;\n  min-height: 20px;\n  padding-left: 20px;\n  margin-top: 10px;\n  margin-bottom: 10px;\n}\n.radio label,\n.checkbox label {\n  display: inline;\n  font-weight: normal;\n  cursor: pointer;\n}\n.radio input[type=\"radio\"],\n.radio-inline input[type=\"radio\"],\n.checkbox input[type=\"checkbox\"],\n.checkbox-inline input[type=\"checkbox\"] {\n  float: left;\n  margin-left: -20px;\n}\n.radio + .radio,\n.checkbox + .checkbox {\n  margin-top: -5px;\n}\n.radio-inline,\n.checkbox-inline {\n  display: inline-block;\n  padding-left: 20px;\n  margin-bottom: 0;\n  font-weight: normal;\n  vertical-align: middle;\n  cursor: pointer;\n}\n.radio-inline + .radio-inline,\n.checkbox-inline + .checkbox-inline {\n  margin-top: 0;\n  margin-left: 10px;\n}\ninput[type=\"radio\"][disabled],\ninput[type=\"checkbox\"][disabled],\n.radio[disabled],\n.radio-inline[disabled],\n.checkbox[disabled],\n.checkbox-inline[disabled],\nfieldset[disabled] input[type=\"radio\"],\nfieldset[disabled] input[type=\"checkbox\"],\nfieldset[disabled] .radio,\nfieldset[disabled] .radio-inline,\nfieldset[disabled] .checkbox,\nfieldset[disabled] .checkbox-inline {\n  cursor: not-allowed;\n}\n.input-sm {\n  height: 30px;\n  padding: 5px 10px;\n  font-size: 12px;\n  line-height: 1.5;\n  border-radius: 3px;\n}\nselect.input-sm {\n  height: 30px;\n  line-height: 30px;\n}\ntextarea.input-sm,\nselect[multiple].input-sm {\n  height: auto;\n}\n.input-lg {\n  height: 46px;\n  padding: 10px 16px;\n  font-size: 18px;\n  line-height: 1.33;\n  border-radius: 6px;\n}\nselect.input-lg {\n  height: 46px;\n  line-height: 46px;\n}\ntextarea.input-lg,\nselect[multiple].input-lg {\n  height: auto;\n}\n.has-feedback {\n  position: relative;\n}\n.has-feedback .form-control {\n  padding-right: 42.5px;\n}\n.has-feedback .form-control-feedback {\n  position: absolute;\n  top: 25px;\n  right: 0;\n  display: block;\n  width: 34px;\n  height: 34px;\n  line-height: 34px;\n  text-align: center;\n}\n.has-success .help-block,\n.has-success .control-label,\n.has-success .radio,\n.has-success .checkbox,\n.has-success .radio-inline,\n.has-success .checkbox-inline {\n  color: #3c763d;\n}\n.has-success .form-control {\n  border-color: #3c763d;\n  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);\n          box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);\n}\n.has-success .form-control:focus {\n  border-color: #2b542c;\n  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #67b168;\n          box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #67b168;\n}\n.has-success .input-group-addon {\n  color: #3c763d;\n  background-color: #dff0d8;\n  border-color: #3c763d;\n}\n.has-success .form-control-feedback {\n  color: #3c763d;\n}\n.has-warning .help-block,\n.has-warning .control-label,\n.has-warning .radio,\n.has-warning .checkbox,\n.has-warning .radio-inline,\n.has-warning .checkbox-inline {\n  color: #8a6d3b;\n}\n.has-warning .form-control {\n  border-color: #8a6d3b;\n  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);\n          box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);\n}\n.has-warning .form-control:focus {\n  border-color: #66512c;\n  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #c0a16b;\n          box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #c0a16b;\n}\n.has-warning .input-group-addon {\n  color: #8a6d3b;\n  background-color: #fcf8e3;\n  border-color: #8a6d3b;\n}\n.has-warning .form-control-feedback {\n  color: #8a6d3b;\n}\n.has-error .help-block,\n.has-error .control-label,\n.has-error .radio,\n.has-error .checkbox,\n.has-error .radio-inline,\n.has-error .checkbox-inline {\n  color: #a94442;\n}\n.has-error .form-control {\n  border-color: #a94442;\n  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);\n          box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);\n}\n.has-error .form-control:focus {\n  border-color: #843534;\n  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #ce8483;\n          box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #ce8483;\n}\n.has-error .input-group-addon {\n  color: #a94442;\n  background-color: #f2dede;\n  border-color: #a94442;\n}\n.has-error .form-control-feedback {\n  color: #a94442;\n}\n.form-control-static {\n  margin-bottom: 0;\n}\n.help-block {\n  display: block;\n  margin-top: 5px;\n  margin-bottom: 10px;\n  color: #737373;\n}\n@media (min-width: 768px) {\n  .form-inline .form-group {\n    display: inline-block;\n    margin-bottom: 0;\n    vertical-align: middle;\n  }\n  .form-inline .form-control {\n    display: inline-block;\n    width: auto;\n    vertical-align: middle;\n  }\n  .form-inline .input-group > .form-control {\n    width: 100%;\n  }\n  .form-inline .control-label {\n    margin-bottom: 0;\n    vertical-align: middle;\n  }\n  .form-inline .radio,\n  .form-inline .checkbox {\n    display: inline-block;\n    padding-left: 0;\n    margin-top: 0;\n    margin-bottom: 0;\n    vertical-align: middle;\n  }\n  .form-inline .radio input[type=\"radio\"],\n  .form-inline .checkbox input[type=\"checkbox\"] {\n    float: none;\n    margin-left: 0;\n  }\n  .form-inline .has-feedback .form-control-feedback {\n    top: 0;\n  }\n}\n.form-horizontal .control-label,\n.form-horizontal .radio,\n.form-horizontal .checkbox,\n.form-horizontal .radio-inline,\n.form-horizontal .checkbox-inline {\n  padding-top: 7px;\n  margin-top: 0;\n  margin-bottom: 0;\n}\n.form-horizontal .radio,\n.form-horizontal .checkbox {\n  min-height: 27px;\n}\n.form-horizontal .form-group {\n  margin-right: -15px;\n  margin-left: -15px;\n}\n.form-horizontal .form-control-static {\n  padding-top: 7px;\n}\n@media (min-width: 768px) {\n  .form-horizontal .control-label {\n    text-align: right;\n  }\n}\n.form-horizontal .has-feedback .form-control-feedback {\n  top: 0;\n  right: 15px;\n}\n.btn {\n  display: inline-block;\n  padding: 6px 12px;\n  margin-bottom: 0;\n  font-size: 14px;\n  font-weight: normal;\n  line-height: 1.42857143;\n  text-align: center;\n  white-space: nowrap;\n  vertical-align: middle;\n  cursor: pointer;\n  -webkit-user-select: none;\n     -moz-user-select: none;\n      -ms-user-select: none;\n          user-select: none;\n  background-image: none;\n  border: 1px solid transparent;\n  border-radius: 4px;\n}\n.btn:focus,\n.btn:active:focus,\n.btn.active:focus {\n  outline: thin dotted;\n  outline: 5px auto -webkit-focus-ring-color;\n  outline-offset: -2px;\n}\n.btn:hover,\n.btn:focus {\n  color: #333;\n  text-decoration: none;\n}\n.btn:active,\n.btn.active {\n  background-image: none;\n  outline: 0;\n  -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);\n          box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);\n}\n.btn.disabled,\n.btn[disabled],\nfieldset[disabled] .btn {\n  pointer-events: none;\n  cursor: not-allowed;\n  filter: alpha(opacity=65);\n  -webkit-box-shadow: none;\n          box-shadow: none;\n  opacity: .65;\n}\n.btn-default {\n  color: #333;\n  background-color: #fff;\n  border-color: #ccc;\n}\n.btn-default:hover,\n.btn-default:focus,\n.btn-default:active,\n.btn-default.active,\n.open .dropdown-toggle.btn-default {\n  color: #333;\n  background-color: #ebebeb;\n  border-color: #adadad;\n}\n.btn-default:active,\n.btn-default.active,\n.open .dropdown-toggle.btn-default {\n  background-image: none;\n}\n.btn-default.disabled,\n.btn-default[disabled],\nfieldset[disabled] .btn-default,\n.btn-default.disabled:hover,\n.btn-default[disabled]:hover,\nfieldset[disabled] .btn-default:hover,\n.btn-default.disabled:focus,\n.btn-default[disabled]:focus,\nfieldset[disabled] .btn-default:focus,\n.btn-default.disabled:active,\n.btn-default[disabled]:active,\nfieldset[disabled] .btn-default:active,\n.btn-default.disabled.active,\n.btn-default[disabled].active,\nfieldset[disabled] .btn-default.active {\n  background-color: #fff;\n  border-color: #ccc;\n}\n.btn-default .badge {\n  color: #fff;\n  background-color: #333;\n}\n.btn-primary {\n  color: #fff;\n  background-color: #428bca;\n  border-color: #357ebd;\n}\n.btn-primary:hover,\n.btn-primary:focus,\n.btn-primary:active,\n.btn-primary.active,\n.open .dropdown-toggle.btn-primary {\n  color: #fff;\n  background-color: #3276b1;\n  border-color: #285e8e;\n}\n.btn-primary:active,\n.btn-primary.active,\n.open .dropdown-toggle.btn-primary {\n  background-image: none;\n}\n.btn-primary.disabled,\n.btn-primary[disabled],\nfieldset[disabled] .btn-primary,\n.btn-primary.disabled:hover,\n.btn-primary[disabled]:hover,\nfieldset[disabled] .btn-primary:hover,\n.btn-primary.disabled:focus,\n.btn-primary[disabled]:focus,\nfieldset[disabled] .btn-primary:focus,\n.btn-primary.disabled:active,\n.btn-primary[disabled]:active,\nfieldset[disabled] .btn-primary:active,\n.btn-primary.disabled.active,\n.btn-primary[disabled].active,\nfieldset[disabled] .btn-primary.active {\n  background-color: #428bca;\n  border-color: #357ebd;\n}\n.btn-primary .badge {\n  color: #428bca;\n  background-color: #fff;\n}\n.btn-success {\n  color: #fff;\n  background-color: #5cb85c;\n  border-color: #4cae4c;\n}\n.btn-success:hover,\n.btn-success:focus,\n.btn-success:active,\n.btn-success.active,\n.open .dropdown-toggle.btn-success {\n  color: #fff;\n  background-color: #47a447;\n  border-color: #398439;\n}\n.btn-success:active,\n.btn-success.active,\n.open .dropdown-toggle.btn-success {\n  background-image: none;\n}\n.btn-success.disabled,\n.btn-success[disabled],\nfieldset[disabled] .btn-success,\n.btn-success.disabled:hover,\n.btn-success[disabled]:hover,\nfieldset[disabled] .btn-success:hover,\n.btn-success.disabled:focus,\n.btn-success[disabled]:focus,\nfieldset[disabled] .btn-success:focus,\n.btn-success.disabled:active,\n.btn-success[disabled]:active,\nfieldset[disabled] .btn-success:active,\n.btn-success.disabled.active,\n.btn-success[disabled].active,\nfieldset[disabled] .btn-success.active {\n  background-color: #5cb85c;\n  border-color: #4cae4c;\n}\n.btn-success .badge {\n  color: #5cb85c;\n  background-color: #fff;\n}\n.btn-info {\n  color: #fff;\n  background-color: #5bc0de;\n  border-color: #46b8da;\n}\n.btn-info:hover,\n.btn-info:focus,\n.btn-info:active,\n.btn-info.active,\n.open .dropdown-toggle.btn-info {\n  color: #fff;\n  background-color: #39b3d7;\n  border-color: #269abc;\n}\n.btn-info:active,\n.btn-info.active,\n.open .dropdown-toggle.btn-info {\n  background-image: none;\n}\n.btn-info.disabled,\n.btn-info[disabled],\nfieldset[disabled] .btn-info,\n.btn-info.disabled:hover,\n.btn-info[disabled]:hover,\nfieldset[disabled] .btn-info:hover,\n.btn-info.disabled:focus,\n.btn-info[disabled]:focus,\nfieldset[disabled] .btn-info:focus,\n.btn-info.disabled:active,\n.btn-info[disabled]:active,\nfieldset[disabled] .btn-info:active,\n.btn-info.disabled.active,\n.btn-info[disabled].active,\nfieldset[disabled] .btn-info.active {\n  background-color: #5bc0de;\n  border-color: #46b8da;\n}\n.btn-info .badge {\n  color: #5bc0de;\n  background-color: #fff;\n}\n.btn-warning {\n  color: #fff;\n  background-color: #f0ad4e;\n  border-color: #eea236;\n}\n.btn-warning:hover,\n.btn-warning:focus,\n.btn-warning:active,\n.btn-warning.active,\n.open .dropdown-toggle.btn-warning {\n  color: #fff;\n  background-color: #ed9c28;\n  border-color: #d58512;\n}\n.btn-warning:active,\n.btn-warning.active,\n.open .dropdown-toggle.btn-warning {\n  background-image: none;\n}\n.btn-warning.disabled,\n.btn-warning[disabled],\nfieldset[disabled] .btn-warning,\n.btn-warning.disabled:hover,\n.btn-warning[disabled]:hover,\nfieldset[disabled] .btn-warning:hover,\n.btn-warning.disabled:focus,\n.btn-warning[disabled]:focus,\nfieldset[disabled] .btn-warning:focus,\n.btn-warning.disabled:active,\n.btn-warning[disabled]:active,\nfieldset[disabled] .btn-warning:active,\n.btn-warning.disabled.active,\n.btn-warning[disabled].active,\nfieldset[disabled] .btn-warning.active {\n  background-color: #f0ad4e;\n  border-color: #eea236;\n}\n.btn-warning .badge {\n  color: #f0ad4e;\n  background-color: #fff;\n}\n.btn-danger {\n  color: #fff;\n  background-color: #d9534f;\n  border-color: #d43f3a;\n}\n.btn-danger:hover,\n.btn-danger:focus,\n.btn-danger:active,\n.btn-danger.active,\n.open .dropdown-toggle.btn-danger {\n  color: #fff;\n  background-color: #d2322d;\n  border-color: #ac2925;\n}\n.btn-danger:active,\n.btn-danger.active,\n.open .dropdown-toggle.btn-danger {\n  background-image: none;\n}\n.btn-danger.disabled,\n.btn-danger[disabled],\nfieldset[disabled] .btn-danger,\n.btn-danger.disabled:hover,\n.btn-danger[disabled]:hover,\nfieldset[disabled] .btn-danger:hover,\n.btn-danger.disabled:focus,\n.btn-danger[disabled]:focus,\nfieldset[disabled] .btn-danger:focus,\n.btn-danger.disabled:active,\n.btn-danger[disabled]:active,\nfieldset[disabled] .btn-danger:active,\n.btn-danger.disabled.active,\n.btn-danger[disabled].active,\nfieldset[disabled] .btn-danger.active {\n  background-color: #d9534f;\n  border-color: #d43f3a;\n}\n.btn-danger .badge {\n  color: #d9534f;\n  background-color: #fff;\n}\n.btn-link {\n  font-weight: normal;\n  color: #428bca;\n  cursor: pointer;\n  border-radius: 0;\n}\n.btn-link,\n.btn-link:active,\n.btn-link[disabled],\nfieldset[disabled] .btn-link {\n  background-color: transparent;\n  -webkit-box-shadow: none;\n          box-shadow: none;\n}\n.btn-link,\n.btn-link:hover,\n.btn-link:focus,\n.btn-link:active {\n  border-color: transparent;\n}\n.btn-link:hover,\n.btn-link:focus {\n  color: #2a6496;\n  text-decoration: underline;\n  background-color: transparent;\n}\n.btn-link[disabled]:hover,\nfieldset[disabled] .btn-link:hover,\n.btn-link[disabled]:focus,\nfieldset[disabled] .btn-link:focus {\n  color: #999;\n  text-decoration: none;\n}\n.btn-lg,\n.btn-group-lg > .btn {\n  padding: 10px 16px;\n  font-size: 18px;\n  line-height: 1.33;\n  border-radius: 6px;\n}\n.btn-sm,\n.btn-group-sm > .btn {\n  padding: 5px 10px;\n  font-size: 12px;\n  line-height: 1.5;\n  border-radius: 3px;\n}\n.btn-xs,\n.btn-group-xs > .btn {\n  padding: 1px 5px;\n  font-size: 12px;\n  line-height: 1.5;\n  border-radius: 3px;\n}\n.btn-block {\n  display: block;\n  width: 100%;\n  padding-right: 0;\n  padding-left: 0;\n}\n.btn-block + .btn-block {\n  margin-top: 5px;\n}\ninput[type=\"submit\"].btn-block,\ninput[type=\"reset\"].btn-block,\ninput[type=\"button\"].btn-block {\n  width: 100%;\n}\n.fade {\n  opacity: 0;\n  -webkit-transition: opacity .15s linear;\n          transition: opacity .15s linear;\n}\n.fade.in {\n  opacity: 1;\n}\n.collapse {\n  display: none;\n}\n.collapse.in {\n  display: block;\n}\n.collapsing {\n  position: relative;\n  height: 0;\n  overflow: hidden;\n  -webkit-transition: height .35s ease;\n          transition: height .35s ease;\n}\n@font-face {\n  font-family: 'Glyphicons Halflings';\n\n  src: url('../fonts/glyphicons-halflings-regular.eot');\n  src: url('../fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), url('../fonts/glyphicons-halflings-regular.woff') format('woff'), url('../fonts/glyphicons-halflings-regular.ttf') format('truetype'), url('../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg');\n}\n.glyphicon {\n  position: relative;\n  top: 1px;\n  display: inline-block;\n  font-family: 'Glyphicons Halflings';\n  font-style: normal;\n  font-weight: normal;\n  line-height: 1;\n\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n.glyphicon-asterisk:before {\n  content: \"\\2a\";\n}\n.glyphicon-plus:before {\n  content: \"\\2b\";\n}\n.glyphicon-euro:before {\n  content: \"\\20ac\";\n}\n.glyphicon-minus:before {\n  content: \"\\2212\";\n}\n.glyphicon-cloud:before {\n  content: \"\\2601\";\n}\n.glyphicon-envelope:before {\n  content: \"\\2709\";\n}\n.glyphicon-pencil:before {\n  content: \"\\270f\";\n}\n.glyphicon-glass:before {\n  content: \"\\e001\";\n}\n.glyphicon-music:before {\n  content: \"\\e002\";\n}\n.glyphicon-search:before {\n  content: \"\\e003\";\n}\n.glyphicon-heart:before {\n  content: \"\\e005\";\n}\n.glyphicon-star:before {\n  content: \"\\e006\";\n}\n.glyphicon-star-empty:before {\n  content: \"\\e007\";\n}\n.glyphicon-user:before {\n  content: \"\\e008\";\n}\n.glyphicon-film:before {\n  content: \"\\e009\";\n}\n.glyphicon-th-large:before {\n  content: \"\\e010\";\n}\n.glyphicon-th:before {\n  content: \"\\e011\";\n}\n.glyphicon-th-list:before {\n  content: \"\\e012\";\n}\n.glyphicon-ok:before {\n  content: \"\\e013\";\n}\n.glyphicon-remove:before {\n  content: \"\\e014\";\n}\n.glyphicon-zoom-in:before {\n  content: \"\\e015\";\n}\n.glyphicon-zoom-out:before {\n  content: \"\\e016\";\n}\n.glyphicon-off:before {\n  content: \"\\e017\";\n}\n.glyphicon-signal:before {\n  content: \"\\e018\";\n}\n.glyphicon-cog:before {\n  content: \"\\e019\";\n}\n.glyphicon-trash:before {\n  content: \"\\e020\";\n}\n.glyphicon-home:before {\n  content: \"\\e021\";\n}\n.glyphicon-file:before {\n  content: \"\\e022\";\n}\n.glyphicon-time:before {\n  content: \"\\e023\";\n}\n.glyphicon-road:before {\n  content: \"\\e024\";\n}\n.glyphicon-download-alt:before {\n  content: \"\\e025\";\n}\n.glyphicon-download:before {\n  content: \"\\e026\";\n}\n.glyphicon-upload:before {\n  content: \"\\e027\";\n}\n.glyphicon-inbox:before {\n  content: \"\\e028\";\n}\n.glyphicon-play-circle:before {\n  content: \"\\e029\";\n}\n.glyphicon-repeat:before {\n  content: \"\\e030\";\n}\n.glyphicon-refresh:before {\n  content: \"\\e031\";\n}\n.glyphicon-list-alt:before {\n  content: \"\\e032\";\n}\n.glyphicon-lock:before {\n  content: \"\\e033\";\n}\n.glyphicon-flag:before {\n  content: \"\\e034\";\n}\n.glyphicon-headphones:before {\n  content: \"\\e035\";\n}\n.glyphicon-volume-off:before {\n  content: \"\\e036\";\n}\n.glyphicon-volume-down:before {\n  content: \"\\e037\";\n}\n.glyphicon-volume-up:before {\n  content: \"\\e038\";\n}\n.glyphicon-qrcode:before {\n  content: \"\\e039\";\n}\n.glyphicon-barcode:before {\n  content: \"\\e040\";\n}\n.glyphicon-tag:before {\n  content: \"\\e041\";\n}\n.glyphicon-tags:before {\n  content: \"\\e042\";\n}\n.glyphicon-book:before {\n  content: \"\\e043\";\n}\n.glyphicon-bookmark:before {\n  content: \"\\e044\";\n}\n.glyphicon-print:before {\n  content: \"\\e045\";\n}\n.glyphicon-camera:before {\n  content: \"\\e046\";\n}\n.glyphicon-font:before {\n  content: \"\\e047\";\n}\n.glyphicon-bold:before {\n  content: \"\\e048\";\n}\n.glyphicon-italic:before {\n  content: \"\\e049\";\n}\n.glyphicon-text-height:before {\n  content: \"\\e050\";\n}\n.glyphicon-text-width:before {\n  content: \"\\e051\";\n}\n.glyphicon-align-left:before {\n  content: \"\\e052\";\n}\n.glyphicon-align-center:before {\n  content: \"\\e053\";\n}\n.glyphicon-align-right:before {\n  content: \"\\e054\";\n}\n.glyphicon-align-justify:before {\n  content: \"\\e055\";\n}\n.glyphicon-list:before {\n  content: \"\\e056\";\n}\n.glyphicon-indent-left:before {\n  content: \"\\e057\";\n}\n.glyphicon-indent-right:before {\n  content: \"\\e058\";\n}\n.glyphicon-facetime-video:before {\n  content: \"\\e059\";\n}\n.glyphicon-picture:before {\n  content: \"\\e060\";\n}\n.glyphicon-map-marker:before {\n  content: \"\\e062\";\n}\n.glyphicon-adjust:before {\n  content: \"\\e063\";\n}\n.glyphicon-tint:before {\n  content: \"\\e064\";\n}\n.glyphicon-edit:before {\n  content: \"\\e065\";\n}\n.glyphicon-share:before {\n  content: \"\\e066\";\n}\n.glyphicon-check:before {\n  content: \"\\e067\";\n}\n.glyphicon-move:before {\n  content: \"\\e068\";\n}\n.glyphicon-step-backward:before {\n  content: \"\\e069\";\n}\n.glyphicon-fast-backward:before {\n  content: \"\\e070\";\n}\n.glyphicon-backward:before {\n  content: \"\\e071\";\n}\n.glyphicon-play:before {\n  content: \"\\e072\";\n}\n.glyphicon-pause:before {\n  content: \"\\e073\";\n}\n.glyphicon-stop:before {\n  content: \"\\e074\";\n}\n.glyphicon-forward:before {\n  content: \"\\e075\";\n}\n.glyphicon-fast-forward:before {\n  content: \"\\e076\";\n}\n.glyphicon-step-forward:before {\n  content: \"\\e077\";\n}\n.glyphicon-eject:before {\n  content: \"\\e078\";\n}\n.glyphicon-chevron-left:before {\n  content: \"\\e079\";\n}\n.glyphicon-chevron-right:before {\n  content: \"\\e080\";\n}\n.glyphicon-plus-sign:before {\n  content: \"\\e081\";\n}\n.glyphicon-minus-sign:before {\n  content: \"\\e082\";\n}\n.glyphicon-remove-sign:before {\n  content: \"\\e083\";\n}\n.glyphicon-ok-sign:before {\n  content: \"\\e084\";\n}\n.glyphicon-question-sign:before {\n  content: \"\\e085\";\n}\n.glyphicon-info-sign:before {\n  content: \"\\e086\";\n}\n.glyphicon-screenshot:before {\n  content: \"\\e087\";\n}\n.glyphicon-remove-circle:before {\n  content: \"\\e088\";\n}\n.glyphicon-ok-circle:before {\n  content: \"\\e089\";\n}\n.glyphicon-ban-circle:before {\n  content: \"\\e090\";\n}\n.glyphicon-arrow-left:before {\n  content: \"\\e091\";\n}\n.glyphicon-arrow-right:before {\n  content: \"\\e092\";\n}\n.glyphicon-arrow-up:before {\n  content: \"\\e093\";\n}\n.glyphicon-arrow-down:before {\n  content: \"\\e094\";\n}\n.glyphicon-share-alt:before {\n  content: \"\\e095\";\n}\n.glyphicon-resize-full:before {\n  content: \"\\e096\";\n}\n.glyphicon-resize-small:before {\n  content: \"\\e097\";\n}\n.glyphicon-exclamation-sign:before {\n  content: \"\\e101\";\n}\n.glyphicon-gift:before {\n  content: \"\\e102\";\n}\n.glyphicon-leaf:before {\n  content: \"\\e103\";\n}\n.glyphicon-fire:before {\n  content: \"\\e104\";\n}\n.glyphicon-eye-open:before {\n  content: \"\\e105\";\n}\n.glyphicon-eye-close:before {\n  content: \"\\e106\";\n}\n.glyphicon-warning-sign:before {\n  content: \"\\e107\";\n}\n.glyphicon-plane:before {\n  content: \"\\e108\";\n}\n.glyphicon-calendar:before {\n  content: \"\\e109\";\n}\n.glyphicon-random:before {\n  content: \"\\e110\";\n}\n.glyphicon-comment:before {\n  content: \"\\e111\";\n}\n.glyphicon-magnet:before {\n  content: \"\\e112\";\n}\n.glyphicon-chevron-up:before {\n  content: \"\\e113\";\n}\n.glyphicon-chevron-down:before {\n  content: \"\\e114\";\n}\n.glyphicon-retweet:before {\n  content: \"\\e115\";\n}\n.glyphicon-shopping-cart:before {\n  content: \"\\e116\";\n}\n.glyphicon-folder-close:before {\n  content: \"\\e117\";\n}\n.glyphicon-folder-open:before {\n  content: \"\\e118\";\n}\n.glyphicon-resize-vertical:before {\n  content: \"\\e119\";\n}\n.glyphicon-resize-horizontal:before {\n  content: \"\\e120\";\n}\n.glyphicon-hdd:before {\n  content: \"\\e121\";\n}\n.glyphicon-bullhorn:before {\n  content: \"\\e122\";\n}\n.glyphicon-bell:before {\n  content: \"\\e123\";\n}\n.glyphicon-certificate:before {\n  content: \"\\e124\";\n}\n.glyphicon-thumbs-up:before {\n  content: \"\\e125\";\n}\n.glyphicon-thumbs-down:before {\n  content: \"\\e126\";\n}\n.glyphicon-hand-right:before {\n  content: \"\\e127\";\n}\n.glyphicon-hand-left:before {\n  content: \"\\e128\";\n}\n.glyphicon-hand-up:before {\n  content: \"\\e129\";\n}\n.glyphicon-hand-down:before {\n  content: \"\\e130\";\n}\n.glyphicon-circle-arrow-right:before {\n  content: \"\\e131\";\n}\n.glyphicon-circle-arrow-left:before {\n  content: \"\\e132\";\n}\n.glyphicon-circle-arrow-up:before {\n  content: \"\\e133\";\n}\n.glyphicon-circle-arrow-down:before {\n  content: \"\\e134\";\n}\n.glyphicon-globe:before {\n  content: \"\\e135\";\n}\n.glyphicon-wrench:before {\n  content: \"\\e136\";\n}\n.glyphicon-tasks:before {\n  content: \"\\e137\";\n}\n.glyphicon-filter:before {\n  content: \"\\e138\";\n}\n.glyphicon-briefcase:before {\n  content: \"\\e139\";\n}\n.glyphicon-fullscreen:before {\n  content: \"\\e140\";\n}\n.glyphicon-dashboard:before {\n  content: \"\\e141\";\n}\n.glyphicon-paperclip:before {\n  content: \"\\e142\";\n}\n.glyphicon-heart-empty:before {\n  content: \"\\e143\";\n}\n.glyphicon-link:before {\n  content: \"\\e144\";\n}\n.glyphicon-phone:before {\n  content: \"\\e145\";\n}\n.glyphicon-pushpin:before {\n  content: \"\\e146\";\n}\n.glyphicon-usd:before {\n  content: \"\\e148\";\n}\n.glyphicon-gbp:before {\n  content: \"\\e149\";\n}\n.glyphicon-sort:before {\n  content: \"\\e150\";\n}\n.glyphicon-sort-by-alphabet:before {\n  content: \"\\e151\";\n}\n.glyphicon-sort-by-alphabet-alt:before {\n  content: \"\\e152\";\n}\n.glyphicon-sort-by-order:before {\n  content: \"\\e153\";\n}\n.glyphicon-sort-by-order-alt:before {\n  content: \"\\e154\";\n}\n.glyphicon-sort-by-attributes:before {\n  content: \"\\e155\";\n}\n.glyphicon-sort-by-attributes-alt:before {\n  content: \"\\e156\";\n}\n.glyphicon-unchecked:before {\n  content: \"\\e157\";\n}\n.glyphicon-expand:before {\n  content: \"\\e158\";\n}\n.glyphicon-collapse-down:before {\n  content: \"\\e159\";\n}\n.glyphicon-collapse-up:before {\n  content: \"\\e160\";\n}\n.glyphicon-log-in:before {\n  content: \"\\e161\";\n}\n.glyphicon-flash:before {\n  content: \"\\e162\";\n}\n.glyphicon-log-out:before {\n  content: \"\\e163\";\n}\n.glyphicon-new-window:before {\n  content: \"\\e164\";\n}\n.glyphicon-record:before {\n  content: \"\\e165\";\n}\n.glyphicon-save:before {\n  content: \"\\e166\";\n}\n.glyphicon-open:before {\n  content: \"\\e167\";\n}\n.glyphicon-saved:before {\n  content: \"\\e168\";\n}\n.glyphicon-import:before {\n  content: \"\\e169\";\n}\n.glyphicon-export:before {\n  content: \"\\e170\";\n}\n.glyphicon-send:before {\n  content: \"\\e171\";\n}\n.glyphicon-floppy-disk:before {\n  content: \"\\e172\";\n}\n.glyphicon-floppy-saved:before {\n  content: \"\\e173\";\n}\n.glyphicon-floppy-remove:before {\n  content: \"\\e174\";\n}\n.glyphicon-floppy-save:before {\n  content: \"\\e175\";\n}\n.glyphicon-floppy-open:before {\n  content: \"\\e176\";\n}\n.glyphicon-credit-card:before {\n  content: \"\\e177\";\n}\n.glyphicon-transfer:before {\n  content: \"\\e178\";\n}\n.glyphicon-cutlery:before {\n  content: \"\\e179\";\n}\n.glyphicon-header:before {\n  content: \"\\e180\";\n}\n.glyphicon-compressed:before {\n  content: \"\\e181\";\n}\n.glyphicon-earphone:before {\n  content: \"\\e182\";\n}\n.glyphicon-phone-alt:before {\n  content: \"\\e183\";\n}\n.glyphicon-tower:before {\n  content: \"\\e184\";\n}\n.glyphicon-stats:before {\n  content: \"\\e185\";\n}\n.glyphicon-sd-video:before {\n  content: \"\\e186\";\n}\n.glyphicon-hd-video:before {\n  content: \"\\e187\";\n}\n.glyphicon-subtitles:before {\n  content: \"\\e188\";\n}\n.glyphicon-sound-stereo:before {\n  content: \"\\e189\";\n}\n.glyphicon-sound-dolby:before {\n  content: \"\\e190\";\n}\n.glyphicon-sound-5-1:before {\n  content: \"\\e191\";\n}\n.glyphicon-sound-6-1:before {\n  content: \"\\e192\";\n}\n.glyphicon-sound-7-1:before {\n  content: \"\\e193\";\n}\n.glyphicon-copyright-mark:before {\n  content: \"\\e194\";\n}\n.glyphicon-registration-mark:before {\n  content: \"\\e195\";\n}\n.glyphicon-cloud-download:before {\n  content: \"\\e197\";\n}\n.glyphicon-cloud-upload:before {\n  content: \"\\e198\";\n}\n.glyphicon-tree-conifer:before {\n  content: \"\\e199\";\n}\n.glyphicon-tree-deciduous:before {\n  content: \"\\e200\";\n}\n.caret {\n  display: inline-block;\n  width: 0;\n  height: 0;\n  margin-left: 2px;\n  vertical-align: middle;\n  border-top: 4px solid;\n  border-right: 4px solid transparent;\n  border-left: 4px solid transparent;\n}\n.dropdown {\n  position: relative;\n}\n.dropdown-toggle:focus {\n  outline: 0;\n}\n.dropdown-menu {\n  position: absolute;\n  top: 100%;\n  left: 0;\n  z-index: 1000;\n  display: none;\n  float: left;\n  min-width: 160px;\n  padding: 5px 0;\n  margin: 2px 0 0;\n  font-size: 14px;\n  list-style: none;\n  background-color: #fff;\n  background-clip: padding-box;\n  border: 1px solid #ccc;\n  border: 1px solid rgba(0, 0, 0, .15);\n  border-radius: 4px;\n  -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, .175);\n          box-shadow: 0 6px 12px rgba(0, 0, 0, .175);\n}\n.dropdown-menu.pull-right {\n  right: 0;\n  left: auto;\n}\n.dropdown-menu .divider {\n  height: 1px;\n  margin: 9px 0;\n  overflow: hidden;\n  background-color: #e5e5e5;\n}\n.dropdown-menu > li > a {\n  display: block;\n  padding: 3px 20px;\n  clear: both;\n  font-weight: normal;\n  line-height: 1.42857143;\n  color: #333;\n  white-space: nowrap;\n}\n.dropdown-menu > li > a:hover,\n.dropdown-menu > li > a:focus {\n  color: #262626;\n  text-decoration: none;\n  background-color: #f5f5f5;\n}\n.dropdown-menu > .active > a,\n.dropdown-menu > .active > a:hover,\n.dropdown-menu > .active > a:focus {\n  color: #fff;\n  text-decoration: none;\n  background-color: #428bca;\n  outline: 0;\n}\n.dropdown-menu > .disabled > a,\n.dropdown-menu > .disabled > a:hover,\n.dropdown-menu > .disabled > a:focus {\n  color: #999;\n}\n.dropdown-menu > .disabled > a:hover,\n.dropdown-menu > .disabled > a:focus {\n  text-decoration: none;\n  cursor: not-allowed;\n  background-color: transparent;\n  background-image: none;\n  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n}\n.open > .dropdown-menu {\n  display: block;\n}\n.open > a {\n  outline: 0;\n}\n.dropdown-menu-right {\n  right: 0;\n  left: auto;\n}\n.dropdown-menu-left {\n  right: auto;\n  left: 0;\n}\n.dropdown-header {\n  display: block;\n  padding: 3px 20px;\n  font-size: 12px;\n  line-height: 1.42857143;\n  color: #999;\n}\n.dropdown-backdrop {\n  position: fixed;\n  top: 0;\n  right: 0;\n  bottom: 0;\n  left: 0;\n  z-index: 990;\n}\n.pull-right > .dropdown-menu {\n  right: 0;\n  left: auto;\n}\n.dropup .caret,\n.navbar-fixed-bottom .dropdown .caret {\n  content: \"\";\n  border-top: 0;\n  border-bottom: 4px solid;\n}\n.dropup .dropdown-menu,\n.navbar-fixed-bottom .dropdown .dropdown-menu {\n  top: auto;\n  bottom: 100%;\n  margin-bottom: 1px;\n}\n@media (min-width: 768px) {\n  .navbar-right .dropdown-menu {\n    right: 0;\n    left: auto;\n  }\n  .navbar-right .dropdown-menu-left {\n    right: auto;\n    left: 0;\n  }\n}\n.btn-group,\n.btn-group-vertical {\n  position: relative;\n  display: inline-block;\n  vertical-align: middle;\n}\n.btn-group > .btn,\n.btn-group-vertical > .btn {\n  position: relative;\n  float: left;\n}\n.btn-group > .btn:hover,\n.btn-group-vertical > .btn:hover,\n.btn-group > .btn:focus,\n.btn-group-vertical > .btn:focus,\n.btn-group > .btn:active,\n.btn-group-vertical > .btn:active,\n.btn-group > .btn.active,\n.btn-group-vertical > .btn.active {\n  z-index: 2;\n}\n.btn-group > .btn:focus,\n.btn-group-vertical > .btn:focus {\n  outline: none;\n}\n.btn-group .btn + .btn,\n.btn-group .btn + .btn-group,\n.btn-group .btn-group + .btn,\n.btn-group .btn-group + .btn-group {\n  margin-left: -1px;\n}\n.btn-toolbar {\n  margin-left: -5px;\n}\n.btn-toolbar .btn-group,\n.btn-toolbar .input-group {\n  float: left;\n}\n.btn-toolbar > .btn,\n.btn-toolbar > .btn-group,\n.btn-toolbar > .input-group {\n  margin-left: 5px;\n}\n.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) {\n  border-radius: 0;\n}\n.btn-group > .btn:first-child {\n  margin-left: 0;\n}\n.btn-group > .btn:first-child:not(:last-child):not(.dropdown-toggle) {\n  border-top-right-radius: 0;\n  border-bottom-right-radius: 0;\n}\n.btn-group > .btn:last-child:not(:first-child),\n.btn-group > .dropdown-toggle:not(:first-child) {\n  border-top-left-radius: 0;\n  border-bottom-left-radius: 0;\n}\n.btn-group > .btn-group {\n  float: left;\n}\n.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn {\n  border-radius: 0;\n}\n.btn-group > .btn-group:first-child > .btn:last-child,\n.btn-group > .btn-group:first-child > .dropdown-toggle {\n  border-top-right-radius: 0;\n  border-bottom-right-radius: 0;\n}\n.btn-group > .btn-group:last-child > .btn:first-child {\n  border-top-left-radius: 0;\n  border-bottom-left-radius: 0;\n}\n.btn-group .dropdown-toggle:active,\n.btn-group.open .dropdown-toggle {\n  outline: 0;\n}\n.btn-group > .btn + .dropdown-toggle {\n  padding-right: 8px;\n  padding-left: 8px;\n}\n.btn-group > .btn-lg + .dropdown-toggle {\n  padding-right: 12px;\n  padding-left: 12px;\n}\n.btn-group.open .dropdown-toggle {\n  -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);\n          box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);\n}\n.btn-group.open .dropdown-toggle.btn-link {\n  -webkit-box-shadow: none;\n          box-shadow: none;\n}\n.btn .caret {\n  margin-left: 0;\n}\n.btn-lg .caret {\n  border-width: 5px 5px 0;\n  border-bottom-width: 0;\n}\n.dropup .btn-lg .caret {\n  border-width: 0 5px 5px;\n}\n.btn-group-vertical > .btn,\n.btn-group-vertical > .btn-group,\n.btn-group-vertical > .btn-group > .btn {\n  display: block;\n  float: none;\n  width: 100%;\n  max-width: 100%;\n}\n.btn-group-vertical > .btn-group > .btn {\n  float: none;\n}\n.btn-group-vertical > .btn + .btn,\n.btn-group-vertical > .btn + .btn-group,\n.btn-group-vertical > .btn-group + .btn,\n.btn-group-vertical > .btn-group + .btn-group {\n  margin-top: -1px;\n  margin-left: 0;\n}\n.btn-group-vertical > .btn:not(:first-child):not(:last-child) {\n  border-radius: 0;\n}\n.btn-group-vertical > .btn:first-child:not(:last-child) {\n  border-top-right-radius: 4px;\n  border-bottom-right-radius: 0;\n  border-bottom-left-radius: 0;\n}\n.btn-group-vertical > .btn:last-child:not(:first-child) {\n  border-top-left-radius: 0;\n  border-top-right-radius: 0;\n  border-bottom-left-radius: 4px;\n}\n.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn {\n  border-radius: 0;\n}\n.btn-group-vertical > .btn-group:first-child:not(:last-child) > .btn:last-child,\n.btn-group-vertical > .btn-group:first-child:not(:last-child) > .dropdown-toggle {\n  border-bottom-right-radius: 0;\n  border-bottom-left-radius: 0;\n}\n.btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child {\n  border-top-left-radius: 0;\n  border-top-right-radius: 0;\n}\n.btn-group-justified {\n  display: table;\n  width: 100%;\n  table-layout: fixed;\n  border-collapse: separate;\n}\n.btn-group-justified > .btn,\n.btn-group-justified > .btn-group {\n  display: table-cell;\n  float: none;\n  width: 1%;\n}\n.btn-group-justified > .btn-group .btn {\n  width: 100%;\n}\n[data-toggle=\"buttons\"] > .btn > input[type=\"radio\"],\n[data-toggle=\"buttons\"] > .btn > input[type=\"checkbox\"] {\n  display: none;\n}\n.input-group {\n  position: relative;\n  display: table;\n  border-collapse: separate;\n}\n.input-group[class*=\"col-\"] {\n  float: none;\n  padding-right: 0;\n  padding-left: 0;\n}\n.input-group .form-control {\n  position: relative;\n  z-index: 2;\n  float: left;\n  width: 100%;\n  margin-bottom: 0;\n}\n.input-group-lg > .form-control,\n.input-group-lg > .input-group-addon,\n.input-group-lg > .input-group-btn > .btn {\n  height: 46px;\n  padding: 10px 16px;\n  font-size: 18px;\n  line-height: 1.33;\n  border-radius: 6px;\n}\nselect.input-group-lg > .form-control,\nselect.input-group-lg > .input-group-addon,\nselect.input-group-lg > .input-group-btn > .btn {\n  height: 46px;\n  line-height: 46px;\n}\ntextarea.input-group-lg > .form-control,\ntextarea.input-group-lg > .input-group-addon,\ntextarea.input-group-lg > .input-group-btn > .btn,\nselect[multiple].input-group-lg > .form-control,\nselect[multiple].input-group-lg > .input-group-addon,\nselect[multiple].input-group-lg > .input-group-btn > .btn {\n  height: auto;\n}\n.input-group-sm > .form-control,\n.input-group-sm > .input-group-addon,\n.input-group-sm > .input-group-btn > .btn {\n  height: 30px;\n  padding: 5px 10px;\n  font-size: 12px;\n  line-height: 1.5;\n  border-radius: 3px;\n}\nselect.input-group-sm > .form-control,\nselect.input-group-sm > .input-group-addon,\nselect.input-group-sm > .input-group-btn > .btn {\n  height: 30px;\n  line-height: 30px;\n}\ntextarea.input-group-sm > .form-control,\ntextarea.input-group-sm > .input-group-addon,\ntextarea.input-group-sm > .input-group-btn > .btn,\nselect[multiple].input-group-sm > .form-control,\nselect[multiple].input-group-sm > .input-group-addon,\nselect[multiple].input-group-sm > .input-group-btn > .btn {\n  height: auto;\n}\n.input-group-addon,\n.input-group-btn,\n.input-group .form-control {\n  display: table-cell;\n}\n.input-group-addon:not(:first-child):not(:last-child),\n.input-group-btn:not(:first-child):not(:last-child),\n.input-group .form-control:not(:first-child):not(:last-child) {\n  border-radius: 0;\n}\n.input-group-addon,\n.input-group-btn {\n  width: 1%;\n  white-space: nowrap;\n  vertical-align: middle;\n}\n.input-group-addon {\n  padding: 6px 12px;\n  font-size: 14px;\n  font-weight: normal;\n  line-height: 1;\n  color: #555;\n  text-align: center;\n  background-color: #eee;\n  border: 1px solid #ccc;\n  border-radius: 4px;\n}\n.input-group-addon.input-sm {\n  padding: 5px 10px;\n  font-size: 12px;\n  border-radius: 3px;\n}\n.input-group-addon.input-lg {\n  padding: 10px 16px;\n  font-size: 18px;\n  border-radius: 6px;\n}\n.input-group-addon input[type=\"radio\"],\n.input-group-addon input[type=\"checkbox\"] {\n  margin-top: 0;\n}\n.input-group .form-control:first-child,\n.input-group-addon:first-child,\n.input-group-btn:first-child > .btn,\n.input-group-btn:first-child > .btn-group > .btn,\n.input-group-btn:first-child > .dropdown-toggle,\n.input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle),\n.input-group-btn:last-child > .btn-group:not(:last-child) > .btn {\n  border-top-right-radius: 0;\n  border-bottom-right-radius: 0;\n}\n.input-group-addon:first-child {\n  border-right: 0;\n}\n.input-group .form-control:last-child,\n.input-group-addon:last-child,\n.input-group-btn:last-child > .btn,\n.input-group-btn:last-child > .btn-group > .btn,\n.input-group-btn:last-child > .dropdown-toggle,\n.input-group-btn:first-child > .btn:not(:first-child),\n.input-group-btn:first-child > .btn-group:not(:first-child) > .btn {\n  border-top-left-radius: 0;\n  border-bottom-left-radius: 0;\n}\n.input-group-addon:last-child {\n  border-left: 0;\n}\n.input-group-btn {\n  position: relative;\n  font-size: 0;\n  white-space: nowrap;\n}\n.input-group-btn > .btn {\n  position: relative;\n}\n.input-group-btn > .btn + .btn {\n  margin-left: -1px;\n}\n.input-group-btn > .btn:hover,\n.input-group-btn > .btn:focus,\n.input-group-btn > .btn:active {\n  z-index: 2;\n}\n.input-group-btn:first-child > .btn,\n.input-group-btn:first-child > .btn-group {\n  margin-right: -1px;\n}\n.input-group-btn:last-child > .btn,\n.input-group-btn:last-child > .btn-group {\n  margin-left: -1px;\n}\n.nav {\n  padding-left: 0;\n  margin-bottom: 0;\n  list-style: none;\n}\n.nav > li {\n  position: relative;\n  display: block;\n}\n.nav > li > a {\n  position: relative;\n  display: block;\n  padding: 10px 15px;\n}\n.nav > li > a:hover,\n.nav > li > a:focus {\n  text-decoration: none;\n  background-color: #eee;\n}\n.nav > li.disabled > a {\n  color: #999;\n}\n.nav > li.disabled > a:hover,\n.nav > li.disabled > a:focus {\n  color: #999;\n  text-decoration: none;\n  cursor: not-allowed;\n  background-color: transparent;\n}\n.nav .open > a,\n.nav .open > a:hover,\n.nav .open > a:focus {\n  background-color: #eee;\n  border-color: #428bca;\n}\n.nav .nav-divider {\n  height: 1px;\n  margin: 9px 0;\n  overflow: hidden;\n  background-color: #e5e5e5;\n}\n.nav > li > a > img {\n  max-width: none;\n}\n.nav-tabs {\n  border-bottom: 1px solid #ddd;\n}\n.nav-tabs > li {\n  float: left;\n  margin-bottom: -1px;\n}\n.nav-tabs > li > a {\n  margin-right: 2px;\n  line-height: 1.42857143;\n  border: 1px solid transparent;\n  border-radius: 4px 4px 0 0;\n}\n.nav-tabs > li > a:hover {\n  border-color: #eee #eee #ddd;\n}\n.nav-tabs > li.active > a,\n.nav-tabs > li.active > a:hover,\n.nav-tabs > li.active > a:focus {\n  color: #555;\n  cursor: default;\n  background-color: #fff;\n  border: 1px solid #ddd;\n  border-bottom-color: transparent;\n}\n.nav-tabs.nav-justified {\n  width: 100%;\n  border-bottom: 0;\n}\n.nav-tabs.nav-justified > li {\n  float: none;\n}\n.nav-tabs.nav-justified > li > a {\n  margin-bottom: 5px;\n  text-align: center;\n}\n.nav-tabs.nav-justified > .dropdown .dropdown-menu {\n  top: auto;\n  left: auto;\n}\n@media (min-width: 768px) {\n  .nav-tabs.nav-justified > li {\n    display: table-cell;\n    width: 1%;\n  }\n  .nav-tabs.nav-justified > li > a {\n    margin-bottom: 0;\n  }\n}\n.nav-tabs.nav-justified > li > a {\n  margin-right: 0;\n  border-radius: 4px;\n}\n.nav-tabs.nav-justified > .active > a,\n.nav-tabs.nav-justified > .active > a:hover,\n.nav-tabs.nav-justified > .active > a:focus {\n  border: 1px solid #ddd;\n}\n@media (min-width: 768px) {\n  .nav-tabs.nav-justified > li > a {\n    border-bottom: 1px solid #ddd;\n    border-radius: 4px 4px 0 0;\n  }\n  .nav-tabs.nav-justified > .active > a,\n  .nav-tabs.nav-justified > .active > a:hover,\n  .nav-tabs.nav-justified > .active > a:focus {\n    border-bottom-color: #fff;\n  }\n}\n.nav-pills > li {\n  float: left;\n}\n.nav-pills > li > a {\n  border-radius: 4px;\n}\n.nav-pills > li + li {\n  margin-left: 2px;\n}\n.nav-pills > li.active > a,\n.nav-pills > li.active > a:hover,\n.nav-pills > li.active > a:focus {\n  color: #fff;\n  background-color: #428bca;\n}\n.nav-stacked > li {\n  float: none;\n}\n.nav-stacked > li + li {\n  margin-top: 2px;\n  margin-left: 0;\n}\n.nav-justified {\n  width: 100%;\n}\n.nav-justified > li {\n  float: none;\n}\n.nav-justified > li > a {\n  margin-bottom: 5px;\n  text-align: center;\n}\n.nav-justified > .dropdown .dropdown-menu {\n  top: auto;\n  left: auto;\n}\n@media (min-width: 768px) {\n  .nav-justified > li {\n    display: table-cell;\n    width: 1%;\n  }\n  .nav-justified > li > a {\n    margin-bottom: 0;\n  }\n}\n.nav-tabs-justified {\n  border-bottom: 0;\n}\n.nav-tabs-justified > li > a {\n  margin-right: 0;\n  border-radius: 4px;\n}\n.nav-tabs-justified > .active > a,\n.nav-tabs-justified > .active > a:hover,\n.nav-tabs-justified > .active > a:focus {\n  border: 1px solid #ddd;\n}\n@media (min-width: 768px) {\n  .nav-tabs-justified > li > a {\n    border-bottom: 1px solid #ddd;\n    border-radius: 4px 4px 0 0;\n  }\n  .nav-tabs-justified > .active > a,\n  .nav-tabs-justified > .active > a:hover,\n  .nav-tabs-justified > .active > a:focus {\n    border-bottom-color: #fff;\n  }\n}\n.tab-content > .tab-pane {\n  display: none;\n}\n.tab-content > .active {\n  display: block;\n}\n.nav-tabs .dropdown-menu {\n  margin-top: -1px;\n  border-top-left-radius: 0;\n  border-top-right-radius: 0;\n}\n.navbar {\n  position: relative;\n  min-height: 50px;\n  margin-bottom: 20px;\n  border: 1px solid transparent;\n}\n@media (min-width: 768px) {\n  .navbar {\n    border-radius: 4px;\n  }\n}\n@media (min-width: 768px) {\n  .navbar-header {\n    float: left;\n  }\n}\n.navbar-collapse {\n  max-height: 340px;\n  padding-right: 15px;\n  padding-left: 15px;\n  overflow-x: visible;\n  -webkit-overflow-scrolling: touch;\n  border-top: 1px solid transparent;\n  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1);\n}\n.navbar-collapse.in {\n  overflow-y: auto;\n}\n@media (min-width: 768px) {\n  .navbar-collapse {\n    width: auto;\n    border-top: 0;\n    box-shadow: none;\n  }\n  .navbar-collapse.collapse {\n    display: block !important;\n    height: auto !important;\n    padding-bottom: 0;\n    overflow: visible !important;\n  }\n  .navbar-collapse.in {\n    overflow-y: visible;\n  }\n  .navbar-fixed-top .navbar-collapse,\n  .navbar-static-top .navbar-collapse,\n  .navbar-fixed-bottom .navbar-collapse {\n    padding-right: 0;\n    padding-left: 0;\n  }\n}\n.container > .navbar-header,\n.container-fluid > .navbar-header,\n.container > .navbar-collapse,\n.container-fluid > .navbar-collapse {\n  margin-right: -15px;\n  margin-left: -15px;\n}\n@media (min-width: 768px) {\n  .container > .navbar-header,\n  .container-fluid > .navbar-header,\n  .container > .navbar-collapse,\n  .container-fluid > .navbar-collapse {\n    margin-right: 0;\n    margin-left: 0;\n  }\n}\n.navbar-static-top {\n  z-index: 1000;\n  border-width: 0 0 1px;\n}\n@media (min-width: 768px) {\n  .navbar-static-top {\n    border-radius: 0;\n  }\n}\n.navbar-fixed-top,\n.navbar-fixed-bottom {\n  position: fixed;\n  right: 0;\n  left: 0;\n  z-index: 1030;\n}\n@media (min-width: 768px) {\n  .navbar-fixed-top,\n  .navbar-fixed-bottom {\n    border-radius: 0;\n  }\n}\n.navbar-fixed-top {\n  top: 0;\n  border-width: 0 0 1px;\n}\n.navbar-fixed-bottom {\n  bottom: 0;\n  margin-bottom: 0;\n  border-width: 1px 0 0;\n}\n.navbar-brand {\n  float: left;\n  height: 50px;\n  padding: 15px 15px;\n  font-size: 18px;\n  line-height: 20px;\n}\n.navbar-brand:hover,\n.navbar-brand:focus {\n  text-decoration: none;\n}\n@media (min-width: 768px) {\n  .navbar > .container .navbar-brand,\n  .navbar > .container-fluid .navbar-brand {\n    margin-left: -15px;\n  }\n}\n.navbar-toggle {\n  position: relative;\n  float: right;\n  padding: 9px 10px;\n  margin-top: 8px;\n  margin-right: 15px;\n  margin-bottom: 8px;\n  background-color: transparent;\n  background-image: none;\n  border: 1px solid transparent;\n  border-radius: 4px;\n}\n.navbar-toggle:focus {\n  outline: none;\n}\n.navbar-toggle .icon-bar {\n  display: block;\n  width: 22px;\n  height: 2px;\n  border-radius: 1px;\n}\n.navbar-toggle .icon-bar + .icon-bar {\n  margin-top: 4px;\n}\n@media (min-width: 768px) {\n  .navbar-toggle {\n    display: none;\n  }\n}\n.navbar-nav {\n  margin: 7.5px -15px;\n}\n.navbar-nav > li > a {\n  padding-top: 10px;\n  padding-bottom: 10px;\n  line-height: 20px;\n}\n@media (max-width: 767px) {\n  .navbar-nav .open .dropdown-menu {\n    position: static;\n    float: none;\n    width: auto;\n    margin-top: 0;\n    background-color: transparent;\n    border: 0;\n    box-shadow: none;\n  }\n  .navbar-nav .open .dropdown-menu > li > a,\n  .navbar-nav .open .dropdown-menu .dropdown-header {\n    padding: 5px 15px 5px 25px;\n  }\n  .navbar-nav .open .dropdown-menu > li > a {\n    line-height: 20px;\n  }\n  .navbar-nav .open .dropdown-menu > li > a:hover,\n  .navbar-nav .open .dropdown-menu > li > a:focus {\n    background-image: none;\n  }\n}\n@media (min-width: 768px) {\n  .navbar-nav {\n    float: left;\n    margin: 0;\n  }\n  .navbar-nav > li {\n    float: left;\n  }\n  .navbar-nav > li > a {\n    padding-top: 15px;\n    padding-bottom: 15px;\n  }\n  .navbar-nav.navbar-right:last-child {\n    margin-right: -15px;\n  }\n}\n@media (min-width: 768px) {\n  .navbar-left {\n    float: left !important;\n  }\n  .navbar-right {\n    float: right !important;\n  }\n}\n.navbar-form {\n  padding: 10px 15px;\n  margin-top: 8px;\n  margin-right: -15px;\n  margin-bottom: 8px;\n  margin-left: -15px;\n  border-top: 1px solid transparent;\n  border-bottom: 1px solid transparent;\n  -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1), 0 1px 0 rgba(255, 255, 255, .1);\n          box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1), 0 1px 0 rgba(255, 255, 255, .1);\n}\n@media (min-width: 768px) {\n  .navbar-form .form-group {\n    display: inline-block;\n    margin-bottom: 0;\n    vertical-align: middle;\n  }\n  .navbar-form .form-control {\n    display: inline-block;\n    width: auto;\n    vertical-align: middle;\n  }\n  .navbar-form .input-group > .form-control {\n    width: 100%;\n  }\n  .navbar-form .control-label {\n    margin-bottom: 0;\n    vertical-align: middle;\n  }\n  .navbar-form .radio,\n  .navbar-form .checkbox {\n    display: inline-block;\n    padding-left: 0;\n    margin-top: 0;\n    margin-bottom: 0;\n    vertical-align: middle;\n  }\n  .navbar-form .radio input[type=\"radio\"],\n  .navbar-form .checkbox input[type=\"checkbox\"] {\n    float: none;\n    margin-left: 0;\n  }\n  .navbar-form .has-feedback .form-control-feedback {\n    top: 0;\n  }\n}\n@media (max-width: 767px) {\n  .navbar-form .form-group {\n    margin-bottom: 5px;\n  }\n}\n@media (min-width: 768px) {\n  .navbar-form {\n    width: auto;\n    padding-top: 0;\n    padding-bottom: 0;\n    margin-right: 0;\n    margin-left: 0;\n    border: 0;\n    -webkit-box-shadow: none;\n            box-shadow: none;\n  }\n  .navbar-form.navbar-right:last-child {\n    margin-right: -15px;\n  }\n}\n.navbar-nav > li > .dropdown-menu {\n  margin-top: 0;\n  border-top-left-radius: 0;\n  border-top-right-radius: 0;\n}\n.navbar-fixed-bottom .navbar-nav > li > .dropdown-menu {\n  border-bottom-right-radius: 0;\n  border-bottom-left-radius: 0;\n}\n.navbar-btn {\n  margin-top: 8px;\n  margin-bottom: 8px;\n}\n.navbar-btn.btn-sm {\n  margin-top: 10px;\n  margin-bottom: 10px;\n}\n.navbar-btn.btn-xs {\n  margin-top: 14px;\n  margin-bottom: 14px;\n}\n.navbar-text {\n  margin-top: 15px;\n  margin-bottom: 15px;\n}\n@media (min-width: 768px) {\n  .navbar-text {\n    float: left;\n    margin-right: 15px;\n    margin-left: 15px;\n  }\n  .navbar-text.navbar-right:last-child {\n    margin-right: 0;\n  }\n}\n.navbar-default {\n  background-color: #f8f8f8;\n  border-color: #e7e7e7;\n}\n.navbar-default .navbar-brand {\n  color: #777;\n}\n.navbar-default .navbar-brand:hover,\n.navbar-default .navbar-brand:focus {\n  color: #5e5e5e;\n  background-color: transparent;\n}\n.navbar-default .navbar-text {\n  color: #777;\n}\n.navbar-default .navbar-nav > li > a {\n  color: #777;\n}\n.navbar-default .navbar-nav > li > a:hover,\n.navbar-default .navbar-nav > li > a:focus {\n  color: #333;\n  background-color: transparent;\n}\n.navbar-default .navbar-nav > .active > a,\n.navbar-default .navbar-nav > .active > a:hover,\n.navbar-default .navbar-nav > .active > a:focus {\n  color: #555;\n  background-color: #e7e7e7;\n}\n.navbar-default .navbar-nav > .disabled > a,\n.navbar-default .navbar-nav > .disabled > a:hover,\n.navbar-default .navbar-nav > .disabled > a:focus {\n  color: #ccc;\n  background-color: transparent;\n}\n.navbar-default .navbar-toggle {\n  border-color: #ddd;\n}\n.navbar-default .navbar-toggle:hover,\n.navbar-default .navbar-toggle:focus {\n  background-color: #ddd;\n}\n.navbar-default .navbar-toggle .icon-bar {\n  background-color: #888;\n}\n.navbar-default .navbar-collapse,\n.navbar-default .navbar-form {\n  border-color: #e7e7e7;\n}\n.navbar-default .navbar-nav > .open > a,\n.navbar-default .navbar-nav > .open > a:hover,\n.navbar-default .navbar-nav > .open > a:focus {\n  color: #555;\n  background-color: #e7e7e7;\n}\n@media (max-width: 767px) {\n  .navbar-default .navbar-nav .open .dropdown-menu > li > a {\n    color: #777;\n  }\n  .navbar-default .navbar-nav .open .dropdown-menu > li > a:hover,\n  .navbar-default .navbar-nav .open .dropdown-menu > li > a:focus {\n    color: #333;\n    background-color: transparent;\n  }\n  .navbar-default .navbar-nav .open .dropdown-menu > .active > a,\n  .navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover,\n  .navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus {\n    color: #555;\n    background-color: #e7e7e7;\n  }\n  .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a,\n  .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:hover,\n  .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:focus {\n    color: #ccc;\n    background-color: transparent;\n  }\n}\n.navbar-default .navbar-link {\n  color: #777;\n}\n.navbar-default .navbar-link:hover {\n  color: #333;\n}\n.navbar-inverse {\n  background-color: #222;\n  border-color: #080808;\n}\n.navbar-inverse .navbar-brand {\n  color: #999;\n}\n.navbar-inverse .navbar-brand:hover,\n.navbar-inverse .navbar-brand:focus {\n  color: #fff;\n  background-color: transparent;\n}\n.navbar-inverse .navbar-text {\n  color: #999;\n}\n.navbar-inverse .navbar-nav > li > a {\n  color: #999;\n}\n.navbar-inverse .navbar-nav > li > a:hover,\n.navbar-inverse .navbar-nav > li > a:focus {\n  color: #fff;\n  background-color: transparent;\n}\n.navbar-inverse .navbar-nav > .active > a,\n.navbar-inverse .navbar-nav > .active > a:hover,\n.navbar-inverse .navbar-nav > .active > a:focus {\n  color: #fff;\n  background-color: #080808;\n}\n.navbar-inverse .navbar-nav > .disabled > a,\n.navbar-inverse .navbar-nav > .disabled > a:hover,\n.navbar-inverse .navbar-nav > .disabled > a:focus {\n  color: #444;\n  background-color: transparent;\n}\n.navbar-inverse .navbar-toggle {\n  border-color: #333;\n}\n.navbar-inverse .navbar-toggle:hover,\n.navbar-inverse .navbar-toggle:focus {\n  background-color: #333;\n}\n.navbar-inverse .navbar-toggle .icon-bar {\n  background-color: #fff;\n}\n.navbar-inverse .navbar-collapse,\n.navbar-inverse .navbar-form {\n  border-color: #101010;\n}\n.navbar-inverse .navbar-nav > .open > a,\n.navbar-inverse .navbar-nav > .open > a:hover,\n.navbar-inverse .navbar-nav > .open > a:focus {\n  color: #fff;\n  background-color: #080808;\n}\n@media (max-width: 767px) {\n  .navbar-inverse .navbar-nav .open .dropdown-menu > .dropdown-header {\n    border-color: #080808;\n  }\n  .navbar-inverse .navbar-nav .open .dropdown-menu .divider {\n    background-color: #080808;\n  }\n  .navbar-inverse .navbar-nav .open .dropdown-menu > li > a {\n    color: #999;\n  }\n  .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:hover,\n  .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:focus {\n    color: #fff;\n    background-color: transparent;\n  }\n  .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a,\n  .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:hover,\n  .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:focus {\n    color: #fff;\n    background-color: #080808;\n  }\n  .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a,\n  .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:hover,\n  .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:focus {\n    color: #444;\n    background-color: transparent;\n  }\n}\n.navbar-inverse .navbar-link {\n  color: #999;\n}\n.navbar-inverse .navbar-link:hover {\n  color: #fff;\n}\n.breadcrumb {\n  padding: 8px 15px;\n  margin-bottom: 20px;\n  list-style: none;\n  background-color: #f5f5f5;\n  border-radius: 4px;\n}\n.breadcrumb > li {\n  display: inline-block;\n}\n.breadcrumb > li + li:before {\n  padding: 0 5px;\n  color: #ccc;\n  content: \"/\\00a0\";\n}\n.breadcrumb > .active {\n  color: #999;\n}\n.pagination {\n  display: inline-block;\n  padding-left: 0;\n  margin: 20px 0;\n  border-radius: 4px;\n}\n.pagination > li {\n  display: inline;\n}\n.pagination > li > a,\n.pagination > li > span {\n  position: relative;\n  float: left;\n  padding: 6px 12px;\n  margin-left: -1px;\n  line-height: 1.42857143;\n  color: #428bca;\n  text-decoration: none;\n  background-color: #fff;\n  border: 1px solid #ddd;\n}\n.pagination > li:first-child > a,\n.pagination > li:first-child > span {\n  margin-left: 0;\n  border-top-left-radius: 4px;\n  border-bottom-left-radius: 4px;\n}\n.pagination > li:last-child > a,\n.pagination > li:last-child > span {\n  border-top-right-radius: 4px;\n  border-bottom-right-radius: 4px;\n}\n.pagination > li > a:hover,\n.pagination > li > span:hover,\n.pagination > li > a:focus,\n.pagination > li > span:focus {\n  color: #2a6496;\n  background-color: #eee;\n  border-color: #ddd;\n}\n.pagination > .active > a,\n.pagination > .active > span,\n.pagination > .active > a:hover,\n.pagination > .active > span:hover,\n.pagination > .active > a:focus,\n.pagination > .active > span:focus {\n  z-index: 2;\n  color: #fff;\n  cursor: default;\n  background-color: #428bca;\n  border-color: #428bca;\n}\n.pagination > .disabled > span,\n.pagination > .disabled > span:hover,\n.pagination > .disabled > span:focus,\n.pagination > .disabled > a,\n.pagination > .disabled > a:hover,\n.pagination > .disabled > a:focus {\n  color: #999;\n  cursor: not-allowed;\n  background-color: #fff;\n  border-color: #ddd;\n}\n.pagination-lg > li > a,\n.pagination-lg > li > span {\n  padding: 10px 16px;\n  font-size: 18px;\n}\n.pagination-lg > li:first-child > a,\n.pagination-lg > li:first-child > span {\n  border-top-left-radius: 6px;\n  border-bottom-left-radius: 6px;\n}\n.pagination-lg > li:last-child > a,\n.pagination-lg > li:last-child > span {\n  border-top-right-radius: 6px;\n  border-bottom-right-radius: 6px;\n}\n.pagination-sm > li > a,\n.pagination-sm > li > span {\n  padding: 5px 10px;\n  font-size: 12px;\n}\n.pagination-sm > li:first-child > a,\n.pagination-sm > li:first-child > span {\n  border-top-left-radius: 3px;\n  border-bottom-left-radius: 3px;\n}\n.pagination-sm > li:last-child > a,\n.pagination-sm > li:last-child > span {\n  border-top-right-radius: 3px;\n  border-bottom-right-radius: 3px;\n}\n.pager {\n  padding-left: 0;\n  margin: 20px 0;\n  text-align: center;\n  list-style: none;\n}\n.pager li {\n  display: inline;\n}\n.pager li > a,\n.pager li > span {\n  display: inline-block;\n  padding: 5px 14px;\n  background-color: #fff;\n  border: 1px solid #ddd;\n  border-radius: 15px;\n}\n.pager li > a:hover,\n.pager li > a:focus {\n  text-decoration: none;\n  background-color: #eee;\n}\n.pager .next > a,\n.pager .next > span {\n  float: right;\n}\n.pager .previous > a,\n.pager .previous > span {\n  float: left;\n}\n.pager .disabled > a,\n.pager .disabled > a:hover,\n.pager .disabled > a:focus,\n.pager .disabled > span {\n  color: #999;\n  cursor: not-allowed;\n  background-color: #fff;\n}\n.label {\n  display: inline;\n  padding: .2em .6em .3em;\n  font-size: 75%;\n  font-weight: bold;\n  line-height: 1;\n  color: #fff;\n  text-align: center;\n  white-space: nowrap;\n  vertical-align: baseline;\n  border-radius: .25em;\n}\n.label[href]:hover,\n.label[href]:focus {\n  color: #fff;\n  text-decoration: none;\n  cursor: pointer;\n}\n.label:empty {\n  display: none;\n}\n.btn .label {\n  position: relative;\n  top: -1px;\n}\n.label-default {\n  background-color: #999;\n}\n.label-default[href]:hover,\n.label-default[href]:focus {\n  background-color: #808080;\n}\n.label-primary {\n  background-color: #428bca;\n}\n.label-primary[href]:hover,\n.label-primary[href]:focus {\n  background-color: #3071a9;\n}\n.label-success {\n  background-color: #5cb85c;\n}\n.label-success[href]:hover,\n.label-success[href]:focus {\n  background-color: #449d44;\n}\n.label-info {\n  background-color: #5bc0de;\n}\n.label-info[href]:hover,\n.label-info[href]:focus {\n  background-color: #31b0d5;\n}\n.label-warning {\n  background-color: #f0ad4e;\n}\n.label-warning[href]:hover,\n.label-warning[href]:focus {\n  background-color: #ec971f;\n}\n.label-danger {\n  background-color: #d9534f;\n}\n.label-danger[href]:hover,\n.label-danger[href]:focus {\n  background-color: #c9302c;\n}\n.badge {\n  display: inline-block;\n  min-width: 10px;\n  padding: 3px 7px;\n  font-size: 12px;\n  font-weight: bold;\n  line-height: 1;\n  color: #fff;\n  text-align: center;\n  white-space: nowrap;\n  vertical-align: baseline;\n  background-color: #999;\n  border-radius: 10px;\n}\n.badge:empty {\n  display: none;\n}\n.btn .badge {\n  position: relative;\n  top: -1px;\n}\n.btn-xs .badge {\n  top: 0;\n  padding: 1px 5px;\n}\na.badge:hover,\na.badge:focus {\n  color: #fff;\n  text-decoration: none;\n  cursor: pointer;\n}\na.list-group-item.active > .badge,\n.nav-pills > .active > a > .badge {\n  color: #428bca;\n  background-color: #fff;\n}\n.nav-pills > li > a > .badge {\n  margin-left: 3px;\n}\n.jumbotron {\n  padding: 30px;\n  margin-bottom: 30px;\n  color: inherit;\n  background-color: #eee;\n}\n.jumbotron h1,\n.jumbotron .h1 {\n  color: inherit;\n}\n.jumbotron p {\n  margin-bottom: 15px;\n  font-size: 21px;\n  font-weight: 200;\n}\n.container .jumbotron {\n  border-radius: 6px;\n}\n.jumbotron .container {\n  max-width: 100%;\n}\n@media screen and (min-width: 768px) {\n  .jumbotron {\n    padding-top: 48px;\n    padding-bottom: 48px;\n  }\n  .container .jumbotron {\n    padding-right: 60px;\n    padding-left: 60px;\n  }\n  .jumbotron h1,\n  .jumbotron .h1 {\n    font-size: 63px;\n  }\n}\n.thumbnail {\n  display: block;\n  padding: 4px;\n  margin-bottom: 20px;\n  line-height: 1.42857143;\n  background-color: #fff;\n  border: 1px solid #ddd;\n  border-radius: 4px;\n  -webkit-transition: all .2s ease-in-out;\n          transition: all .2s ease-in-out;\n}\n.thumbnail > img,\n.thumbnail a > img {\n  margin-right: auto;\n  margin-left: auto;\n}\na.thumbnail:hover,\na.thumbnail:focus,\na.thumbnail.active {\n  border-color: #428bca;\n}\n.thumbnail .caption {\n  padding: 9px;\n  color: #333;\n}\n.alert {\n  padding: 15px;\n  margin-bottom: 20px;\n  border: 1px solid transparent;\n  border-radius: 4px;\n}\n.alert h4 {\n  margin-top: 0;\n  color: inherit;\n}\n.alert .alert-link {\n  font-weight: bold;\n}\n.alert > p,\n.alert > ul {\n  margin-bottom: 0;\n}\n.alert > p + p {\n  margin-top: 5px;\n}\n.alert-dismissable {\n  padding-right: 35px;\n}\n.alert-dismissable .close {\n  position: relative;\n  top: -2px;\n  right: -21px;\n  color: inherit;\n}\n.alert-success {\n  color: #3c763d;\n  background-color: #dff0d8;\n  border-color: #d6e9c6;\n}\n.alert-success hr {\n  border-top-color: #c9e2b3;\n}\n.alert-success .alert-link {\n  color: #2b542c;\n}\n.alert-info {\n  color: #31708f;\n  background-color: #d9edf7;\n  border-color: #bce8f1;\n}\n.alert-info hr {\n  border-top-color: #a6e1ec;\n}\n.alert-info .alert-link {\n  color: #245269;\n}\n.alert-warning {\n  color: #8a6d3b;\n  background-color: #fcf8e3;\n  border-color: #faebcc;\n}\n.alert-warning hr {\n  border-top-color: #f7e1b5;\n}\n.alert-warning .alert-link {\n  color: #66512c;\n}\n.alert-danger {\n  color: #a94442;\n  background-color: #f2dede;\n  border-color: #ebccd1;\n}\n.alert-danger hr {\n  border-top-color: #e4b9c0;\n}\n.alert-danger .alert-link {\n  color: #843534;\n}\n@-webkit-keyframes progress-bar-stripes {\n  from {\n    background-position: 40px 0;\n  }\n  to {\n    background-position: 0 0;\n  }\n}\n@keyframes progress-bar-stripes {\n  from {\n    background-position: 40px 0;\n  }\n  to {\n    background-position: 0 0;\n  }\n}\n.progress {\n  height: 20px;\n  margin-bottom: 20px;\n  overflow: hidden;\n  background-color: #f5f5f5;\n  border-radius: 4px;\n  -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, .1);\n          box-shadow: inset 0 1px 2px rgba(0, 0, 0, .1);\n}\n.progress-bar {\n  float: left;\n  width: 0;\n  height: 100%;\n  font-size: 12px;\n  line-height: 20px;\n  color: #fff;\n  text-align: center;\n  background-color: #428bca;\n  -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .15);\n          box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .15);\n  -webkit-transition: width .6s ease;\n          transition: width .6s ease;\n}\n.progress-striped .progress-bar {\n  background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);\n  background-image:         linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);\n  background-size: 40px 40px;\n}\n.progress.active .progress-bar {\n  -webkit-animation: progress-bar-stripes 2s linear infinite;\n          animation: progress-bar-stripes 2s linear infinite;\n}\n.progress-bar-success {\n  background-color: #5cb85c;\n}\n.progress-striped .progress-bar-success {\n  background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);\n  background-image:         linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);\n}\n.progress-bar-info {\n  background-color: #5bc0de;\n}\n.progress-striped .progress-bar-info {\n  background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);\n  background-image:         linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);\n}\n.progress-bar-warning {\n  background-color: #f0ad4e;\n}\n.progress-striped .progress-bar-warning {\n  background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);\n  background-image:         linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);\n}\n.progress-bar-danger {\n  background-color: #d9534f;\n}\n.progress-striped .progress-bar-danger {\n  background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);\n  background-image:         linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);\n}\n.media,\n.media-body {\n  overflow: hidden;\n  zoom: 1;\n}\n.media,\n.media .media {\n  margin-top: 15px;\n}\n.media:first-child {\n  margin-top: 0;\n}\n.media-object {\n  display: block;\n}\n.media-heading {\n  margin: 0 0 5px;\n}\n.media > .pull-left {\n  margin-right: 10px;\n}\n.media > .pull-right {\n  margin-left: 10px;\n}\n.media-list {\n  padding-left: 0;\n  list-style: none;\n}\n.list-group {\n  padding-left: 0;\n  margin-bottom: 20px;\n}\n.list-group-item {\n  position: relative;\n  display: block;\n  padding: 10px 15px;\n  margin-bottom: -1px;\n  background-color: #fff;\n  border: 1px solid #ddd;\n}\n.list-group-item:first-child {\n  border-top-left-radius: 4px;\n  border-top-right-radius: 4px;\n}\n.list-group-item:last-child {\n  margin-bottom: 0;\n  border-bottom-right-radius: 4px;\n  border-bottom-left-radius: 4px;\n}\n.list-group-item > .badge {\n  float: right;\n}\n.list-group-item > .badge + .badge {\n  margin-right: 5px;\n}\na.list-group-item {\n  color: #555;\n}\na.list-group-item .list-group-item-heading {\n  color: #333;\n}\na.list-group-item:hover,\na.list-group-item:focus {\n  text-decoration: none;\n  background-color: #f5f5f5;\n}\na.list-group-item.active,\na.list-group-item.active:hover,\na.list-group-item.active:focus {\n  z-index: 2;\n  color: #fff;\n  background-color: #428bca;\n  border-color: #428bca;\n}\na.list-group-item.active .list-group-item-heading,\na.list-group-item.active:hover .list-group-item-heading,\na.list-group-item.active:focus .list-group-item-heading {\n  color: inherit;\n}\na.list-group-item.active .list-group-item-text,\na.list-group-item.active:hover .list-group-item-text,\na.list-group-item.active:focus .list-group-item-text {\n  color: #e1edf7;\n}\n.list-group-item-success {\n  color: #3c763d;\n  background-color: #dff0d8;\n}\na.list-group-item-success {\n  color: #3c763d;\n}\na.list-group-item-success .list-group-item-heading {\n  color: inherit;\n}\na.list-group-item-success:hover,\na.list-group-item-success:focus {\n  color: #3c763d;\n  background-color: #d0e9c6;\n}\na.list-group-item-success.active,\na.list-group-item-success.active:hover,\na.list-group-item-success.active:focus {\n  color: #fff;\n  background-color: #3c763d;\n  border-color: #3c763d;\n}\n.list-group-item-info {\n  color: #31708f;\n  background-color: #d9edf7;\n}\na.list-group-item-info {\n  color: #31708f;\n}\na.list-group-item-info .list-group-item-heading {\n  color: inherit;\n}\na.list-group-item-info:hover,\na.list-group-item-info:focus {\n  color: #31708f;\n  background-color: #c4e3f3;\n}\na.list-group-item-info.active,\na.list-group-item-info.active:hover,\na.list-group-item-info.active:focus {\n  color: #fff;\n  background-color: #31708f;\n  border-color: #31708f;\n}\n.list-group-item-warning {\n  color: #8a6d3b;\n  background-color: #fcf8e3;\n}\na.list-group-item-warning {\n  color: #8a6d3b;\n}\na.list-group-item-warning .list-group-item-heading {\n  color: inherit;\n}\na.list-group-item-warning:hover,\na.list-group-item-warning:focus {\n  color: #8a6d3b;\n  background-color: #faf2cc;\n}\na.list-group-item-warning.active,\na.list-group-item-warning.active:hover,\na.list-group-item-warning.active:focus {\n  color: #fff;\n  background-color: #8a6d3b;\n  border-color: #8a6d3b;\n}\n.list-group-item-danger {\n  color: #a94442;\n  background-color: #f2dede;\n}\na.list-group-item-danger {\n  color: #a94442;\n}\na.list-group-item-danger .list-group-item-heading {\n  color: inherit;\n}\na.list-group-item-danger:hover,\na.list-group-item-danger:focus {\n  color: #a94442;\n  background-color: #ebcccc;\n}\na.list-group-item-danger.active,\na.list-group-item-danger.active:hover,\na.list-group-item-danger.active:focus {\n  color: #fff;\n  background-color: #a94442;\n  border-color: #a94442;\n}\n.list-group-item-heading {\n  margin-top: 0;\n  margin-bottom: 5px;\n}\n.list-group-item-text {\n  margin-bottom: 0;\n  line-height: 1.3;\n}\n.panel {\n  margin-bottom: 20px;\n  background-color: #fff;\n  border: 1px solid transparent;\n  border-radius: 4px;\n  -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, .05);\n          box-shadow: 0 1px 1px rgba(0, 0, 0, .05);\n}\n.panel-body {\n  padding: 15px;\n}\n.panel-heading {\n  padding: 10px 15px;\n  border-bottom: 1px solid transparent;\n  border-top-left-radius: 3px;\n  border-top-right-radius: 3px;\n}\n.panel-heading > .dropdown .dropdown-toggle {\n  color: inherit;\n}\n.panel-title {\n  margin-top: 0;\n  margin-bottom: 0;\n  font-size: 16px;\n  color: inherit;\n}\n.panel-title > a {\n  color: inherit;\n}\n.panel-footer {\n  padding: 10px 15px;\n  background-color: #f5f5f5;\n  border-top: 1px solid #ddd;\n  border-bottom-right-radius: 3px;\n  border-bottom-left-radius: 3px;\n}\n.panel > .list-group {\n  margin-bottom: 0;\n}\n.panel > .list-group .list-group-item {\n  border-width: 1px 0;\n  border-radius: 0;\n}\n.panel > .list-group:first-child .list-group-item:first-child {\n  border-top: 0;\n  border-top-left-radius: 3px;\n  border-top-right-radius: 3px;\n}\n.panel > .list-group:last-child .list-group-item:last-child {\n  border-bottom: 0;\n  border-bottom-right-radius: 3px;\n  border-bottom-left-radius: 3px;\n}\n.panel-heading + .list-group .list-group-item:first-child {\n  border-top-width: 0;\n}\n.panel > .table,\n.panel > .table-responsive > .table {\n  margin-bottom: 0;\n}\n.panel > .table:first-child,\n.panel > .table-responsive:first-child > .table:first-child {\n  border-top-left-radius: 3px;\n  border-top-right-radius: 3px;\n}\n.panel > .table:first-child > thead:first-child > tr:first-child td:first-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:first-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child td:first-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:first-child,\n.panel > .table:first-child > thead:first-child > tr:first-child th:first-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:first-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child th:first-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:first-child {\n  border-top-left-radius: 3px;\n}\n.panel > .table:first-child > thead:first-child > tr:first-child td:last-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:last-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child td:last-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:last-child,\n.panel > .table:first-child > thead:first-child > tr:first-child th:last-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:last-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child th:last-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:last-child {\n  border-top-right-radius: 3px;\n}\n.panel > .table:last-child,\n.panel > .table-responsive:last-child > .table:last-child {\n  border-bottom-right-radius: 3px;\n  border-bottom-left-radius: 3px;\n}\n.panel > .table:last-child > tbody:last-child > tr:last-child td:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:first-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child td:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:first-child,\n.panel > .table:last-child > tbody:last-child > tr:last-child th:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:first-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child th:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:first-child {\n  border-bottom-left-radius: 3px;\n}\n.panel > .table:last-child > tbody:last-child > tr:last-child td:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:last-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child td:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:last-child,\n.panel > .table:last-child > tbody:last-child > tr:last-child th:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:last-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child th:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:last-child {\n  border-bottom-right-radius: 3px;\n}\n.panel > .panel-body + .table,\n.panel > .panel-body + .table-responsive {\n  border-top: 1px solid #ddd;\n}\n.panel > .table > tbody:first-child > tr:first-child th,\n.panel > .table > tbody:first-child > tr:first-child td {\n  border-top: 0;\n}\n.panel > .table-bordered,\n.panel > .table-responsive > .table-bordered {\n  border: 0;\n}\n.panel > .table-bordered > thead > tr > th:first-child,\n.panel > .table-responsive > .table-bordered > thead > tr > th:first-child,\n.panel > .table-bordered > tbody > tr > th:first-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > th:first-child,\n.panel > .table-bordered > tfoot > tr > th:first-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > th:first-child,\n.panel > .table-bordered > thead > tr > td:first-child,\n.panel > .table-responsive > .table-bordered > thead > tr > td:first-child,\n.panel > .table-bordered > tbody > tr > td:first-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > td:first-child,\n.panel > .table-bordered > tfoot > tr > td:first-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > td:first-child {\n  border-left: 0;\n}\n.panel > .table-bordered > thead > tr > th:last-child,\n.panel > .table-responsive > .table-bordered > thead > tr > th:last-child,\n.panel > .table-bordered > tbody > tr > th:last-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > th:last-child,\n.panel > .table-bordered > tfoot > tr > th:last-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > th:last-child,\n.panel > .table-bordered > thead > tr > td:last-child,\n.panel > .table-responsive > .table-bordered > thead > tr > td:last-child,\n.panel > .table-bordered > tbody > tr > td:last-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > td:last-child,\n.panel > .table-bordered > tfoot > tr > td:last-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > td:last-child {\n  border-right: 0;\n}\n.panel > .table-bordered > thead > tr:first-child > td,\n.panel > .table-responsive > .table-bordered > thead > tr:first-child > td,\n.panel > .table-bordered > tbody > tr:first-child > td,\n.panel > .table-responsive > .table-bordered > tbody > tr:first-child > td,\n.panel > .table-bordered > thead > tr:first-child > th,\n.panel > .table-responsive > .table-bordered > thead > tr:first-child > th,\n.panel > .table-bordered > tbody > tr:first-child > th,\n.panel > .table-responsive > .table-bordered > tbody > tr:first-child > th {\n  border-bottom: 0;\n}\n.panel > .table-bordered > tbody > tr:last-child > td,\n.panel > .table-responsive > .table-bordered > tbody > tr:last-child > td,\n.panel > .table-bordered > tfoot > tr:last-child > td,\n.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > td,\n.panel > .table-bordered > tbody > tr:last-child > th,\n.panel > .table-responsive > .table-bordered > tbody > tr:last-child > th,\n.panel > .table-bordered > tfoot > tr:last-child > th,\n.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > th {\n  border-bottom: 0;\n}\n.panel > .table-responsive {\n  margin-bottom: 0;\n  border: 0;\n}\n.panel-group {\n  margin-bottom: 20px;\n}\n.panel-group .panel {\n  margin-bottom: 0;\n  overflow: hidden;\n  border-radius: 4px;\n}\n.panel-group .panel + .panel {\n  margin-top: 5px;\n}\n.panel-group .panel-heading {\n  border-bottom: 0;\n}\n.panel-group .panel-heading + .panel-collapse .panel-body {\n  border-top: 1px solid #ddd;\n}\n.panel-group .panel-footer {\n  border-top: 0;\n}\n.panel-group .panel-footer + .panel-collapse .panel-body {\n  border-bottom: 1px solid #ddd;\n}\n.panel-default {\n  border-color: #ddd;\n}\n.panel-default > .panel-heading {\n  color: #333;\n  background-color: #f5f5f5;\n  border-color: #ddd;\n}\n.panel-default > .panel-heading + .panel-collapse .panel-body {\n  border-top-color: #ddd;\n}\n.panel-default > .panel-footer + .panel-collapse .panel-body {\n  border-bottom-color: #ddd;\n}\n.panel-primary {\n  border-color: #428bca;\n}\n.panel-primary > .panel-heading {\n  color: #fff;\n  background-color: #428bca;\n  border-color: #428bca;\n}\n.panel-primary > .panel-heading + .panel-collapse .panel-body {\n  border-top-color: #428bca;\n}\n.panel-primary > .panel-footer + .panel-collapse .panel-body {\n  border-bottom-color: #428bca;\n}\n.panel-success {\n  border-color: #d6e9c6;\n}\n.panel-success > .panel-heading {\n  color: #3c763d;\n  background-color: #dff0d8;\n  border-color: #d6e9c6;\n}\n.panel-success > .panel-heading + .panel-collapse .panel-body {\n  border-top-color: #d6e9c6;\n}\n.panel-success > .panel-footer + .panel-collapse .panel-body {\n  border-bottom-color: #d6e9c6;\n}\n.panel-info {\n  border-color: #bce8f1;\n}\n.panel-info > .panel-heading {\n  color: #31708f;\n  background-color: #d9edf7;\n  border-color: #bce8f1;\n}\n.panel-info > .panel-heading + .panel-collapse .panel-body {\n  border-top-color: #bce8f1;\n}\n.panel-info > .panel-footer + .panel-collapse .panel-body {\n  border-bottom-color: #bce8f1;\n}\n.panel-warning {\n  border-color: #faebcc;\n}\n.panel-warning > .panel-heading {\n  color: #8a6d3b;\n  background-color: #fcf8e3;\n  border-color: #faebcc;\n}\n.panel-warning > .panel-heading + .panel-collapse .panel-body {\n  border-top-color: #faebcc;\n}\n.panel-warning > .panel-footer + .panel-collapse .panel-body {\n  border-bottom-color: #faebcc;\n}\n.panel-danger {\n  border-color: #ebccd1;\n}\n.panel-danger > .panel-heading {\n  color: #a94442;\n  background-color: #f2dede;\n  border-color: #ebccd1;\n}\n.panel-danger > .panel-heading + .panel-collapse .panel-body {\n  border-top-color: #ebccd1;\n}\n.panel-danger > .panel-footer + .panel-collapse .panel-body {\n  border-bottom-color: #ebccd1;\n}\n.well {\n  min-height: 20px;\n  padding: 19px;\n  margin-bottom: 20px;\n  background-color: #f5f5f5;\n  border: 1px solid #e3e3e3;\n  border-radius: 4px;\n  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .05);\n          box-shadow: inset 0 1px 1px rgba(0, 0, 0, .05);\n}\n.well blockquote {\n  border-color: #ddd;\n  border-color: rgba(0, 0, 0, .15);\n}\n.well-lg {\n  padding: 24px;\n  border-radius: 6px;\n}\n.well-sm {\n  padding: 9px;\n  border-radius: 3px;\n}\n.close {\n  float: right;\n  font-size: 21px;\n  font-weight: bold;\n  line-height: 1;\n  color: #000;\n  text-shadow: 0 1px 0 #fff;\n  filter: alpha(opacity=20);\n  opacity: .2;\n}\n.close:hover,\n.close:focus {\n  color: #000;\n  text-decoration: none;\n  cursor: pointer;\n  filter: alpha(opacity=50);\n  opacity: .5;\n}\nbutton.close {\n  -webkit-appearance: none;\n  padding: 0;\n  cursor: pointer;\n  background: transparent;\n  border: 0;\n}\n.modal-open {\n  overflow: hidden;\n}\n.modal {\n  position: fixed;\n  top: 0;\n  right: 0;\n  bottom: 0;\n  left: 0;\n  z-index: 1050;\n  display: none;\n  overflow: auto;\n  overflow-y: scroll;\n  -webkit-overflow-scrolling: touch;\n  outline: 0;\n}\n.modal.fade .modal-dialog {\n  -webkit-transition: -webkit-transform .3s ease-out;\n     -moz-transition:    -moz-transform .3s ease-out;\n       -o-transition:      -o-transform .3s ease-out;\n          transition:         transform .3s ease-out;\n  -webkit-transform: translate(0, -25%);\n      -ms-transform: translate(0, -25%);\n          transform: translate(0, -25%);\n}\n.modal.in .modal-dialog {\n  -webkit-transform: translate(0, 0);\n      -ms-transform: translate(0, 0);\n          transform: translate(0, 0);\n}\n.modal-dialog {\n  position: relative;\n  width: auto;\n  margin: 10px;\n}\n.modal-content {\n  position: relative;\n  background-color: #fff;\n  background-clip: padding-box;\n  border: 1px solid #999;\n  border: 1px solid rgba(0, 0, 0, .2);\n  border-radius: 6px;\n  outline: none;\n  -webkit-box-shadow: 0 3px 9px rgba(0, 0, 0, .5);\n          box-shadow: 0 3px 9px rgba(0, 0, 0, .5);\n}\n.modal-backdrop {\n  position: fixed;\n  top: 0;\n  right: 0;\n  bottom: 0;\n  left: 0;\n  z-index: 1040;\n  background-color: #000;\n}\n.modal-backdrop.fade {\n  filter: alpha(opacity=0);\n  opacity: 0;\n}\n.modal-backdrop.in {\n  filter: alpha(opacity=50);\n  opacity: .5;\n}\n.modal-header {\n  min-height: 16.42857143px;\n  padding: 15px;\n  border-bottom: 1px solid #e5e5e5;\n}\n.modal-header .close {\n  margin-top: -2px;\n}\n.modal-title {\n  margin: 0;\n  line-height: 1.42857143;\n}\n.modal-body {\n  position: relative;\n  padding: 20px;\n}\n.modal-footer {\n  padding: 19px 20px 20px;\n  margin-top: 15px;\n  text-align: right;\n  border-top: 1px solid #e5e5e5;\n}\n.modal-footer .btn + .btn {\n  margin-bottom: 0;\n  margin-left: 5px;\n}\n.modal-footer .btn-group .btn + .btn {\n  margin-left: -1px;\n}\n.modal-footer .btn-block + .btn-block {\n  margin-left: 0;\n}\n@media (min-width: 768px) {\n  .modal-dialog {\n    width: 600px;\n    margin: 30px auto;\n  }\n  .modal-content {\n    -webkit-box-shadow: 0 5px 15px rgba(0, 0, 0, .5);\n            box-shadow: 0 5px 15px rgba(0, 0, 0, .5);\n  }\n  .modal-sm {\n    width: 300px;\n  }\n}\n@media (min-width: 992px) {\n  .modal-lg {\n    width: 900px;\n  }\n}\n.tooltip {\n  position: absolute;\n  z-index: 1030;\n  display: block;\n  font-size: 12px;\n  line-height: 1.4;\n  visibility: visible;\n  filter: alpha(opacity=0);\n  opacity: 0;\n}\n.tooltip.in {\n  filter: alpha(opacity=90);\n  opacity: .9;\n}\n.tooltip.top {\n  padding: 5px 0;\n  margin-top: -3px;\n}\n.tooltip.right {\n  padding: 0 5px;\n  margin-left: 3px;\n}\n.tooltip.bottom {\n  padding: 5px 0;\n  margin-top: 3px;\n}\n.tooltip.left {\n  padding: 0 5px;\n  margin-left: -3px;\n}\n.tooltip-inner {\n  max-width: 200px;\n  padding: 3px 8px;\n  color: #fff;\n  text-align: center;\n  text-decoration: none;\n  background-color: #000;\n  border-radius: 4px;\n}\n.tooltip-arrow {\n  position: absolute;\n  width: 0;\n  height: 0;\n  border-color: transparent;\n  border-style: solid;\n}\n.tooltip.top .tooltip-arrow {\n  bottom: 0;\n  left: 50%;\n  margin-left: -5px;\n  border-width: 5px 5px 0;\n  border-top-color: #000;\n}\n.tooltip.top-left .tooltip-arrow {\n  bottom: 0;\n  left: 5px;\n  border-width: 5px 5px 0;\n  border-top-color: #000;\n}\n.tooltip.top-right .tooltip-arrow {\n  right: 5px;\n  bottom: 0;\n  border-width: 5px 5px 0;\n  border-top-color: #000;\n}\n.tooltip.right .tooltip-arrow {\n  top: 50%;\n  left: 0;\n  margin-top: -5px;\n  border-width: 5px 5px 5px 0;\n  border-right-color: #000;\n}\n.tooltip.left .tooltip-arrow {\n  top: 50%;\n  right: 0;\n  margin-top: -5px;\n  border-width: 5px 0 5px 5px;\n  border-left-color: #000;\n}\n.tooltip.bottom .tooltip-arrow {\n  top: 0;\n  left: 50%;\n  margin-left: -5px;\n  border-width: 0 5px 5px;\n  border-bottom-color: #000;\n}\n.tooltip.bottom-left .tooltip-arrow {\n  top: 0;\n  left: 5px;\n  border-width: 0 5px 5px;\n  border-bottom-color: #000;\n}\n.tooltip.bottom-right .tooltip-arrow {\n  top: 0;\n  right: 5px;\n  border-width: 0 5px 5px;\n  border-bottom-color: #000;\n}\n.popover {\n  position: absolute;\n  top: 0;\n  left: 0;\n  z-index: 1010;\n  display: none;\n  max-width: 276px;\n  padding: 1px;\n  text-align: left;\n  white-space: normal;\n  background-color: #fff;\n  background-clip: padding-box;\n  border: 1px solid #ccc;\n  border: 1px solid rgba(0, 0, 0, .2);\n  border-radius: 6px;\n  -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, .2);\n          box-shadow: 0 5px 10px rgba(0, 0, 0, .2);\n}\n.popover.top {\n  margin-top: -10px;\n}\n.popover.right {\n  margin-left: 10px;\n}\n.popover.bottom {\n  margin-top: 10px;\n}\n.popover.left {\n  margin-left: -10px;\n}\n.popover-title {\n  padding: 8px 14px;\n  margin: 0;\n  font-size: 14px;\n  font-weight: normal;\n  line-height: 18px;\n  background-color: #f7f7f7;\n  border-bottom: 1px solid #ebebeb;\n  border-radius: 5px 5px 0 0;\n}\n.popover-content {\n  padding: 9px 14px;\n}\n.popover > .arrow,\n.popover > .arrow:after {\n  position: absolute;\n  display: block;\n  width: 0;\n  height: 0;\n  border-color: transparent;\n  border-style: solid;\n}\n.popover > .arrow {\n  border-width: 11px;\n}\n.popover > .arrow:after {\n  content: \"\";\n  border-width: 10px;\n}\n.popover.top > .arrow {\n  bottom: -11px;\n  left: 50%;\n  margin-left: -11px;\n  border-top-color: #999;\n  border-top-color: rgba(0, 0, 0, .25);\n  border-bottom-width: 0;\n}\n.popover.top > .arrow:after {\n  bottom: 1px;\n  margin-left: -10px;\n  content: \" \";\n  border-top-color: #fff;\n  border-bottom-width: 0;\n}\n.popover.right > .arrow {\n  top: 50%;\n  left: -11px;\n  margin-top: -11px;\n  border-right-color: #999;\n  border-right-color: rgba(0, 0, 0, .25);\n  border-left-width: 0;\n}\n.popover.right > .arrow:after {\n  bottom: -10px;\n  left: 1px;\n  content: \" \";\n  border-right-color: #fff;\n  border-left-width: 0;\n}\n.popover.bottom > .arrow {\n  top: -11px;\n  left: 50%;\n  margin-left: -11px;\n  border-top-width: 0;\n  border-bottom-color: #999;\n  border-bottom-color: rgba(0, 0, 0, .25);\n}\n.popover.bottom > .arrow:after {\n  top: 1px;\n  margin-left: -10px;\n  content: \" \";\n  border-top-width: 0;\n  border-bottom-color: #fff;\n}\n.popover.left > .arrow {\n  top: 50%;\n  right: -11px;\n  margin-top: -11px;\n  border-right-width: 0;\n  border-left-color: #999;\n  border-left-color: rgba(0, 0, 0, .25);\n}\n.popover.left > .arrow:after {\n  right: 1px;\n  bottom: -10px;\n  content: \" \";\n  border-right-width: 0;\n  border-left-color: #fff;\n}\n.carousel {\n  position: relative;\n}\n.carousel-inner {\n  position: relative;\n  width: 100%;\n  overflow: hidden;\n}\n.carousel-inner > .item {\n  position: relative;\n  display: none;\n  -webkit-transition: .6s ease-in-out left;\n          transition: .6s ease-in-out left;\n}\n.carousel-inner > .item > img,\n.carousel-inner > .item > a > img {\n  line-height: 1;\n}\n.carousel-inner > .active,\n.carousel-inner > .next,\n.carousel-inner > .prev {\n  display: block;\n}\n.carousel-inner > .active {\n  left: 0;\n}\n.carousel-inner > .next,\n.carousel-inner > .prev {\n  position: absolute;\n  top: 0;\n  width: 100%;\n}\n.carousel-inner > .next {\n  left: 100%;\n}\n.carousel-inner > .prev {\n  left: -100%;\n}\n.carousel-inner > .next.left,\n.carousel-inner > .prev.right {\n  left: 0;\n}\n.carousel-inner > .active.left {\n  left: -100%;\n}\n.carousel-inner > .active.right {\n  left: 100%;\n}\n.carousel-control {\n  position: absolute;\n  top: 0;\n  bottom: 0;\n  left: 0;\n  width: 15%;\n  font-size: 20px;\n  color: #fff;\n  text-align: center;\n  text-shadow: 0 1px 2px rgba(0, 0, 0, .6);\n  filter: alpha(opacity=50);\n  opacity: .5;\n}\n.carousel-control.left {\n  background-image: -webkit-linear-gradient(left, color-stop(rgba(0, 0, 0, .5) 0%), color-stop(rgba(0, 0, 0, .0001) 100%));\n  background-image:         linear-gradient(to right, rgba(0, 0, 0, .5) 0%, rgba(0, 0, 0, .0001) 100%);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);\n  background-repeat: repeat-x;\n}\n.carousel-control.right {\n  right: 0;\n  left: auto;\n  background-image: -webkit-linear-gradient(left, color-stop(rgba(0, 0, 0, .0001) 0%), color-stop(rgba(0, 0, 0, .5) 100%));\n  background-image:         linear-gradient(to right, rgba(0, 0, 0, .0001) 0%, rgba(0, 0, 0, .5) 100%);\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);\n  background-repeat: repeat-x;\n}\n.carousel-control:hover,\n.carousel-control:focus {\n  color: #fff;\n  text-decoration: none;\n  filter: alpha(opacity=90);\n  outline: none;\n  opacity: .9;\n}\n.carousel-control .icon-prev,\n.carousel-control .icon-next,\n.carousel-control .glyphicon-chevron-left,\n.carousel-control .glyphicon-chevron-right {\n  position: absolute;\n  top: 50%;\n  z-index: 5;\n  display: inline-block;\n}\n.carousel-control .icon-prev,\n.carousel-control .glyphicon-chevron-left {\n  left: 50%;\n}\n.carousel-control .icon-next,\n.carousel-control .glyphicon-chevron-right {\n  right: 50%;\n}\n.carousel-control .icon-prev,\n.carousel-control .icon-next {\n  width: 20px;\n  height: 20px;\n  margin-top: -10px;\n  margin-left: -10px;\n  font-family: serif;\n}\n.carousel-control .icon-prev:before {\n  content: '\\2039';\n}\n.carousel-control .icon-next:before {\n  content: '\\203a';\n}\n.carousel-indicators {\n  position: absolute;\n  bottom: 10px;\n  left: 50%;\n  z-index: 15;\n  width: 60%;\n  padding-left: 0;\n  margin-left: -30%;\n  text-align: center;\n  list-style: none;\n}\n.carousel-indicators li {\n  display: inline-block;\n  width: 10px;\n  height: 10px;\n  margin: 1px;\n  text-indent: -999px;\n  cursor: pointer;\n  background-color: #000 \\9;\n  background-color: rgba(0, 0, 0, 0);\n  border: 1px solid #fff;\n  border-radius: 10px;\n}\n.carousel-indicators .active {\n  width: 12px;\n  height: 12px;\n  margin: 0;\n  background-color: #fff;\n}\n.carousel-caption {\n  position: absolute;\n  right: 15%;\n  bottom: 20px;\n  left: 15%;\n  z-index: 10;\n  padding-top: 20px;\n  padding-bottom: 20px;\n  color: #fff;\n  text-align: center;\n  text-shadow: 0 1px 2px rgba(0, 0, 0, .6);\n}\n.carousel-caption .btn {\n  text-shadow: none;\n}\n@media screen and (min-width: 768px) {\n  .carousel-control .glyphicon-chevron-left,\n  .carousel-control .glyphicon-chevron-right,\n  .carousel-control .icon-prev,\n  .carousel-control .icon-next {\n    width: 30px;\n    height: 30px;\n    margin-top: -15px;\n    margin-left: -15px;\n    font-size: 30px;\n  }\n  .carousel-caption {\n    right: 20%;\n    left: 20%;\n    padding-bottom: 30px;\n  }\n  .carousel-indicators {\n    bottom: 20px;\n  }\n}\n.clearfix:before,\n.clearfix:after,\n.container:before,\n.container:after,\n.container-fluid:before,\n.container-fluid:after,\n.row:before,\n.row:after,\n.form-horizontal .form-group:before,\n.form-horizontal .form-group:after,\n.btn-toolbar:before,\n.btn-toolbar:after,\n.btn-group-vertical > .btn-group:before,\n.btn-group-vertical > .btn-group:after,\n.nav:before,\n.nav:after,\n.navbar:before,\n.navbar:after,\n.navbar-header:before,\n.navbar-header:after,\n.navbar-collapse:before,\n.navbar-collapse:after,\n.pager:before,\n.pager:after,\n.panel-body:before,\n.panel-body:after,\n.modal-footer:before,\n.modal-footer:after {\n  display: table;\n  content: \" \";\n}\n.clearfix:after,\n.container:after,\n.container-fluid:after,\n.row:after,\n.form-horizontal .form-group:after,\n.btn-toolbar:after,\n.btn-group-vertical > .btn-group:after,\n.nav:after,\n.navbar:after,\n.navbar-header:after,\n.navbar-collapse:after,\n.pager:after,\n.panel-body:after,\n.modal-footer:after {\n  clear: both;\n}\n.center-block {\n  display: block;\n  margin-right: auto;\n  margin-left: auto;\n}\n.pull-right {\n  float: right !important;\n}\n.pull-left {\n  float: left !important;\n}\n.hide {\n  display: none !important;\n}\n.show {\n  display: block !important;\n}\n.invisible {\n  visibility: hidden;\n}\n.text-hide {\n  font: 0/0 a;\n  color: transparent;\n  text-shadow: none;\n  background-color: transparent;\n  border: 0;\n}\n.hidden {\n  display: none !important;\n  visibility: hidden !important;\n}\n.affix {\n  position: fixed;\n}\n@-ms-viewport {\n  width: device-width;\n}\n.visible-xs,\n.visible-sm,\n.visible-md,\n.visible-lg {\n  display: none !important;\n}\n@media (max-width: 767px) {\n  .visible-xs {\n    display: block !important;\n  }\n  table.visible-xs {\n    display: table;\n  }\n  tr.visible-xs {\n    display: table-row !important;\n  }\n  th.visible-xs,\n  td.visible-xs {\n    display: table-cell !important;\n  }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n  .visible-sm {\n    display: block !important;\n  }\n  table.visible-sm {\n    display: table;\n  }\n  tr.visible-sm {\n    display: table-row !important;\n  }\n  th.visible-sm,\n  td.visible-sm {\n    display: table-cell !important;\n  }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n  .visible-md {\n    display: block !important;\n  }\n  table.visible-md {\n    display: table;\n  }\n  tr.visible-md {\n    display: table-row !important;\n  }\n  th.visible-md,\n  td.visible-md {\n    display: table-cell !important;\n  }\n}\n@media (min-width: 1200px) {\n  .visible-lg {\n    display: block !important;\n  }\n  table.visible-lg {\n    display: table;\n  }\n  tr.visible-lg {\n    display: table-row !important;\n  }\n  th.visible-lg,\n  td.visible-lg {\n    display: table-cell !important;\n  }\n}\n@media (max-width: 767px) {\n  .hidden-xs {\n    display: none !important;\n  }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n  .hidden-sm {\n    display: none !important;\n  }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n  .hidden-md {\n    display: none !important;\n  }\n}\n@media (min-width: 1200px) {\n  .hidden-lg {\n    display: none !important;\n  }\n}\n.visible-print {\n  display: none !important;\n}\n@media print {\n  .visible-print {\n    display: block !important;\n  }\n  table.visible-print {\n    display: table;\n  }\n  tr.visible-print {\n    display: table-row !important;\n  }\n  th.visible-print,\n  td.visible-print {\n    display: table-cell !important;\n  }\n}\n@media print {\n  .hidden-print {\n    display: none !important;\n  }\n}\n/*# sourceMappingURL=bootstrap.css.map */\n"
  },
  {
    "path": "price_monitor/static/price_monitor/css/base.css",
    "content": "#footer div {\n    font-size: 12px;\n    margin-top: 30px;\n    text-align: center;\n}"
  },
  {
    "path": "price_monitor/static/price_monitor/css/inline-form.css",
    "content": "#empty-line {\n    display: none;\n}\n\nform.form-inline .glyphicon {\n    cursor: pointer;\n}\n\nform.form-inline .row {\n    padding: 5px 0;\n}\n\nform.form-inline input,\nform.form-inline select {\n    width: 100%;\n}\n\nform.form-inline #form-add-button {\n    margin-left: 5px;\n}"
  },
  {
    "path": "price_monitor/tasks.py",
    "content": "\"\"\"General tasks\"\"\"\nimport logging\n\nfrom celery.task import Task\n\nfrom price_monitor.models import (\n    Price,\n    Product,\n    Subscription,\n)\n\n\nlogger = logging.getLogger('price_monitor.tasks')\n\n\nclass ProductCleanupTask(Task):\n\n    \"\"\"Task for removing a product if it has no subscribers.\"\"\"\n\n    def run(self, asin):\n        \"\"\"\n        Checks if there are subscribers for the product with the given asin. If not, the product and its prices are deleted.\n\n        :param asin: the ASIN of the product\n        :type asin: str\n        :return: success or failure\n        \"\"\"\n        try:\n            product = Product.objects.get(asin=asin)\n        except Product.DoesNotExist:\n            logger.error('Product with ASIN %d does not exist, skipping ProductCleanupTask', asin)\n            return\n\n        subscribers = Subscription.objects.filter(product=product).count()\n\n        if subscribers == 0:\n            prices = Price.objects.filter(product=product)\n            logger.info('Removing product with ASIN %s (PK: %d) and its %d prices', asin, product.pk, prices.count())\n            prices.delete()\n            product.delete()\n            return True\n"
  },
  {
    "path": "price_monitor/templates/price_monitor/angular_index_view.html",
    "content": "{% load static %}<!DOCTYPE html>\n<html ng-app=\"PriceMonitorApp\">\n    <head>\n        <title>Amazon Price Monitor</title>\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n        <!-- Bootstrap -->\n        <link href=\"{% get_static_prefix %}price_monitor/bootstrap/css/bootstrap.min.css\" rel=\"stylesheet\" media=\"screen\">\n        <link href=\"{% get_static_prefix %}price_monitor/app/css/app.css\" rel=\"stylesheet\" media=\"screen\">\n        <link href=\"{% get_static_prefix %}price_monitor/app/css/xeditable.css\" rel=\"stylesheet\" media=\"screen\">\n    </head>\n    <body ng-controller=\"MainCtrl\">\n        <header class=\"navbar navbar-default navbar-fixed-top\" role=\"navigation\">\n            <div class=\"container\">\n                <div class=\"navbar-header\">\n                    <button type=\"button\" class=\"navbar-toggle\" data-toggle=\"collapse\" data-target=\"#bs-navbar-collapse-1\">\n                        <span class=\"sr-only\">Toggle navigation</span>\n                        <span class=\"icon-bar\"></span>\n                        <span class=\"icon-bar\"></span>\n                    </button>\n                    <a class=\"navbar-brand\" href=\"#\">Amazon Price Monitor</a>\n                </div>\n                <div class=\"collapse navbar-collapse\" id=\"bs-navbar-collapse-1\">\n                    <ul class=\"nav navbar-nav\">\n                        <li ng-class=\"{active:isActive('/products')}\">\n                            <a href=\"#/products\">\n                                <span class=\"glyphicon glyphicon-home\"></span>\n                                Products\n                            </a>\n                        </li>\n                    </ul>\n                </div>\n            </div>\n        </header>\n    \n        <div class=\"content\">\n            <div class=\"container\">\n                <div ng-view></div>\n                <div class=\"row\" id=\"footer\">\n                    <div class=\"col-md-12\">\n                        {{ product_advertising_disclaimer }}\n                        {{ associate_disclaimer }}\n                    </div>\n                </div>\n                {% block footer %}{% endblock %}\n            </div>\n        </div>\n                \n        <!-- Angular.js -->\n        <script type=\"text/javascript\">\n            var SETTINGS = {\n                'uris': {\n                    'static': '{% get_static_prefix %}',\n                    'price': '{% url \"api_product_list\" %}:asin/prices/',\n                    'product': '{% url \"api_product_list\" %}:asin/',\n                    'subscription': '{% url \"api_subscription_list\" %}:public_id/{#{% url \"api_subscription_retrieve\" public_id=\":public_id\" %}#}',\n                    'emailNotification': '{% url \"api_email_notification_list\" %}',\n                    'sparkline': '{% url \"api_product_list\" %}:asin/prices/?show_legend=false&show_dots=false&margin=4&spacing=0&height=40&width=400&show_x_labels=false&y_labels_major_count=2&show_minor_y_labels=false&no_data_font_size=10',\n                    'chart': {\n                        'default': '{% url \"api_product_list\" %}:asin/prices/?show_legend=false&show_x_values=false&margin=4&spacing=0&y_labels_major_count=5&show_minor_y_labels=true&no_data_font_size=10&width=400&height=200',\n                        'small': '{% url \"api_product_list\" %}:asin/prices/?show_legend=false&margin=7&spacing=16&y_labels_major_count=5&show_minor_y_labels=true&no_data_font_size=20&width=600&height=200',\n                        'medium': '{% url \"api_product_list\" %}:asin/prices/?show_legend=false&margin=7&spacing=16&y_labels_major_count=5&show_minor_y_labels=true&no_data_font_size=20&width=1000&height=200',\n                        'large': '{% url \"api_product_list\" %}:asin/prices/?show_legend=false&margin=7&spacing=16&y_labels_major_count=5&show_minor_y_labels=true&no_data_font_size=20&width=1140&height=200'\n                    }\n                },\n                'pagination': {\n                    'maxPageCount': 7,\n                    'itemsPerPage': 10,\n                    'paginationBoundaryLinks': true,\n                    'paginationRotate': true\n                },\n                'defaultCurrency': '{{ default_currency }}',\n                'siteName': '{{ site_name }}'\n            };\n        </script>\n        <script src=\"{% get_static_prefix %}price_monitor/angular/angular.1.3.9.min.js\"></script>\n        <script src=\"{% get_static_prefix %}price_monitor/angular/angular-cookies.min.js\"></script>\n        <script src=\"{% get_static_prefix %}price_monitor/angular/angular-resource.min.js\"></script>\n        <script src=\"{% get_static_prefix %}price_monitor/angular/angular-route.min.js\"></script>\n        <script src=\"{% get_static_prefix %}price_monitor/angular/ui-bootstrap-tpls-0.11.0.min.js\"></script>\n        <script src=\"{% get_static_prefix %}price_monitor/angular/angular-django-rest-resource.js\"></script>\n        <script src=\"{% get_static_prefix %}price_monitor/angular/angular-responsive-images.js\"></script>\n        <script src=\"{% get_static_prefix %}price_monitor/angular/xeditable.min.js\"></script>\n        <script src=\"{% get_static_prefix %}price_monitor/app/js/server-connector.js\"></script>\n        <script src=\"{% get_static_prefix %}price_monitor/app/js/app.js\"></script>\n        <script src=\"{% get_static_prefix %}price_monitor/app/js/filters.js\"></script>\n        <script src=\"{% get_static_prefix %}price_monitor/app/js/controller/main-ctrl.js\"></script>\n        <script src=\"{% get_static_prefix %}price_monitor/app/js/controller/emailnotification-create-ctrl.js\"></script>\n        <script src=\"{% get_static_prefix %}price_monitor/app/js/controller/product-delete-ctrl.js\"></script>\n        <script src=\"{% get_static_prefix %}price_monitor/app/js/controller/product-detail-ctrl.js\"></script>\n        <script src=\"{% get_static_prefix %}price_monitor/app/js/controller/product-list-ctrl.js\"></script>\n    </body>\n</html>\n"
  },
  {
    "path": "price_monitor/urls.py",
    "content": "from django.conf.urls import include, url\n\nfrom price_monitor.views import AngularIndexView\n\nurlpatterns = [\n    url(r'^$', AngularIndexView.as_view(), name='angular_view'),\n    url(r'^api/', include('price_monitor.api.urls')),\n]\n"
  },
  {
    "path": "price_monitor/utils.py",
    "content": "\"\"\"Several util functions\"\"\"\nimport logging\n\nfrom django.core.mail import send_mail as django_send_mail\nfrom django.utils.translation import ugettext as _\n\nfrom price_monitor import app_settings\n\n\nlogger = logging.getLogger('price_monitor.utils')\n\n\ndef get_offer_url(asin):\n    \"\"\"\n    Returns the offer url for an ASIN.\n\n    :param asin: the asin\n    :type asin: basestring\n    :return: the url to the offer\n    :rtype: basestring\n    \"\"\"\n    return app_settings.PRICE_MONITOR_OFFER_URL.format(**{\n        'domain': app_settings.PRICE_MONITOR_AMAZON_REGION_DOMAINS[app_settings.PRICE_MONITOR_AMAZON_PRODUCT_API_REGION],\n        'asin': asin,\n        'assoc_tag': app_settings.PRICE_MONITOR_AMAZON_PRODUCT_API_ASSOC_TAG,\n    })\n\n\ndef get_product_detail_url(asin):\n    \"\"\"\n    Returns the url to a product detail view.\n\n    As the frontend is AngularJS, we cannot use any Django reverse functionality.\n    :param asin: the asin to use\n    :return: the link\n    \"\"\"\n    return '{base_url:s}/#/products/{asin:s}'.format(\n        base_url=app_settings.PRICE_MONITOR_BASE_URL,\n        asin=asin,\n    )\n\n\ndef send_mail(product, subscription, price, additional_text=''):\n    \"\"\"\n    Sends an email using the appropriate settings for formatting aso.\n\n    :param product: the product\n    :type product: price_monitor.models.Product\n    :param subscription: the subscription\n    :type subscription: price_monitor.models.Subscription\n    :param price: the current price\n    :type price: price_monitor.models.Price\n    :param additional_text: additional text to include in mail\n    :type additional_text: str\n    \"\"\"\n    django_send_mail(\n        _(app_settings.PRICE_MONITOR_I18N_EMAIL_NOTIFICATION_SUBJECT) % {'product': product.title},\n        _(app_settings.PRICE_MONITOR_I18N_EMAIL_NOTIFICATION_BODY).format(\n            price_limit=subscription.price_limit,\n            currency=price.currency,\n            price=price.value,\n            price_date=price.date_seen.strftime('%b %d, %Y %H:%M %p %Z'),\n            product_title=product.get_title(),\n            url_product_amazon=product.offer_url,\n            url_product_detail=product.get_detail_url(),\n            additional_text=additional_text,\n        ),\n        app_settings.PRICE_MONITOR_EMAIL_SENDER,\n        [subscription.email_notification.email],\n        fail_silently=False,\n    )\n\n\ndef chunk_list(the_list, chunk_size):\n    \"\"\"\n    Chunks a list.\n\n    :param the_list: list to chunk\n    :type the_list: list\n    :param chunk_size: number of elements to be contained in each created chunk list\n    :type chunk_size: int\n    :return: generator object with the chunked lists\n    :rtype: generator\n    \"\"\"\n    for i in range(0, len(the_list), chunk_size):\n        yield the_list[i:i + chunk_size]\n"
  },
  {
    "path": "price_monitor/views.py",
    "content": "import json\nimport logging\n\nfrom django.contrib.auth.decorators import login_required\nfrom django.http import HttpResponse\nfrom django.utils.decorators import method_decorator\nfrom django.views.generic import TemplateView\n\nfrom . import app_settings\nfrom .forms import SubscriptionCreationForm\n\nlogger = logging.getLogger('price_monitor')\n\n\nclass AngularIndexView(TemplateView):\n    template_name = 'price_monitor/angular_index_view.html'\n    form = SubscriptionCreationForm\n\n    @method_decorator(login_required)\n    def dispatch(self, *args, **kwargs):\n        \"\"\"\n        Overwriting this method the make every instance of the view\n        login_required\n        :param args: positional arguments\n        :type args: List\n        :param kwargs: keyword arguments\n        :type kwargs: Dict\n        :return: Result of super method. As this dispatches the handling method\n        for the incoming request and calls it, the return is a HttpResponse\n        object\n        :rtype: HttpResponse\n        \"\"\"\n        return super(AngularIndexView, self).dispatch(*args, **kwargs)\n\n    def get_context_data(self, form=None, **kwargs):\n        context = super(AngularIndexView, self).get_context_data(**kwargs)\n        context.update(\n            default_currency=app_settings.PRICE_MONITOR_DEFAULT_CURRENCY,\n            subscription_create_form=form,\n            site_name=app_settings.PRICE_MONITOR_SITENAME,\n            product_advertising_disclaimer=app_settings.PRICE_MONITOR_AMAZON_PRODUCT_ADVERTISING_API_DISCLAIMER,\n            associate_disclaimer=app_settings.PRICE_MONITOR_ASSOCIATE_DISCLAIMER,\n        )\n        return context\n\n    def get(self, request, **kwargs):\n        form = self.form()\n        context = self.get_context_data(form=form, **kwargs)\n        return self.render_to_response(context)\n\n    def post(self, request, **kwargs):\n        in_data = json.loads(request.body)\n        form = self.form(data=in_data)\n        response_data = {'errors': form.errors}\n        return HttpResponse(json.dumps(response_data), content_type=\"application/json\")\n"
  },
  {
    "path": "setup.cfg",
    "content": "[wheel]\nuniversal = 1"
  },
  {
    "path": "setup.py",
    "content": "#!/usr/bin/env python\n\"\"\"Setup file for the django-amazon-price-monitor package.\"\"\"\ntry:\n    from setuptools import setup\nexcept ImportError:\n    from distutils.core import setup\n\n\nreadme = open('README.rst').read()\nhistory = open('HISTORY.rst').read()\n\nsetup(\n    name='django-amazon-price-monitor',\n    version=__import__('price_monitor').get_version().replace(' ', '-'),\n    description='Monitors prices of Amazon products via Product Advertising API',\n    long_description=readme + '\\n\\n' + history,\n    author='Alexander Herrmann, Martin Mrose',\n    author_email='django-amazon-price-monitor@googlegroups.com',\n    url='https://github.com/ponyriders/django-amazon-price-monitor',\n    packages=[\n        'price_monitor'\n    ],\n    include_package_data=True,\n    install_requires=[\n        # main dependencies\n        'Django>=1.8,<2',\n        # for product advertising api\n        'beautifulsoup4<=4.6',\n        'bottlenose>=0.6.2,<1.2',\n        'celery>=4,<4.1',\n        'python-dateutil>=2.5.1,<2.7',\n        'kombu>=4.1.0,<4.2',\n        # for pm api\n        'djangorestframework>=3.3,<3.7',\n        # for graphs\n        'pygal>=2.0.7,<2.5',\n        'lxml>=4,<4.1',\n        # pygal png output\n        'CairoSVG>=2,<2.1',\n        'tinycss>=0.4,<0.5',\n        'cssselect>=1.0.1,<1.1',\n    ],\n    license='MIT',\n    zip_safe=False,\n)\n"
  },
  {
    "path": "tests/__init__.py",
    "content": ""
  },
  {
    "path": "tests/product_advertising_api/__init__.py",
    "content": ""
  },
  {
    "path": "tests/product_advertising_api/data.py",
    "content": "product_sample_lookup_fail = \"\"\"\n<?xml version=\"1.0\" ?>\n<itemlookupresponse xmlns=\"http://webservices.amazon.com/AWSECommerceService/2011-08-01\">\n    <items>\n        <request>\n            <isvalid>False</isvalid>\n            <itemlookuprequest>\n                <idtype>ASIN</idtype>\n                <itemid>DEMOASIN01</itemid>\n                <responsegroup>Large</responsegroup>\n                <variationpage>All</variationpage>\n            </itemlookuprequest>\n        </request>\n    </items>\n</itemlookupresponse>\n\"\"\"\n\nproduct_sample_no_item = \"\"\"\n<?xml version=\"1.0\" ?>\n<itemlookupresponse xmlns=\"http://webservices.amazon.com/AWSECommerceService/2011-08-01\">\n    <items>\n        <request>\n            <isvalid>True</isvalid>\n            <itemlookuprequest>\n                <idtype>ASIN</idtype>\n                <itemid>DEMOASIN02</itemid>\n                <responsegroup>Large</responsegroup>\n                <variationpage>All</variationpage>\n            </itemlookuprequest>\n        </request>\n    </items>\n</itemlookupresponse>\n\"\"\"\n\nproduct_sample_ok = \"\"\"\n<?xml version=\"1.0\" ?>\n<itemlookupresponse xmlns=\"http://webservices.amazon.com/AWSECommerceService/2011-08-01\">\n    <items>\n        <request>\n            <isvalid>True</isvalid>\n            <itemlookuprequest>\n                <idtype>ASIN</idtype>\n                <itemid>DEMOASIN03</itemid>\n                <responsegroup>Large</responsegroup>\n                <variationpage>All</variationpage>\n            </itemlookuprequest>\n        </request>\n        <item>\n            <asin>DEMOASIN03</asin>\n            <itemattributes>\n                <title>Demo Series - Season 2 [Blu-ray]</title>\n                <binding>Blu-ray</binding>\n                <publicationdate>2014-12-22</publicationdate>\n                <releasedate>2014-12-22</releasedate>\n                <audiencerating>Freigegeben ab 16 Jahren</audiencerating>\n            </itemattributes>\n            <smallimage>\n                <url>http://ecx.images-amazon.com/images/I/DEMOASIN03IMAGE._SL75_.jpg</url>\n                <height units=\"pixels\">75</height>\n                <width units=\"pixels\">59</width>\n            </smallimage>\n            <mediumimage>\n                <url>http://ecx.images-amazon.com/images/I/DEMOASIN03IMAGE._SL160_.jpg</url>\n                <height units=\"pixels\">160</height>\n                <width units=\"pixels\">126</width>\n            </mediumimage>\n            <largeimage>\n                <url>http://ecx.images-amazon.com/images/I/DEMOASIN03IMAGE.jpg</url>\n                <height units=\"pixels\">500</height>\n                <width units=\"pixels\">393</width>\n            </largeimage>\n            <offers>\n                <totaloffers>1</totaloffers>\n                <offer>\n                    <offerlisting>\n                        <price>\n                            <amount>1799</amount>\n                            <currencycode>EUR</currencycode>\n                            <formattedprice>EUR 17,99</formattedprice>\n                        </price>\n                    </offerlisting>\n                </offer>\n            </offers>\n        </item>\n    </items>\n</itemlookupresponse>\n\"\"\"\n\nproduct_sample_no_images = \"\"\"\n<?xml version=\"1.0\" ?>\n<itemlookupresponse xmlns=\"http://webservices.amazon.com/AWSECommerceService/2011-08-01\">\n    <items>\n        <request>\n            <isvalid>True</isvalid>\n            <itemlookuprequest>\n                <idtype>ASIN</idtype>\n                <itemid>DEMOASIN03</itemid>\n                <responsegroup>Large</responsegroup>\n                <variationpage>All</variationpage>\n            </itemlookuprequest>\n        </request>\n        <item>\n            <asin>DEMOASIN03</asin>\n            <itemattributes>\n                <title>Demo Series - Season 2 [Blu-ray]</title>\n                <binding>Blu-ray</binding>\n                <publicationdate>2014-12-22</publicationdate>\n                <releasedate>2014-12-22</releasedate>\n                <audiencerating>Freigegeben ab 16 Jahren</audiencerating>\n            </itemattributes>\n            <smallimage>\n            </smallimage>\n            <mediumimage>\n            </mediumimage>\n            <largeimage>\n            </largeimage>\n            <offers>\n                <totaloffers>1</totaloffers>\n                <offer>\n                    <offerlisting>\n                        <price>\n                            <amount>1799</amount>\n                            <currencycode>EUR</currencycode>\n                            <formattedprice>EUR 17,99</formattedprice>\n                        </price>\n                    </offerlisting>\n                </offer>\n            </offers>\n        </item>\n    </items>\n</itemlookupresponse>\n\"\"\"\n\nproduct_sample_no_price = \"\"\"\n<?xml version=\"1.0\" ?>\n<itemlookupresponse xmlns=\"http://webservices.amazon.com/AWSECommerceService/2011-08-01\">\n    <items>\n        <request>\n            <isvalid>True</isvalid>\n            <itemlookuprequest>\n                <idtype>ASIN</idtype>\n                <itemid>DEMOASIN04</itemid>\n                <responsegroup>Large</responsegroup>\n                <variationpage>All</variationpage>\n            </itemlookuprequest>\n        </request>\n        <item>\n            <asin>DEMOASIN04</asin>\n            <itemattributes>\n                <title>Another Demo Series - Season 1 (8 DVDs)</title>\n                <binding>DVD</binding>\n                <publicationdate>2004-11</publicationdate>\n                <releasedate>2004-10-27</releasedate>\n                <audiencerating>Freigegeben ab 16 Jahren</audiencerating>\n            </itemattributes>\n            <smallimage>\n                <url>http://ecx.images-amazon.com/images/I/DEMOASIN04IMAGE._SL75_.jpg</url>\n                <height units=\"pixels\">75</height>\n                <width units=\"pixels\">59</width>\n            </smallimage>\n            <mediumimage>\n                <url>http://ecx.images-amazon.com/images/I/DEMOASIN04IMAGE._SL160_.jpg</url>\n                <height units=\"pixels\">160</height>\n                <width units=\"pixels\">126</width>\n            </mediumimage>\n            <largeimage>\n                <url>http://ecx.images-amazon.com/images/I/DEMOASIN04IMAGE.jpg</url>\n                <height units=\"pixels\">500</height>\n                <width units=\"pixels\">393</width>\n            </largeimage>\n            <offers>\n                <totaloffers>0</totaloffers>\n                <totalofferpages>0</totalofferpages>\n                <moreoffersurl>0</moreoffersurl>\n            </offers>\n        </item>\n    </items>\n</itemlookupresponse>\n\"\"\"\n\nproduct_sample_no_audience_rating = \"\"\"\n<?xml version=\"1.0\" ?>\n<itemlookupresponse xmlns=\"http://webservices.amazon.com/AWSECommerceService/2011-08-01\">\n    <items>\n        <request>\n            <isvalid>True</isvalid>\n            <itemlookuprequest>\n                <idtype>ASIN</idtype>\n                <itemid>123456789X</itemid>\n                <responsegroup>Large</responsegroup>\n                <variationpage>All</variationpage>\n            </itemlookuprequest>\n        </request>\n        <item>\n            <asin>123456789X</asin>\n            <itemattributes>\n                <title>A Sample Book</title>\n                <binding>Taschenbuch</binding>\n                <publicationdate>2014-08-18</publicationdate>\n                <releasedate>2014-08-18</releasedate>\n                <ean>9876543219876</ean>\n                <eanlist>\n                    <eanlistelement>9876543219876</eanlistelement>\n                </eanlist>\n                <isbn>123456789X</isbn>\n            </itemattributes>\n            <smallimage>\n                <url>http://ecx.images-amazon.com/images/I/123456789XIMAGE._SL75_.jpg</url>\n                <height units=\"pixels\">75</height>\n                <width units=\"pixels\">59</width>\n            </smallimage>\n            <mediumimage>\n                <url>http://ecx.images-amazon.com/images/I/123456789XIMAGE._SL160_.jpg</url>\n                <height units=\"pixels\">160</height>\n                <width units=\"pixels\">126</width>\n            </mediumimage>\n            <largeimage>\n                <url>http://ecx.images-amazon.com/images/I/123456789XIMAGE.jpg</url>\n                <height units=\"pixels\">500</height>\n                <width units=\"pixels\">393</width>\n            </largeimage>\n            <offers>\n                <totaloffers>1</totaloffers>\n                <totalofferpages>1</totalofferpages>\n                <offer>\n                    <offerattributes>\n                        <condition>New</condition>\n                    </offerattributes>\n                    <offerlisting>\n                        <offerlistingid>SAMPLEOFFERLISTINGID</offerlistingid>\n                        <price>\n                            <amount>1000</amount>\n                            <currencycode>EUR</currencycode>\n                            <formattedprice>EUR 10,00</formattedprice>\n                        </price>\n                        <availability>Gewöhnlich versandfertig in 24 Stunden</availability>\n                        <availabilityattributes>\n                            <availabilitytype>now</availabilitytype>\n                            <minimumhours>0</minimumhours>\n                            <maximumhours>0</maximumhours>\n                        </availabilityattributes>\n                        <iseligibleforsupersavershipping>1</iseligibleforsupersavershipping>\n                    </offerlisting>\n                </offer>\n            </offers>\n        </item>\n    </items>\n</itemlookupresponse>\n\"\"\"\n\nproduct_sample_no_offers = \"\"\"\n<?xml version=\"1.0\" ?>\n<itemlookupresponse xmlns=\"http://webservices.amazon.com/AWSECommerceService/2011-08-01\">\n    <items>\n        <request>\n            <isvalid>True</isvalid>\n            <itemlookuprequest>\n                <idtype>ASIN</idtype>\n                <itemid>DEMOASIN05</itemid>\n                <responsegroup>Large</responsegroup>\n                <variationpage>All</variationpage>\n            </itemlookuprequest>\n        </request>\n        <item>\n            <asin>DEMOASIN05</asin>\n            <itemattributes>\n                <title>Sønderzeichen</title>\n                <binding>Kindle Edition</binding>\n                <publicationdate>2009-10-14</publicationdate>\n                <releasedate>2009-10-14</releasedate>\n            </itemattributes>\n            <smallimage>\n                <url>http://ecx.images-amazon.com/images/I/DEMOASIN05IMAGE._SL75_.jpg</url>\n                <height units=\"pixels\">75</height>\n                <width units=\"pixels\">59</width>\n            </smallimage>\n            <mediumimage>\n                <url>http://ecx.images-amazon.com/images/I/DEMOASIN05IMAGE._SL160_.jpg</url>\n                <height units=\"pixels\">160</height>\n                <width units=\"pixels\">126</width>\n            </mediumimage>\n            <largeimage>\n                <url>http://ecx.images-amazon.com/images/I/DEMOASIN05IMAGE.jpg</url>\n                <height units=\"pixels\">500</height>\n                <width units=\"pixels\">393</width>\n            </largeimage>\n        </item>\n    </items>\n</itemlookupresponse>\n\"\"\"\n\nproduct_sample_10_products = \"\"\"\n<html>\n    <body>\n        <itemlookupresponse xmlns=\"http://webservices.amazon.com/AWSECommerceService/2011-08-01\">\n            <items>\n                <request>\n                    <isvalid>True</isvalid>\n                    <itemlookuprequest>\n                        <idtype>ASIN</idtype>\n                        <itemid>DEMOASIN06</itemid>\n                        <itemid>DEMOASIN07</itemid>\n                        <itemid>DEMOASIN08</itemid>\n                        <itemid>DEMOASIN09</itemid>\n                        <itemid>DEMOASIN10</itemid>\n                        <itemid>DEMOASIN11</itemid>\n                        <itemid>DEMOASIN12</itemid>\n                        <itemid>DEMOASIN13</itemid>\n                        <itemid>DEMOASIN14</itemid>\n                        <itemid>DEMOASIN15</itemid>\n                        <responsegroup>Large</responsegroup>\n                        <variationpage>All</variationpage>\n                    </itemlookuprequest>\n                </request>\n                <item>\n                    <asin>DEMOASIN06</asin>\n                    <smallimage>\n                        <url>http://ecx.images-amazon.com/images/I/DEMOASIN06._SL75_.jpg</url>\n                        <height units=\"pixels\">75</height>\n                        <width units=\"pixels\">53</width>\n                    </smallimage>\n                    <mediumimage>\n                        <url>http://ecx.images-amazon.com/images/I/DEMOASIN06._SL160_.jpg</url>\n                        <height units=\"pixels\">160</height>\n                        <width units=\"pixels\">113</width>\n                    </mediumimage>\n                    <largeimage>\n                        <url>http://ecx.images-amazon.com/images/I/DEMOASIN06.jpg</url>\n                        <height units=\"pixels\">500</height>\n                        <width units=\"pixels\">354</width>\n                    </largeimage>\n                    <itemattributes>\n                        <audiencerating>Freigegeben ab 12 Jahren</audiencerating>\n                        <binding>DVD</binding>\n                        <publicationdate>2012-10-05</publicationdate>\n                        <releasedate>2008-11-07</releasedate>\n                        <title>Hot Shots! - Teil 1 + Teil 2 [2 DVDs]</title>\n                    </itemattributes>\n                    <offers>\n                        <totaloffers>1</totaloffers>\n                        <offer>\n                            <offerlisting>\n                                <price>\n                                    <amount>799</amount>\n                                    <currencycode>EUR</currencycode>\n                                </price>\n                            </offerlisting>\n                        </offer>\n                    </offers>\n                </item>\n                <item>\n                    <asin>DEMOASIN07</asin>\n                    <smallimage>\n                        <url>http://ecx.images-amazon.com/images/I/DEMOASIN07._SL75_.jpg</url>\n                        <height units=\"pixels\">74</height>\n                        <width units=\"pixels\">75</width>\n                    </smallimage>\n                    <mediumimage>\n                        <url>http://ecx.images-amazon.com/images/I/DEMOASIN07._SL160_.jpg</url>\n                        <height units=\"pixels\">159</height>\n                        <width units=\"pixels\">160</width>\n                    </mediumimage>\n                    <largeimage>\n                        <url>http://ecx.images-amazon.com/images/I/DEMOASIN07.jpg</url>\n                        <height units=\"pixels\">496</height>\n                        <width units=\"pixels\">500</width>\n                    </largeimage>\n                    <itemattributes>\n                        <binding>Audio CD</binding>\n                        <releasedate>2004-06-14</releasedate>\n                        <title>Greatest Hits</title>\n                    </itemattributes>\n                    <offers>\n                        <totaloffers>1</totaloffers>\n                        <offer>\n                            <offerlisting>\n                                <price>\n                                    <amount>740</amount>\n                                    <currencycode>EUR</currencycode>\n                                </price>\n                            </offerlisting>\n                        </offer>\n                    </offers>\n                </item>\n                <item>\n                    <asin>DEMOASIN08</asin>\n                    <smallimage>\n                        <url>http://ecx.images-amazon.com/images/I/DEMOASIN08._SL75_.jpg</url>\n                        <height units=\"pixels\">75</height>\n                        <width units=\"pixels\">75</width>\n                    </smallimage>\n                    <mediumimage>\n                        <url>http://ecx.images-amazon.com/images/I/DEMOASIN08._SL160_.jpg</url>\n                        <height units=\"pixels\">160</height>\n                        <width units=\"pixels\">160</width>\n                    </mediumimage>\n                    <largeimage>\n                        <url>http://ecx.images-amazon.com/images/I/DEMOASIN08.jpg</url>\n                        <height units=\"pixels\">500</height>\n                        <width units=\"pixels\">500</width>\n                    </largeimage>\n                    <itemattributes>\n                        <binding>Audio CD</binding>\n                        <publicationdate>2004-07-19</publicationdate>\n                        <releasedate>2004-07-19</releasedate>\n                        <title>Tyrannosaurus Hives</title>\n                    </itemattributes>\n                    <offers>\n                        <totaloffers>1</totaloffers>\n                        <offer>\n                            <offerlisting>\n                                <price>\n                                    <amount>699</amount>\n                                    <currencycode>EUR</currencycode>\n                                </price>\n                            </offerlisting>\n                        </offer>\n                    </offers>\n                </item>\n                <item>\n                    <asin>DEMOASIN09</asin>\n                    <smallimage>\n                        <url>http://ecx.images-amazon.com/images/I/DEMOASIN09._SL75_.jpg</url>\n                        <height units=\"pixels\">75</height>\n                        <width units=\"pixels\">53</width>\n                    </smallimage>\n                    <mediumimage>\n                        <url>http://ecx.images-amazon.com/images/I/DEMOASIN09._SL160_.jpg</url>\n                        <height units=\"pixels\">160</height>\n                        <width units=\"pixels\">113</width>\n                    </mediumimage>\n                    <largeimage>\n                        <url>http://ecx.images-amazon.com/images/I/DEMOASIN09.jpg</url>\n                        <height units=\"pixels\">475</height>\n                        <width units=\"pixels\">336</width>\n                    </largeimage>\n                    <itemattributes>\n                        <audiencerating>Freigegeben ab 6 Jahren</audiencerating>\n                        <binding>DVD</binding>\n                        <releasedate>2004-08-01</releasedate>\n                        <title>Moonlight &amp; Valentino</title>\n                    </itemattributes>\n                    <offers>\n                        <totaloffers>1</totaloffers>\n                        <offer>\n                            <offerlisting>\n                                <price>\n                                    <amount>2297</amount>\n                                    <currencycode>EUR</currencycode>\n                                </price>\n                            </offerlisting>\n                        </offer>\n                    </offers>\n                </item>\n                <item>\n                    <asin>DEMOASIN10</asin>\n                    <smallimage>\n                        <url>http://ecx.images-amazon.com/images/I/DEMOASIN10._SL75_.jpg</url>\n                        <height units=\"pixels\">75</height>\n                        <width units=\"pixels\">56</width>\n                    </smallimage>\n                    <mediumimage>\n                        <url>http://ecx.images-amazon.com/images/I/DEMOASIN10._SL160_.jpg</url>\n                        <height units=\"pixels\">160</height>\n                        <width units=\"pixels\">120</width>\n                    </mediumimage>\n                    <largeimage>\n                        <url>http://ecx.images-amazon.com/images/I/DEMOASIN10.jpg</url>\n                        <height units=\"pixels\">475</height>\n                        <width units=\"pixels\">356</width>\n                    </largeimage>\n                    <itemattributes>\n                        <audiencerating>Freigegeben ab 16 Jahren</audiencerating>\n                        <binding>DVD</binding>\n                        <releasedate>2004-09-07</releasedate>\n                        <title>Jeepers Creepers 1 &amp; 2 [Deluxe Edition] [4 DVDs]</title>\n                    </itemattributes>\n                    <offers>\n                        <totaloffers>1</totaloffers>\n                        <offer>\n                            <offerlisting>\n                                <price>\n                                    <amount>1549</amount>\n                                    <currencycode>EUR</currencycode>\n                                </price>\n                            </offerlisting>\n                        </offer>\n                    </offers>\n                </item>\n                <item>\n                    <asin>DEMOASIN11</asin>\n                    <smallimage>\n                        <url>http://ecx.images-amazon.com/images/I/DEMOASIN11._SL75_.jpg</url>\n                        <height units=\"pixels\">75</height>\n                        <width units=\"pixels\">75</width>\n                    </smallimage>\n                    <mediumimage>\n                        <url>http://ecx.images-amazon.com/images/I/DEMOASIN11._SL160_.jpg</url>\n                        <height units=\"pixels\">160</height>\n                        <width units=\"pixels\">160</width>\n                    </mediumimage>\n                    <largeimage>\n                        <url>http://ecx.images-amazon.com/images/I/DEMOASIN11.jpg</url>\n                        <height units=\"pixels\">500</height>\n                        <width units=\"pixels\">500</width>\n                    </largeimage>\n                    <itemattributes>\n                        <binding>Audio CD</binding>\n                        <publicationdate>2004-10-05</publicationdate>\n                        <releasedate>2004-09-13</releasedate>\n                        <title>Wintersun</title>\n                    </itemattributes>\n                    <offers>\n                        <totaloffers>1</totaloffers>\n                        <offer>\n                            <offerlisting>\n                                <price>\n                                    <amount>2199</amount>\n                                    <currencycode>EUR</currencycode>\n                                </price>\n                            </offerlisting>\n                        </offer>\n                    </offers>\n                </item>\n                <item>\n                    <asin>DEMOASIN12</asin>\n                    <smallimage>\n                        <url>http://ecx.images-amazon.com/images/I/DEMOASIN12._SL75_.jpg</url>\n                        <height units=\"pixels\">75</height>\n                        <width units=\"pixels\">56</width>\n                    </smallimage>\n                    <mediumimage>\n                        <url>http://ecx.images-amazon.com/images/I/DEMOASIN12._SL160_.jpg</url>\n                        <height units=\"pixels\">160</height>\n                        <width units=\"pixels\">120</width>\n                    </mediumimage>\n                    <largeimage>\n                        <url>http://ecx.images-amazon.com/images/I/DEMOASIN12.jpg</url>\n                        <height units=\"pixels\">475</height>\n                        <width units=\"pixels\">356</width>\n                    </largeimage>\n                    <itemattributes>\n                        <audiencerating>Freigegeben ab 16 Jahren</audiencerating>\n                        <binding>DVD</binding>\n                        <publicationdate>2004-11</publicationdate>\n                        <releasedate>2004-10-27</releasedate>\n                        <title>Farscape - Season 1 (8 DVDs)</title>\n                    </itemattributes>\n                    <offers>\n                        <totaloffers>1</totaloffers>\n                        <offer>\n                            <offerlisting>\n                                <price>\n                                    <amount>5999</amount>\n                                    <currencycode>EUR</currencycode>\n                                </price>\n                            </offerlisting>\n                        </offer>\n                    </offers>\n                </item>\n                <item>\n                    <asin>DEMOASIN13</asin>\n                    <smallimage>\n                        <url>http://ecx.images-amazon.com/images/I/DEMOASIN13._SL75_.jpg</url>\n                        <height units=\"pixels\">75</height>\n                        <width units=\"pixels\">75</width>\n                    </smallimage>\n                    <mediumimage>\n                        <url>http://ecx.images-amazon.com/images/I/DEMOASIN13._SL160_.jpg</url>\n                        <height units=\"pixels\">160</height>\n                        <width units=\"pixels\">160</width>\n                    </mediumimage>\n                    <largeimage>\n                        <url>http://ecx.images-amazon.com/images/I/DEMOASIN13.jpg</url>\n                        <height units=\"pixels\">500</height>\n                        <width units=\"pixels\">500</width>\n                    </largeimage>\n                    <itemattributes>\n                        <audiencerating>Freigegeben ohne Altersbeschränkung</audiencerating>\n                        <binding>Audio CD</binding>\n                        <releasedate>2005-01-21</releasedate>\n                        <title>Hurricane Bar</title>\n                    </itemattributes>\n                    <offers>\n                        <totaloffers>1</totaloffers>\n                        <offer>\n                            <offerlisting>\n                                <price>\n                                    <amount>499</amount>\n                                    <currencycode>EUR</currencycode>\n                                </price>\n                            </offerlisting>\n                        </offer>\n                    </offers>\n                </item>\n                <item>\n                    <asin>DEMOASIN14</asin>\n                    <smallimage>\n                        <url>http://ecx.images-amazon.com/images/I/DEMOASIN14._SL75_.jpg</url>\n                        <height units=\"pixels\">75</height>\n                        <width units=\"pixels\">75</width>\n                    </smallimage>\n                    <mediumimage>\n                        <url>http://ecx.images-amazon.com/images/I/DEMOASIN14._SL160_.jpg</url>\n                        <height units=\"pixels\">160</height>\n                        <width units=\"pixels\">160</width>\n                    </mediumimage>\n                    <largeimage>\n                        <url>http://ecx.images-amazon.com/images/I/DEMOASIN14.jpg</url>\n                        <height units=\"pixels\">500</height>\n                        <width units=\"pixels\">500</width>\n                    </largeimage>\n                    <itemattributes>\n                        <binding>Audio CD</binding>\n                        <releasedate>2007-01-02</releasedate>\n                        <title>Silent Alarm</title>\n                    </itemattributes>\n                    <offers>\n                        <totaloffers>1</totaloffers>\n                        <offer>\n                            <offerlisting>\n                                <price>\n                                    <amount>1186</amount>\n                                    <currencycode>EUR</currencycode>\n                                </price>\n                            </offerlisting>\n                        </offer>\n                    </offers>\n                </item>\n                <item>\n                    <asin>DEMOASIN15</asin>\n                    <smallimage>\n                        <url>http://ecx.images-amazon.com/images/I/DEMOASIN15._SL75_.jpg</url>\n                        <height units=\"pixels\">67</height>\n                        <width units=\"pixels\">75</width>\n                    </smallimage>\n                    <mediumimage>\n                        <url>http://ecx.images-amazon.com/images/I/DEMOASIN15._SL160_.jpg</url>\n                        <height units=\"pixels\">144</height>\n                        <width units=\"pixels\">160</width>\n                    </mediumimage>\n                    <largeimage>\n                        <url>http://ecx.images-amazon.com/images/I/DEMOASIN15.jpg</url>\n                        <height units=\"pixels\">449</height>\n                        <width units=\"pixels\">500</width>\n                    </largeimage>\n                    <itemattributes>\n                        <binding>Audio CD</binding>\n                        <publicationdate>2005</publicationdate>\n                        <releasedate>2005-02-07</releasedate>\n                        <title>Greatest Hits</title>\n                    </itemattributes>\n                    <offers>\n                        <totaloffers>1</totaloffers>\n                        <offer>\n                            <offerlisting>\n                                <price>\n                                    <amount>899</amount>\n                                    <currencycode>EUR</currencycode>\n                                </price>\n                            </offerlisting>\n                        </offer>\n                    </offers>\n                </item>\n            </items>\n        </itemlookupresponse>\n    </body>\n</html>\n\"\"\"\n\n\nproduct_sample_3_products = \"\"\"\n<html>\n    <body>\n        <itemlookupresponse xmlns=\"http://webservices.amazon.com/AWSECommerceService/2011-08-01\">\n            <items>\n                <request>\n                    <isvalid>True</isvalid>\n                    <itemlookuprequest>\n                        <idtype>ASIN</idtype>\n                        <itemid>DEMOASIN16</itemid>\n                        <itemid>DEMOASIN17</itemid>\n                        <itemid>DEMOASIN18</itemid>\n                        <responsegroup>Large</responsegroup>\n                        <variationpage>All</variationpage>\n                    </itemlookuprequest>\n                </request>\n                <item>\n                    <asin>DEMOASIN16</asin>\n                    <itemattributes>\n                        <title>Another Demo Series - Season 1 (8 DVDs)</title>\n                        <binding>DVD</binding>\n                        <publicationdate>2004-11</publicationdate>\n                        <releasedate>2004-10-27</releasedate>\n                        <audiencerating>Freigegeben ab 16 Jahren</audiencerating>\n                    </itemattributes>\n                    <smallimage>\n                        <url>http://ecx.images-amazon.com/images/I/DEMOASIN16._SL75_.jpg</url>\n                        <height units=\"pixels\">75</height>\n                        <width units=\"pixels\">59</width>\n                    </smallimage>\n                    <mediumimage>\n                        <url>http://ecx.images-amazon.com/images/I/DEMOASIN16._SL160_.jpg</url>\n                        <height units=\"pixels\">160</height>\n                        <width units=\"pixels\">126</width>\n                    </mediumimage>\n                    <largeimage>\n                        <url>http://ecx.images-amazon.com/images/I/DEMOASIN16.jpg</url>\n                        <height units=\"pixels\">500</height>\n                        <width units=\"pixels\">393</width>\n                    </largeimage>\n                    <offers>\n                        <totaloffers>0</totaloffers>\n                        <totalofferpages>0</totalofferpages>\n                        <moreoffersurl>0</moreoffersurl>\n                    </offers>\n                </item>\n                <item>\n                    <asin>DEMOASIN17</asin>\n                    <itemattributes>\n                        <title>A Sample Book</title>\n                        <binding>Taschenbuch</binding>\n                        <publicationdate>2014-08-18</publicationdate>\n                        <releasedate>2014-08-18</releasedate>\n                        <ean>9876543219876</ean>\n                        <eanlist>\n                            <eanlistelement>9876543219876</eanlistelement>\n                        </eanlist>\n                        <isbn>123456789X</isbn>\n                    </itemattributes>\n                    <smallimage>\n                        <url>http://ecx.images-amazon.com/images/I/DEMOASIN17._SL75_.jpg</url>\n                        <height units=\"pixels\">75</height>\n                        <width units=\"pixels\">59</width>\n                    </smallimage>\n                    <mediumimage>\n                        <url>http://ecx.images-amazon.com/images/I/DEMOASIN17._SL160_.jpg</url>\n                        <height units=\"pixels\">160</height>\n                        <width units=\"pixels\">126</width>\n                    </mediumimage>\n                    <largeimage>\n                        <url>http://ecx.images-amazon.com/images/I/DEMOASIN17.jpg</url>\n                        <height units=\"pixels\">500</height>\n                        <width units=\"pixels\">393</width>\n                    </largeimage>\n                    <offers>\n                        <totaloffers>1</totaloffers>\n                        <totalofferpages>1</totalofferpages>\n                        <offer>\n                            <offerattributes>\n                                <condition>New</condition>\n                            </offerattributes>\n                            <offerlisting>\n                                <offerlistingid>SAMPLEOFFERLISTINGID</offerlistingid>\n                                <price>\n                                    <amount>1000</amount>\n                                    <currencycode>EUR</currencycode>\n                                    <formattedprice>EUR 10,00</formattedprice>\n                                </price>\n                                <availability>Gewöhnlich versandfertig in 24 Stunden</availability>\n                                <availabilityattributes>\n                                    <availabilitytype>now</availabilitytype>\n                                    <minimumhours>0</minimumhours>\n                                    <maximumhours>0</maximumhours>\n                                </availabilityattributes>\n                                <iseligibleforsupersavershipping>1</iseligibleforsupersavershipping>\n                            </offerlisting>\n                        </offer>\n                    </offers>\n                </item>\n                <item>\n                    <asin>DEMOASIN18</asin>\n                    <itemattributes>\n                        <title>Sønderzeichen</title>\n                        <binding>Kindle Edition</binding>\n                        <publicationdate>2009-10-14</publicationdate>\n                        <releasedate>2009-10-14</releasedate>\n                    </itemattributes>\n                    <smallimage>\n                        <url>http://ecx.images-amazon.com/images/I/DEMOASIN18._SL75_.jpg</url>\n                        <height units=\"pixels\">75</height>\n                        <width units=\"pixels\">59</width>\n                    </smallimage>\n                    <mediumimage>\n                        <url>http://ecx.images-amazon.com/images/I/DEMOASIN18._SL160_.jpg</url>\n                        <height units=\"pixels\">160</height>\n                        <width units=\"pixels\">126</width>\n                    </mediumimage>\n                    <largeimage>\n                        <url>http://ecx.images-amazon.com/images/I/DEMOASIN18.jpg</url>\n                        <height units=\"pixels\">500</height>\n                        <width units=\"pixels\">393</width>\n                    </largeimage>\n                </item>\n            </items>\n        </itemlookupresponse>\n    </body>\n</html>\n\"\"\"\n\nproduct_sample_with_artist = \"\"\"\n<itemlookupresponse xmlns=\"http://webservices.amazon.com/AWSECommerceService/2011-08-01\">\n    <items>\n        <request>\n            <isvalid>True</isvalid>\n            <itemlookuprequest>\n                <idtype>ASIN</idtype>\n                <itemid>DEMOASIN19</itemid>\n                <responsegroup>Large</responsegroup>\n                <variationpage>All</variationpage>\n            </itemlookuprequest>\n        </request>\n        <item>\n            <asin>DEMOASIN19</asin>\n            <smallimage>\n                <url>http://ecx.images-amazon.com/images/I/DEMOASIN19._SL75_.jpg</url>\n                <height units=\"pixels\">75</height>\n                <width units=\"pixels\">75</width>\n            </smallimage>\n            <mediumimage>\n                <url>http://ecx.images-amazon.com/images/I/DEMOASIN19._SL160_.jpg</url>\n                <height units=\"pixels\">160</height>\n                <width units=\"pixels\">160</width>\n            </mediumimage>\n            <largeimage>\n                <url>http://ecx.images-amazon.com/images/I/DEMOASIN19.jpg</url>\n                <height units=\"pixels\">500</height>\n                <width units=\"pixels\">500</width>\n            </largeimage>\n            <itemattributes>\n                <artist>The Artist</artist>\n                <binding>Audio CD</binding>\n                <brand>SOME BRAND</brand>\n                <feature>CD Album</feature>\n                <feature>herausgegeben 2015 in Europa von SOME BRAND (SOMEBRAND123)</feature>\n                <feature>Musikstil: Rock / Alternative Rock</feature>\n                <format>CD</format>\n                <itemdimensions>\n                    <height units=\"hundredths-inches\">50</height>\n                    <length units=\"hundredths-inches\">500</length>\n                    <width units=\"hundredths-inches\">550</width>\n                </itemdimensions>\n                <label>SOME BRAND (SOME COMPANY)</label>\n                <languages>\n                    <language>\n                        <name>Englisch</name>\n                        <type>Unbekannt</type>\n                    </language>\n                </languages>\n                <manufacturer>SOME BRAND (SOME COMPANY)</manufacturer>\n                <mpn>SBR1234567</mpn>\n                <numberofdiscs>1</numberofdiscs>\n                <numberofitems>1</numberofitems>\n                <packagedimensions>\n                    <height units=\"hundredths-inches\">31</height>\n                    <length units=\"hundredths-inches\">551</length>\n                    <weight units=\"hundredths-pounds\">18</weight>\n                    <width units=\"hundredths-inches\">512</width>\n                </packagedimensions>\n                <packagequantity>1</packagequantity>\n                <partnumber>SBR1234567</partnumber>\n                <productgroup>Music</productgroup>\n                <producttypename>SOME BRAND (SOME COMPANY)</producttypename>\n                <publicationdate>2015</publicationdate>\n                <publisher>SOME BRAND (SOME COMPANY)</publisher>\n                <releasedate>2015-02-27</releasedate>\n                <studio>SOME BRAND (SOME COMPANY)</studio>\n                <title>The CD Title</title>\n                <upc>1234567879012</upc>\n                <upclist>\n                    <upclistelement>1234567879012</upclistelement>\n                </upclist>\n            </itemattributes>\n            <offersummary>\n                <lowestnewprice>\n                    <amount>747</amount>\n                    <currencycode>EUR</currencycode>\n                    <formattedprice>EUR 7,47</formattedprice>\n                </lowestnewprice>\n                <lowestusedprice>\n                    <amount>901</amount>\n                    <currencycode>EUR</currencycode>\n                    <formattedprice>EUR 9,01</formattedprice>\n                </lowestusedprice>\n                <totalnew>94</totalnew>\n                <totalused>3</totalused>\n                <totalcollectible>0</totalcollectible>\n                <totalrefurbished>0</totalrefurbished>\n            </offersummary>\n            <offers>\n                <totaloffers>1</totaloffers>\n                <totalofferpages>1</totalofferpages>\n                <offer>\n                    <offerattributes>\n                        <condition>New</condition>\n                    </offerattributes>\n                    <offerlisting>\n                        <offerlistingid>SAMPELOFFERLISTINGID</offerlistingid>\n                        <price>\n                            <amount>1299</amount>\n                            <currencycode>EUR</currencycode>\n                            <formattedprice>EUR 12,99</formattedprice>\n                        </price>\n                        <availability>Gewöhnlich versandfertig in 24 Stunden</availability>\n                        <availabilityattributes>\n                            <availabilitytype>now</availabilitytype>\n                            <minimumhours>0</minimumhours>\n                            <maximumhours>0</maximumhours>\n                        </availabilityattributes>\n                        <iseligibleforsupersavershipping>1</iseligibleforsupersavershipping>\n                        <iseligibleforprime>1</iseligibleforprime>\n                    </offerlisting>\n                </offer>\n            </offers>\n            <tracks>\n                <disc number=\"1\">\n                    <track number=\"1\">Title 1</track>\n                    <track number=\"2\">Title 2</track>\n                    <track number=\"3\">Title 3</track>\n                    <track number=\"4\">Title 4</track>\n                    <track number=\"5\">Title 5</track>\n                </disc>\n            </tracks>\n        </item>\n    </items>\n</itemlookupresponse>\n\"\"\"\n\n# a product with an ISBN-13 in the ISBN-10 field, see issue https://github.com/ponyriders/django-amazon-price-monitor/issues/121\nproduct_sample_isbn_issue = \"\"\"\n<itemlookupresponse xmlns=\"http://webservices.amazon.com/AWSECommerceService/2013-08-01\">\n    <items>\n        <request>\n            <isvalid>True</isvalid>\n            <itemlookuprequest>\n                <idtype>ASIN</idtype>\n                <itemid>TESTASIN20</itemid>\n                <responsegroup>Large</responsegroup>\n                <variationpage>All</variationpage>\n            </itemlookuprequest>\n        </request>\n        <item>\n            <asin>TESTASIN20</asin>\n            <salesrank>2</salesrank>\n            <smallimage>\n                <url>http://ecx.images-amazon.com/images/I/TESTASIN20._SL75_.jpg</url>\n                <height units=\"pixels\">75</height>\n                <width units=\"pixels\">75</width>\n            </smallimage>\n            <mediumimage>\n                <url>http://ecx.images-amazon.com/images/I/TESTASIN20._SL160_.jpg</url>\n                <height units=\"pixels\">160</height>\n                <width units=\"pixels\">160</width>\n            </mediumimage>\n            <largeimage>\n                <url>http://ecx.images-amazon.com/images/I/TESTASIN20.jpg</url>\n                <height units=\"pixels\">500</height>\n                <width units=\"pixels\">500</width>\n            </largeimage>\n            <itemattributes>\n                <actor>John Doe</actor>\n                <actor>Jane Doe</actor>\n                <aspectratio>2.39:1</aspectratio>\n                <audiencerating>Freigegeben ab 16 Jahren</audiencerating>\n                <binding>Blu-ray</binding>\n                <brand>Test Distribution Company</brand>\n                <creator role=\"Hauptdarsteller\">John Doe</creator>\n                <creator role=\"Hauptdarsteller\">Jane Doe</creator>\n                <director>John Director</director>\n                <ean>1234567890123</ean>\n                <eanlist>\n                    <eanlistelement>1234567890123</eanlistelement>\n                </eanlist>\n                <edition>Standard Edition</edition>\n                <format>Letterboxed</format>\n                <format>Widescreen</format>\n                <isbn>1234567890123</isbn>\n                <label>Test Label Company</label>\n                <manufacturer>Test Manufacturer Company</manufacturer>\n                <mpn>12345678</mpn>\n                <numberofdiscs>2</numberofdiscs>\n                <numberofitems>2</numberofitems>\n                <packagedimensions>\n                    <height units=\"Hundertstel Zoll\">39</height>\n                    <length units=\"Hundertstel Zoll\">669</length>\n                    <weight units=\"Hundertstel Pfund\">18</weight>\n                    <width units=\"Hundertstel Zoll\">528</width>\n                </packagedimensions>\n                <partnumber>12345678</partnumber>\n                <productgroup>DVD &amp; Blu-ray</productgroup>\n                <producttypename>ABIS_BOOK</producttypename>\n                <publicationdate>2018-09-27</publicationdate>\n                <publisher>Test Publisher Company</publisher>\n                <regioncode>2</regioncode>\n                <releasedate>2018-09-27</releasedate>\n                <runningtime units=\"Minuten\">119</runningtime>\n                <studio>Test Studio Company</studio>\n                <title>The ISDN Test Product</title>\n            </itemattributes>\n        </item>\n    </items>\n</itemlookupresponse>\n\"\"\"\n"
  },
  {
    "path": "tests/product_advertising_api/test_api.py",
    "content": "import datetime\n\nfrom .data import (\n    product_sample_3_products,\n    product_sample_10_products,\n    product_sample_isbn_issue,\n    product_sample_lookup_fail,\n    product_sample_no_audience_rating,\n    product_sample_no_images,\n    product_sample_no_item,\n    product_sample_no_offers,\n    product_sample_no_price,\n    product_sample_ok,\n    product_sample_with_artist,\n)\n\nfrom bs4 import BeautifulSoup\n\nfrom django.test import TestCase\n\nfrom unittest.mock import patch\n\nfrom price_monitor.product_advertising_api.api import ProductAdvertisingAPI\n\nfrom testfixtures import log_capture\n\n\nclass ProductAdvertisingAPITest(TestCase):\n    \"\"\"\n    Test class for the ProductAdvertisingAPI.\n    \"\"\"\n\n    def __get_product_bs(self, xml):\n        \"\"\"\n        Wraps the given xml string into BeautifulSoup just as bottlenose does.\n        :param xml: the xml to use\n        :return: searchable bs object\n        \"\"\"\n        return BeautifulSoup(xml, 'lxml')\n\n    @patch.object(ProductAdvertisingAPI, 'lookup_at_amazon')\n    @patch.object(ProductAdvertisingAPI, '__init__')\n    @log_capture()\n    def test_item_lookup_response_fail(self, papi_init, papi_lookup, lc):\n        \"\"\"\n        Test for a product whose amazon query returns nothing.\n        :param papi_init: mockup for ProductAdvertisingAPI.__init__\n        :type papi_init: unittest.mock.MagicMock\n        :param papi_lookup: mockup for ProductAdvertisingAPI.lookup_at_amazon\n        :type papi_init: unittest.mock.MagicMock\n        :param lc: log capture instance\n        :type lc: testfixtures.logcapture.LogCaptureForDecorator\n        \"\"\"\n        # mock the return value of amazon call\n        papi_init.return_value = None\n        papi_lookup.return_value = self.__get_product_bs('')\n\n        api = ProductAdvertisingAPI()\n        self.assertEqual({}, api.item_lookup(['ASIN-DUMMY']))\n\n        # check log output\n        lc.check(\n            ('price_monitor.product_advertising_api', 'INFO', 'starting lookup for ASINs ASIN-DUMMY'),\n            ('price_monitor.product_advertising_api', 'ERROR', 'Request for item lookup (ResponseGroup: Large, ASINs: ASIN-DUMMY) returned nothing')\n        )\n\n    @patch.object(ProductAdvertisingAPI, 'lookup_at_amazon')\n    @patch.object(ProductAdvertisingAPI, '__init__')\n    @log_capture()\n    def test_item_lookup_fail(self, papi_init, papi_lookup, lc):\n        \"\"\"\n        Test for a product which failed at the amazon endpoint.\n        :param papi_init: mockup for ProductAdvertisingAPI.__init__\n        :type papi_init: unittest.mock.MagicMock\n        :param papi_lookup: mockup for ProductAdvertisingAPI.lookup_at_amazon\n        :type papi_init: unittest.mock.MagicMock\n        :param lc: log capture instance\n        :type lc: testfixtures.logcapture.LogCaptureForDecorator\n        \"\"\"\n        papi_init.return_value = None\n        papi_lookup.return_value = self.__get_product_bs(product_sample_lookup_fail)\n\n        api = ProductAdvertisingAPI()\n        self.assertEqual({}, api.item_lookup(['DEMOASIN01']))\n\n        # check log output\n        lc.check(\n            ('price_monitor.product_advertising_api', 'INFO', 'starting lookup for ASINs DEMOASIN01'),\n            ('price_monitor.product_advertising_api', 'ERROR', 'Request for item lookup (ResponseGroup: Large, ASINs: DEMOASIN01) was not valid')\n        )\n\n    @patch.object(ProductAdvertisingAPI, 'lookup_at_amazon')\n    @patch.object(ProductAdvertisingAPI, '__init__')\n    @log_capture()\n    def test_item_lookup_no_item(self, papi_init, papi_lookup, lc):\n        \"\"\"\n        Test for a product with no returned items.\n        :param papi_init: mockup for ProductAdvertisingAPI.__init__\n        :type papi_init: unittest.mock.MagicMock\n        :param papi_lookup: mockup for ProductAdvertisingAPI.lookup_at_amazon\n        :type papi_init: unittest.mock.MagicMock\n        :param lc: log capture instance\n        :type lc: testfixtures.logcapture.LogCaptureForDecorator\n        \"\"\"\n        papi_init.return_value = None\n        papi_lookup.return_value = self.__get_product_bs(product_sample_no_item)\n\n        api = ProductAdvertisingAPI()\n        self.assertEqual({}, api.item_lookup(['DEMOASIN02']))\n\n        # check log output\n        lc.check(\n            ('price_monitor.product_advertising_api', 'INFO', 'starting lookup for ASINs DEMOASIN02'),\n            ('price_monitor.product_advertising_api', 'ERROR', 'Lookup for the following ASINs failed: DEMOASIN02')\n        )\n\n    @patch.object(ProductAdvertisingAPI, 'lookup_at_amazon')\n    @patch.object(ProductAdvertisingAPI, '__init__')\n    @log_capture()\n    def test_item_lookup_normal(self, product_api_init, product_api_lookup, lc):\n        \"\"\"\n        Test for a normal bluray.\n        :param product_api_init: mockup for ProductAdvertisingAPI.__init__\n        :type product_api_init: unittest.mock.MagicMock\n        :param product_api_lookup: mockup for ProductAdvertisingAPI.lookup_at_amazon\n        :type product_api_lookup: unittest.mock.MagicMock\n        :param lc: log capture instance\n        :type lc: testfixtures.logcapture.LogCaptureForDecorator\n        \"\"\"\n        product_api_init.return_value = None\n        product_api_lookup.return_value = self.__get_product_bs(product_sample_ok)\n\n        api = ProductAdvertisingAPI()\n        values = api.item_lookup(['DEMOASIN03'])\n\n        # ensure the mocks were called\n        self.assertTrue(product_api_init.called)\n        self.assertTrue(product_api_lookup.called)\n\n        self.assertNotEqual(None, values)\n        self.assertEqual(type(dict()), type(values))\n        self.assertEqual(len(values), 1)\n\n        self.assertTrue('DEMOASIN03' in values)\n        self.assertEqual('DEMOASIN03', values['DEMOASIN03']['asin'])\n        self.assertEqual('Demo Series - Season 2 [Blu-ray]', values['DEMOASIN03']['title'])\n        self.assertEqual(None, values['DEMOASIN03']['isbn'])\n        self.assertEqual(None, values['DEMOASIN03']['eisbn'])\n        self.assertEqual('Blu-ray', values['DEMOASIN03']['binding'])\n        self.assertEqual(datetime.datetime(2014, 12, 22), values['DEMOASIN03']['date_publication'])\n        self.assertEqual(datetime.datetime(2014, 12, 22), values['DEMOASIN03']['date_release'])\n        self.assertEqual('http://ecx.images-amazon.com/images/I/DEMOASIN03IMAGE.jpg', values['DEMOASIN03']['large_image_url'])\n        self.assertEqual('http://ecx.images-amazon.com/images/I/DEMOASIN03IMAGE._SL160_.jpg', values['DEMOASIN03']['medium_image_url'])\n        self.assertEqual('http://ecx.images-amazon.com/images/I/DEMOASIN03IMAGE._SL75_.jpg', values['DEMOASIN03']['small_image_url'])\n        self.assertEqual('http://www.amazon.de/dp/DEMOASIN03/?tag=sample-assoc-tag', values['DEMOASIN03']['offer_url'])\n        self.assertEqual('Freigegeben ab 16 Jahren', values['DEMOASIN03']['audience_rating'])\n        self.assertEqual(17.99, values['DEMOASIN03']['price'])\n        self.assertEqual('EUR', values['DEMOASIN03']['currency'])\n\n        # check log output\n        lc.check(\n            ('price_monitor.product_advertising_api', 'INFO', 'starting lookup for ASINs DEMOASIN03')\n        )\n\n    @patch.object(ProductAdvertisingAPI, 'lookup_at_amazon')\n    @patch.object(ProductAdvertisingAPI, '__init__')\n    @log_capture()\n    def test_item_lookup_no_price(self, product_api_init, product_api_lookup, lc):\n        \"\"\"\n        Test for a dvd without a price. Happens mostly for box sets.\n        :param product_api_init: mockup for ProductAdvertisingAPI.__init__\n        :type product_api_init: unittest.mock.MagicMock\n        :param product_api_lookup: mockup for ProductAdvertisingAPI.lookup_at_amazon\n        :type product_api_lookup: unittest.mock.MagicMock\n        :param lc: log capture instance\n        :type lc: testfixtures.logcapture.LogCaptureForDecorator\n        \"\"\"\n        product_api_init.return_value = None\n        product_api_lookup.return_value = self.__get_product_bs(product_sample_no_price)\n\n        api = ProductAdvertisingAPI()\n        values = api.item_lookup(['DEMOASIN04'])\n\n        # ensure the mocks were called\n        self.assertTrue(product_api_init.called)\n        self.assertTrue(product_api_lookup.called)\n\n        self.assertNotEqual(None, values)\n        self.assertEqual(type(dict()), type(values))\n        self.assertEqual(len(values), 1)\n\n        self.assertTrue('DEMOASIN04' in values)\n        self.assertEqual('DEMOASIN04', values['DEMOASIN04']['asin'])\n        self.assertEqual('Another Demo Series - Season 1 (8 DVDs)', values['DEMOASIN04']['title'])\n        self.assertEqual(None, values['DEMOASIN04']['isbn'])\n        self.assertEqual(None, values['DEMOASIN04']['eisbn'])\n        self.assertEqual('DVD', values['DEMOASIN04']['binding'])\n        # dateutil.parser.parse will find out the year and month of \"2004-11\" but as there is no day, the day is set to the current day\n        self.assertEqual(\n            datetime.datetime(2004, 11, datetime.datetime.now().day if datetime.datetime.now().day < 31 else 30),\n            values['DEMOASIN04']['date_publication']\n        )\n        self.assertEqual(datetime.datetime(2004, 10, 27), values['DEMOASIN04']['date_release'])\n        self.assertEqual('http://ecx.images-amazon.com/images/I/DEMOASIN04IMAGE.jpg', values['DEMOASIN04']['large_image_url'])\n        self.assertEqual('http://ecx.images-amazon.com/images/I/DEMOASIN04IMAGE._SL160_.jpg', values['DEMOASIN04']['medium_image_url'])\n        self.assertEqual('http://ecx.images-amazon.com/images/I/DEMOASIN04IMAGE._SL75_.jpg', values['DEMOASIN04']['small_image_url'])\n        self.assertEqual('http://www.amazon.de/dp/DEMOASIN04/?tag=sample-assoc-tag', values['DEMOASIN04']['offer_url'])\n        self.assertEqual('Freigegeben ab 16 Jahren', values['DEMOASIN04']['audience_rating'])\n        self.assertRaises(KeyError, lambda: values['DEMOASIN04']['price'])\n        self.assertRaises(KeyError, lambda: values['DEMOASIN04']['currency'])\n\n        # check log output\n        lc.check(\n            ('price_monitor.product_advertising_api', 'INFO', 'starting lookup for ASINs DEMOASIN04')\n        )\n\n    @patch.object(ProductAdvertisingAPI, 'lookup_at_amazon')\n    @patch.object(ProductAdvertisingAPI, '__init__')\n    @log_capture()\n    def test_item_lookup_no_audience_rating_isbn(self, product_api_init, product_api_lookup, lc):\n        \"\"\"\n        Test for a book without audience rating.\n        :param product_api_init: mockup for ProductAdvertisingAPI.__init__\n        :type product_api_init: unittest.mock.MagicMock\n        :param product_api_lookup: mockup for ProductAdvertisingAPI.lookup_at_amazon\n        :type product_api_lookup: unittest.mock.MagicMock\n        :param lc: log capture instance\n        :type lc: testfixtures.logcapture.LogCaptureForDecorator\n        \"\"\"\n        product_api_init.return_value = None\n        product_api_lookup.return_value = self.__get_product_bs(product_sample_no_audience_rating)\n\n        api = ProductAdvertisingAPI()\n        values = api.item_lookup(['123456789X'])\n\n        # ensure the mocks were called\n        # ensure the mocks were called\n        self.assertTrue(product_api_init.called)\n        self.assertTrue(product_api_lookup.called)\n\n        self.assertNotEqual(None, values)\n        self.assertEqual(type(dict()), type(values))\n        self.assertEqual(len(values), 1)\n\n        self.assertTrue('123456789X' in values)\n        self.assertEqual('123456789X', values['123456789X']['asin'])\n        self.assertEqual('A Sample Book', values['123456789X']['title'])\n        self.assertEqual('123456789X', values['123456789X']['isbn'])\n        self.assertEqual(None, values['123456789X']['eisbn'])\n        self.assertEqual('Taschenbuch', values['123456789X']['binding'])\n        self.assertEqual(datetime.datetime(2014, 8, 18), values['123456789X']['date_publication'])\n        self.assertEqual(datetime.datetime(2014, 8, 18), values['123456789X']['date_release'])\n        self.assertEqual('http://ecx.images-amazon.com/images/I/123456789XIMAGE.jpg', values['123456789X']['large_image_url'])\n        self.assertEqual('http://ecx.images-amazon.com/images/I/123456789XIMAGE._SL160_.jpg', values['123456789X']['medium_image_url'])\n        self.assertEqual('http://ecx.images-amazon.com/images/I/123456789XIMAGE._SL75_.jpg', values['123456789X']['small_image_url'])\n        self.assertEqual('http://www.amazon.de/dp/123456789X/?tag=sample-assoc-tag', values['123456789X']['offer_url'])\n        self.assertEqual(10.0, values['123456789X']['price'])\n        self.assertEqual('EUR', values['123456789X']['currency'])\n        self.assertIsNone(values['123456789X']['audience_rating'])\n\n        # check log output\n        lc.check(\n            ('price_monitor.product_advertising_api', 'INFO', 'starting lookup for ASINs 123456789X')\n        )\n\n    @patch.object(ProductAdvertisingAPI, 'lookup_at_amazon')\n    @patch.object(ProductAdvertisingAPI, '__init__')\n    @log_capture()\n    def test_item_lookup_no_offers(self, product_api_init, product_api_lookup, lc):\n        \"\"\"\n        Test for a book without offers.\n        :param product_api_init: mockup for ProductAdvertisingAPI.__init__\n        :type product_api_init: unittest.mock.MagicMock\n        :param product_api_lookup: mockup for ProductAdvertisingAPI.lookup_at_amazon\n        :type product_api_lookup: unittest.mock.MagicMock\n        :param lc: log capture instance\n        :type lc: testfixtures.logcapture.LogCaptureForDecorator\n        \"\"\"\n        product_api_init.return_value = None\n        product_api_lookup.return_value = self.__get_product_bs(product_sample_no_offers)\n\n        api = ProductAdvertisingAPI()\n        values = api.item_lookup(['DEMOASIN05'])\n\n        # ensure the mocks were called\n        self.assertTrue(product_api_init.called)\n        self.assertTrue(product_api_lookup.called)\n\n        self.assertNotEqual(None, values)\n        self.assertEqual(type(dict()), type(values))\n        self.assertEqual(len(values), 1)\n\n        self.assertTrue('DEMOASIN05' in values)\n        self.assertEqual('DEMOASIN05', values['DEMOASIN05']['asin'])\n        self.assertEqual('Sønderzeichen', values['DEMOASIN05']['title'])\n        self.assertEqual(None, values['DEMOASIN05']['isbn'])\n        self.assertEqual(None, values['DEMOASIN05']['eisbn'])\n        self.assertEqual('Kindle Edition', values['DEMOASIN05']['binding'])\n        self.assertEqual(datetime.datetime(2009, 10, 14), values['DEMOASIN05']['date_publication'])\n        self.assertEqual(datetime.datetime(2009, 10, 14), values['DEMOASIN05']['date_release'])\n        self.assertEqual('http://ecx.images-amazon.com/images/I/DEMOASIN05IMAGE.jpg', values['DEMOASIN05']['large_image_url'])\n        self.assertEqual('http://ecx.images-amazon.com/images/I/DEMOASIN05IMAGE._SL160_.jpg', values['DEMOASIN05']['medium_image_url'])\n        self.assertEqual('http://ecx.images-amazon.com/images/I/DEMOASIN05IMAGE._SL75_.jpg', values['DEMOASIN05']['small_image_url'])\n        self.assertEqual('http://www.amazon.de/dp/DEMOASIN05/?tag=sample-assoc-tag', values['DEMOASIN05']['offer_url'])\n        self.assertRaises(KeyError, lambda: values['DEMOASIN05']['price'])\n        self.assertRaises(KeyError, lambda: values['DEMOASIN05']['currency'])\n        self.assertIsNone(values['DEMOASIN05']['audience_rating'])\n\n        # check log output\n        lc.check(\n            ('price_monitor.product_advertising_api', 'INFO', 'starting lookup for ASINs DEMOASIN05')\n        )\n\n    @patch.object(ProductAdvertisingAPI, 'lookup_at_amazon')\n    @patch.object(ProductAdvertisingAPI, '__init__')\n    @log_capture()\n    def test_item_lookup_10_products(self, product_api_init, product_api_lookup, lc):\n        \"\"\"\n        Tests for parsing 10 products.\n        :param product_api_init: mockup for ProductAdvertisingAPI.__init__\n        :type product_api_init: unittest.mock.MagicMock\n        :param product_api_lookup: mockup for ProductAdvertisingAPI.lookup_at_amazon\n        :type product_api_lookup: unittest.mock.MagicMock\n        :param lc: log capture instance\n        :type lc: testfixtures.logcapture.LogCaptureForDecorator\n        \"\"\"\n        product_api_init.return_value = None\n        product_api_lookup.return_value = self.__get_product_bs(product_sample_10_products)\n\n        api = ProductAdvertisingAPI()\n        values = api.item_lookup([\n            'DEMOASIN06',\n            'DEMOASIN07',\n            'DEMOASIN08',\n            'DEMOASIN09',\n            'DEMOASIN10',\n            'DEMOASIN11',\n            'DEMOASIN12',\n            'DEMOASIN13',\n            'DEMOASIN14',\n            'DEMOASIN15',\n        ])\n\n        # ensure the mocks were called\n        self.assertTrue(product_api_init.called)\n        self.assertTrue(product_api_lookup.called)\n\n        self.assertNotEqual(None, values)\n        self.assertEqual(type(dict()), type(values))\n        self.assertEqual(len(values), 10)\n\n        # ASIN DEMOASIN06\n        self.assertTrue('DEMOASIN06' in values)\n        self.assertEqual('DEMOASIN06', values['DEMOASIN06']['asin'])\n        self.assertEqual('Hot Shots! - Teil 1 + Teil 2 [2 DVDs]', values['DEMOASIN06']['title'])\n        self.assertEqual(None, values['DEMOASIN06']['isbn'])\n        self.assertEqual(None, values['DEMOASIN06']['eisbn'])\n        self.assertEqual('DVD', values['DEMOASIN06']['binding'])\n        self.assertEqual(datetime.datetime(2012, 10, 5), values['DEMOASIN06']['date_publication'])\n        self.assertEqual(datetime.datetime(2008, 11, 7), values['DEMOASIN06']['date_release'])\n        self.assertEqual('http://ecx.images-amazon.com/images/I/DEMOASIN06.jpg', values['DEMOASIN06']['large_image_url'])\n        self.assertEqual('http://ecx.images-amazon.com/images/I/DEMOASIN06._SL160_.jpg', values['DEMOASIN06']['medium_image_url'])\n        self.assertEqual('http://ecx.images-amazon.com/images/I/DEMOASIN06._SL75_.jpg', values['DEMOASIN06']['small_image_url'])\n        self.assertEqual('http://www.amazon.de/dp/DEMOASIN06/?tag=sample-assoc-tag', values['DEMOASIN06']['offer_url'])\n        self.assertEqual('Freigegeben ab 12 Jahren', values['DEMOASIN06']['audience_rating'])\n        self.assertEqual(7.99, values['DEMOASIN06']['price'])\n        self.assertEqual('EUR', values['DEMOASIN06']['currency'])\n\n        # ASIN DEMOASIN07\n        self.assertTrue('DEMOASIN07' in values)\n        self.assertEqual('DEMOASIN07', values['DEMOASIN07']['asin'])\n        self.assertEqual('Greatest Hits', values['DEMOASIN07']['title'])\n        self.assertEqual(None, values['DEMOASIN07']['isbn'])\n        self.assertEqual(None, values['DEMOASIN07']['eisbn'])\n        self.assertEqual('Audio CD', values['DEMOASIN07']['binding'])\n        self.assertEqual(None, values['DEMOASIN07']['date_publication'])\n        self.assertEqual(datetime.datetime(2004, 6, 14), values['DEMOASIN07']['date_release'])\n        self.assertEqual('http://ecx.images-amazon.com/images/I/DEMOASIN07.jpg', values['DEMOASIN07']['large_image_url'])\n        self.assertEqual('http://ecx.images-amazon.com/images/I/DEMOASIN07._SL160_.jpg', values['DEMOASIN07']['medium_image_url'])\n        self.assertEqual('http://ecx.images-amazon.com/images/I/DEMOASIN07._SL75_.jpg', values['DEMOASIN07']['small_image_url'])\n        self.assertEqual('http://www.amazon.de/dp/DEMOASIN07/?tag=sample-assoc-tag', values['DEMOASIN07']['offer_url'])\n        self.assertIsNone(values['DEMOASIN07']['audience_rating'])\n        self.assertEqual(7.40, values['DEMOASIN07']['price'])\n        self.assertEqual('EUR', values['DEMOASIN07']['currency'])\n\n        # ASIN DEMOASIN08\n        self.assertTrue('DEMOASIN08' in values)\n        self.assertEqual('DEMOASIN08', values['DEMOASIN08']['asin'])\n        self.assertEqual('Tyrannosaurus Hives', values['DEMOASIN08']['title'])\n        self.assertEqual(None, values['DEMOASIN08']['isbn'])\n        self.assertEqual(None, values['DEMOASIN08']['eisbn'])\n        self.assertEqual('Audio CD', values['DEMOASIN08']['binding'])\n        self.assertEqual(datetime.datetime(2004, 7, 19), values['DEMOASIN08']['date_publication'])\n        self.assertEqual(datetime.datetime(2004, 7, 19), values['DEMOASIN08']['date_release'])\n        self.assertEqual('http://ecx.images-amazon.com/images/I/DEMOASIN08.jpg', values['DEMOASIN08']['large_image_url'])\n        self.assertEqual('http://ecx.images-amazon.com/images/I/DEMOASIN08._SL160_.jpg', values['DEMOASIN08']['medium_image_url'])\n        self.assertEqual('http://ecx.images-amazon.com/images/I/DEMOASIN08._SL75_.jpg', values['DEMOASIN08']['small_image_url'])\n        self.assertEqual('http://www.amazon.de/dp/DEMOASIN08/?tag=sample-assoc-tag', values['DEMOASIN08']['offer_url'])\n        self.assertIsNone(values['DEMOASIN08']['audience_rating'])\n        self.assertEqual(6.99, values['DEMOASIN08']['price'])\n        self.assertEqual('EUR', values['DEMOASIN08']['currency'])\n\n        # ASIN DEMOASIN09\n        self.assertTrue('DEMOASIN09' in values)\n        self.assertEqual('DEMOASIN09', values['DEMOASIN09']['asin'])\n        self.assertEqual('Moonlight & Valentino', values['DEMOASIN09']['title'])\n        self.assertEqual(None, values['DEMOASIN09']['isbn'])\n        self.assertEqual(None, values['DEMOASIN09']['eisbn'])\n        self.assertEqual('DVD', values['DEMOASIN09']['binding'])\n        self.assertEqual(None, values['DEMOASIN09']['date_publication'])\n        self.assertEqual(datetime.datetime(2004, 8, 1), values['DEMOASIN09']['date_release'])\n        self.assertEqual('http://ecx.images-amazon.com/images/I/DEMOASIN09.jpg', values['DEMOASIN09']['large_image_url'])\n        self.assertEqual('http://ecx.images-amazon.com/images/I/DEMOASIN09._SL160_.jpg', values['DEMOASIN09']['medium_image_url'])\n        self.assertEqual('http://ecx.images-amazon.com/images/I/DEMOASIN09._SL75_.jpg', values['DEMOASIN09']['small_image_url'])\n        self.assertEqual('http://www.amazon.de/dp/DEMOASIN09/?tag=sample-assoc-tag', values['DEMOASIN09']['offer_url'])\n        self.assertEqual('Freigegeben ab 6 Jahren', values['DEMOASIN09']['audience_rating'])\n        self.assertEqual(22.97, values['DEMOASIN09']['price'])\n        self.assertEqual('EUR', values['DEMOASIN09']['currency'])\n\n        # ASIN DEMOASIN10\n        self.assertTrue('DEMOASIN10' in values)\n        self.assertEqual('DEMOASIN10', values['DEMOASIN10']['asin'])\n        self.assertEqual('Jeepers Creepers 1 & 2 [Deluxe Edition] [4 DVDs]', values['DEMOASIN10']['title'])\n        self.assertEqual(None, values['DEMOASIN10']['isbn'])\n        self.assertEqual(None, values['DEMOASIN10']['eisbn'])\n        self.assertEqual('DVD', values['DEMOASIN10']['binding'])\n        self.assertEqual(None, values['DEMOASIN10']['date_publication'])\n        self.assertEqual(datetime.datetime(2004, 9, 7), values['DEMOASIN10']['date_release'])\n        self.assertEqual('http://ecx.images-amazon.com/images/I/DEMOASIN10.jpg', values['DEMOASIN10']['large_image_url'])\n        self.assertEqual('http://ecx.images-amazon.com/images/I/DEMOASIN10._SL160_.jpg', values['DEMOASIN10']['medium_image_url'])\n        self.assertEqual('http://ecx.images-amazon.com/images/I/DEMOASIN10._SL75_.jpg', values['DEMOASIN10']['small_image_url'])\n        self.assertEqual('http://www.amazon.de/dp/DEMOASIN10/?tag=sample-assoc-tag', values['DEMOASIN10']['offer_url'])\n        self.assertEqual('Freigegeben ab 16 Jahren', values['DEMOASIN10']['audience_rating'])\n        self.assertEqual(15.49, values['DEMOASIN10']['price'])\n        self.assertEqual('EUR', values['DEMOASIN10']['currency'])\n\n        # ASIN DEMOASIN11\n        self.assertTrue('DEMOASIN11' in values)\n        self.assertEqual('DEMOASIN11', values['DEMOASIN11']['asin'])\n        self.assertEqual('Wintersun', values['DEMOASIN11']['title'])\n        self.assertEqual(None, values['DEMOASIN11']['isbn'])\n        self.assertEqual(None, values['DEMOASIN11']['eisbn'])\n        self.assertEqual('Audio CD', values['DEMOASIN11']['binding'])\n        self.assertEqual(datetime.datetime(2004, 10, 5), values['DEMOASIN11']['date_publication'])\n        self.assertEqual(datetime.datetime(2004, 9, 13), values['DEMOASIN11']['date_release'])\n        self.assertEqual('http://ecx.images-amazon.com/images/I/DEMOASIN11.jpg', values['DEMOASIN11']['large_image_url'])\n        self.assertEqual('http://ecx.images-amazon.com/images/I/DEMOASIN11._SL160_.jpg', values['DEMOASIN11']['medium_image_url'])\n        self.assertEqual('http://ecx.images-amazon.com/images/I/DEMOASIN11._SL75_.jpg', values['DEMOASIN11']['small_image_url'])\n        self.assertEqual('http://www.amazon.de/dp/DEMOASIN11/?tag=sample-assoc-tag', values['DEMOASIN11']['offer_url'])\n        self.assertIsNone(values['DEMOASIN11']['audience_rating'])\n        self.assertEqual(21.99, values['DEMOASIN11']['price'])\n        self.assertEqual('EUR', values['DEMOASIN11']['currency'])\n\n        # ASIN DEMOASIN12\n        self.assertTrue('DEMOASIN12' in values)\n        self.assertEqual('DEMOASIN12', values['DEMOASIN12']['asin'])\n        self.assertEqual('Farscape - Season 1 (8 DVDs)', values['DEMOASIN12']['title'])\n        self.assertEqual(None, values['DEMOASIN12']['isbn'])\n        self.assertEqual(None, values['DEMOASIN12']['eisbn'])\n        self.assertEqual('DVD', values['DEMOASIN12']['binding'])\n        self.assertEqual(\n            datetime.datetime(2004, 11, datetime.datetime.now().day if datetime.datetime.now().day < 31 else 30),\n            values['DEMOASIN12']['date_publication']\n        )\n        self.assertEqual(datetime.datetime(2004, 10, 27), values['DEMOASIN12']['date_release'])\n        self.assertEqual('http://ecx.images-amazon.com/images/I/DEMOASIN12.jpg', values['DEMOASIN12']['large_image_url'])\n        self.assertEqual('http://ecx.images-amazon.com/images/I/DEMOASIN12._SL160_.jpg', values['DEMOASIN12']['medium_image_url'])\n        self.assertEqual('http://ecx.images-amazon.com/images/I/DEMOASIN12._SL75_.jpg', values['DEMOASIN12']['small_image_url'])\n        self.assertEqual('http://www.amazon.de/dp/DEMOASIN12/?tag=sample-assoc-tag', values['DEMOASIN12']['offer_url'])\n        self.assertEqual('Freigegeben ab 16 Jahren', values['DEMOASIN12']['audience_rating'])\n        self.assertEqual(59.99, values['DEMOASIN12']['price'])\n        self.assertEqual('EUR', values['DEMOASIN12']['currency'])\n\n        # ASIN DEMOASIN13\n        self.assertTrue('DEMOASIN13' in values)\n        self.assertEqual('DEMOASIN13', values['DEMOASIN13']['asin'])\n        self.assertEqual('Hurricane Bar', values['DEMOASIN13']['title'])\n        self.assertEqual(None, values['DEMOASIN13']['isbn'])\n        self.assertEqual(None, values['DEMOASIN13']['eisbn'])\n        self.assertEqual('Audio CD', values['DEMOASIN13']['binding'])\n        self.assertEqual(None, values['DEMOASIN13']['date_publication'])\n        self.assertEqual(datetime.datetime(2005, 1, 21), values['DEMOASIN13']['date_release'])\n        self.assertEqual('http://ecx.images-amazon.com/images/I/DEMOASIN13.jpg', values['DEMOASIN13']['large_image_url'])\n        self.assertEqual('http://ecx.images-amazon.com/images/I/DEMOASIN13._SL160_.jpg', values['DEMOASIN13']['medium_image_url'])\n        self.assertEqual('http://ecx.images-amazon.com/images/I/DEMOASIN13._SL75_.jpg', values['DEMOASIN13']['small_image_url'])\n        self.assertEqual('http://www.amazon.de/dp/DEMOASIN13/?tag=sample-assoc-tag', values['DEMOASIN13']['offer_url'])\n        self.assertEqual('Freigegeben ohne Altersbeschränkung', values['DEMOASIN13']['audience_rating'])\n        self.assertEqual(4.99, values['DEMOASIN13']['price'])\n        self.assertEqual('EUR', values['DEMOASIN13']['currency'])\n\n        # ASIN DEMOASIN14\n        self.assertTrue('DEMOASIN14' in values)\n        self.assertEqual('DEMOASIN14', values['DEMOASIN14']['asin'])\n        self.assertEqual('Silent Alarm', values['DEMOASIN14']['title'])\n        self.assertEqual(None, values['DEMOASIN14']['isbn'])\n        self.assertEqual(None, values['DEMOASIN14']['eisbn'])\n        self.assertEqual('Audio CD', values['DEMOASIN14']['binding'])\n        self.assertEqual(None, values['DEMOASIN14']['date_publication'])\n        self.assertEqual(datetime.datetime(2007, 1, 2), values['DEMOASIN14']['date_release'])\n        self.assertEqual('http://ecx.images-amazon.com/images/I/DEMOASIN14.jpg', values['DEMOASIN14']['large_image_url'])\n        self.assertEqual('http://ecx.images-amazon.com/images/I/DEMOASIN14._SL160_.jpg', values['DEMOASIN14']['medium_image_url'])\n        self.assertEqual('http://ecx.images-amazon.com/images/I/DEMOASIN14._SL75_.jpg', values['DEMOASIN14']['small_image_url'])\n        self.assertEqual('http://www.amazon.de/dp/DEMOASIN14/?tag=sample-assoc-tag', values['DEMOASIN14']['offer_url'])\n        self.assertIsNone(values['DEMOASIN14']['audience_rating'])\n        self.assertEqual(11.86, values['DEMOASIN14']['price'])\n        self.assertEqual('EUR', values['DEMOASIN14']['currency'])\n\n        # ASIN DEMOASIN15\n        self.assertTrue('DEMOASIN15' in values)\n        self.assertEqual('DEMOASIN15', values['DEMOASIN15']['asin'])\n        self.assertEqual('Greatest Hits', values['DEMOASIN15']['title'])\n        self.assertEqual(None, values['DEMOASIN15']['isbn'])\n        self.assertEqual(None, values['DEMOASIN15']['eisbn'])\n        self.assertEqual('Audio CD', values['DEMOASIN15']['binding'])\n        self.assertEqual(datetime.datetime(2005, datetime.datetime.now().month, datetime.datetime.now().day), values['DEMOASIN15']['date_publication'])\n        self.assertEqual(datetime.datetime(2005, 2, 7), values['DEMOASIN15']['date_release'])\n        self.assertEqual('http://ecx.images-amazon.com/images/I/DEMOASIN15.jpg', values['DEMOASIN15']['large_image_url'])\n        self.assertEqual('http://ecx.images-amazon.com/images/I/DEMOASIN15._SL160_.jpg', values['DEMOASIN15']['medium_image_url'])\n        self.assertEqual('http://ecx.images-amazon.com/images/I/DEMOASIN15._SL75_.jpg', values['DEMOASIN15']['small_image_url'])\n        self.assertEqual('http://www.amazon.de/dp/DEMOASIN15/?tag=sample-assoc-tag', values['DEMOASIN15']['offer_url'])\n        self.assertIsNone(values['DEMOASIN15']['audience_rating'])\n        self.assertEqual(8.99, values['DEMOASIN15']['price'])\n        self.assertEqual('EUR', values['DEMOASIN15']['currency'])\n\n        # check log output\n        lc.check(\n            (\n                'price_monitor.product_advertising_api',\n                'INFO',\n                'starting lookup for ASINs DEMOASIN06, DEMOASIN07, DEMOASIN08, DEMOASIN09, DEMOASIN10, DEMOASIN11, DEMOASIN12, DEMOASIN13, DEMOASIN14, '\n                'DEMOASIN15'\n            )\n        )\n\n    @patch.object(ProductAdvertisingAPI, 'lookup_at_amazon')\n    @patch.object(ProductAdvertisingAPI, '__init__')\n    @log_capture()\n    def test_item_lookup_3_products(self, product_api_init, product_api_lookup, lc):\n        \"\"\"\n        Tests for parsing 3 products.\n        :param product_api_init: mockup for ProductAdvertisingAPI.__init__\n        :type product_api_init: unittest.mock.MagicMock\n        :param product_api_lookup: mockup for ProductAdvertisingAPI.lookup_at_amazon\n        :type product_api_lookup: unittest.mock.MagicMock\n        :param lc: log capture instance\n        :type lc: testfixtures.logcapture.LogCaptureForDecorator\n        \"\"\"\n        product_api_init.return_value = None\n        product_api_lookup.return_value = self.__get_product_bs(product_sample_3_products)\n\n        api = ProductAdvertisingAPI()\n        values = api.item_lookup([\n            'DEMOASIN18',\n            'DEMOASIN17',\n            'DEMOASIN18',\n        ])\n\n        # ensure the mocks were called\n        self.assertTrue(product_api_init.called)\n        self.assertTrue(product_api_lookup.called)\n\n        self.assertNotEqual(None, values)\n        self.assertEqual(type(dict()), type(values))\n        self.assertEqual(len(values), 3)\n\n        # ASIN DEMOASIN16\n        self.assertTrue('DEMOASIN16' in values)\n        self.assertEqual('DEMOASIN16', values['DEMOASIN16']['asin'])\n        self.assertEqual('Another Demo Series - Season 1 (8 DVDs)', values['DEMOASIN16']['title'])\n        self.assertEqual(None, values['DEMOASIN16']['isbn'])\n        self.assertEqual(None, values['DEMOASIN16']['eisbn'])\n        self.assertEqual('DVD', values['DEMOASIN16']['binding'])\n        self.assertEqual(\n            datetime.datetime(2004, 11, datetime.datetime.now().day if datetime.datetime.now().day < 31 else 30),\n            values['DEMOASIN16']['date_publication']\n        )\n        self.assertEqual(datetime.datetime(2004, 10, 27), values['DEMOASIN16']['date_release'])\n        self.assertEqual('http://ecx.images-amazon.com/images/I/DEMOASIN16.jpg', values['DEMOASIN16']['large_image_url'])\n        self.assertEqual('http://ecx.images-amazon.com/images/I/DEMOASIN16._SL160_.jpg', values['DEMOASIN16']['medium_image_url'])\n        self.assertEqual('http://ecx.images-amazon.com/images/I/DEMOASIN16._SL75_.jpg', values['DEMOASIN16']['small_image_url'])\n        self.assertEqual('http://www.amazon.de/dp/DEMOASIN16/?tag=sample-assoc-tag', values['DEMOASIN16']['offer_url'])\n        self.assertEqual('Freigegeben ab 16 Jahren', values['DEMOASIN16']['audience_rating'])\n        self.assertRaises(KeyError, lambda: values['DEMOASIN16']['price'])\n        self.assertRaises(KeyError, lambda: values['DEMOASIN16']['currency'])\n\n        # ASIN DEMOASIN17\n        self.assertTrue('DEMOASIN17' in values)\n        self.assertEqual('DEMOASIN17', values['DEMOASIN17']['asin'])\n        self.assertEqual('A Sample Book', values['DEMOASIN17']['title'])\n        self.assertEqual('123456789X', values['DEMOASIN17']['isbn'])\n        self.assertEqual(None, values['DEMOASIN17']['eisbn'])\n        self.assertEqual('Taschenbuch', values['DEMOASIN17']['binding'])\n        self.assertEqual(datetime.datetime(2014, 8, 18), values['DEMOASIN17']['date_publication'])\n        self.assertEqual(datetime.datetime(2014, 8, 18), values['DEMOASIN17']['date_release'])\n        self.assertEqual('http://ecx.images-amazon.com/images/I/DEMOASIN17.jpg', values['DEMOASIN17']['large_image_url'])\n        self.assertEqual('http://ecx.images-amazon.com/images/I/DEMOASIN17._SL160_.jpg', values['DEMOASIN17']['medium_image_url'])\n        self.assertEqual('http://ecx.images-amazon.com/images/I/DEMOASIN17._SL75_.jpg', values['DEMOASIN17']['small_image_url'])\n        self.assertEqual('http://www.amazon.de/dp/DEMOASIN17/?tag=sample-assoc-tag', values['DEMOASIN17']['offer_url'])\n        self.assertIsNone(values['DEMOASIN17']['audience_rating'])\n        self.assertEqual(10.00, values['DEMOASIN17']['price'])\n        self.assertEqual('EUR', values['DEMOASIN17']['currency'])\n\n        # ASIN DEMOASIN18\n        self.assertTrue('DEMOASIN18' in values)\n        self.assertEqual('DEMOASIN18', values['DEMOASIN18']['asin'])\n        self.assertEqual('Sønderzeichen', values['DEMOASIN18']['title'])\n        self.assertEqual(None, values['DEMOASIN18']['isbn'])\n        self.assertEqual(None, values['DEMOASIN18']['eisbn'])\n        self.assertEqual('Kindle Edition', values['DEMOASIN18']['binding'])\n        self.assertEqual(datetime.datetime(2009, 10, 14), values['DEMOASIN18']['date_publication'])\n        self.assertEqual(datetime.datetime(2009, 10, 14), values['DEMOASIN18']['date_release'])\n        self.assertEqual('http://ecx.images-amazon.com/images/I/DEMOASIN18.jpg', values['DEMOASIN18']['large_image_url'])\n        self.assertEqual('http://ecx.images-amazon.com/images/I/DEMOASIN18._SL160_.jpg', values['DEMOASIN18']['medium_image_url'])\n        self.assertEqual('http://ecx.images-amazon.com/images/I/DEMOASIN18._SL75_.jpg', values['DEMOASIN18']['small_image_url'])\n        self.assertEqual('http://www.amazon.de/dp/DEMOASIN18/?tag=sample-assoc-tag', values['DEMOASIN18']['offer_url'])\n        self.assertIsNone(values['DEMOASIN18']['audience_rating'])\n        self.assertRaises(KeyError, lambda: values['DEMOASIN18']['price'])\n        self.assertRaises(KeyError, lambda: values['DEMOASIN18']['currency'])\n\n    @patch.object(ProductAdvertisingAPI, 'lookup_at_amazon')\n    @patch.object(ProductAdvertisingAPI, '__init__')\n    @log_capture()\n    def test_item_lookup_no_images(self, product_api_init, product_api_lookup, lc):\n        \"\"\"\n        Test for an item with empty image nodes.\n        :param product_api_init: mockup for ProductAdvertisingAPI.__init__\n        :type product_api_init: unittest.mock.MagicMock\n        :param product_api_lookup: mockup for ProductAdvertisingAPI.lookup_at_amazon\n        :type product_api_lookup: unittest.mock.MagicMock\n        :param lc: log capture instance\n        :type lc: testfixtures.logcapture.LogCaptureForDecorator\n        \"\"\"\n        product_api_init.return_value = None\n        product_api_lookup.return_value = self.__get_product_bs(product_sample_no_images)\n\n        api = ProductAdvertisingAPI()\n        values = api.item_lookup(['DEMOASIN03'])\n\n        # ensure the mocks were called\n        self.assertTrue(product_api_init.called)\n        self.assertTrue(product_api_lookup.called)\n\n        self.assertNotEqual(None, values)\n        self.assertEqual(type(dict()), type(values))\n        self.assertEqual(len(values), 1)\n\n        self.assertTrue('DEMOASIN03' in values)\n        self.assertEqual('DEMOASIN03', values['DEMOASIN03']['asin'])\n        self.assertEqual('Demo Series - Season 2 [Blu-ray]', values['DEMOASIN03']['title'])\n        self.assertEqual(None, values['DEMOASIN03']['isbn'])\n        self.assertEqual(None, values['DEMOASIN03']['eisbn'])\n        self.assertEqual('Blu-ray', values['DEMOASIN03']['binding'])\n        self.assertEqual(datetime.datetime(2014, 12, 22), values['DEMOASIN03']['date_publication'])\n        self.assertEqual(datetime.datetime(2014, 12, 22), values['DEMOASIN03']['date_release'])\n        self.assertEqual(None, values['DEMOASIN03']['large_image_url'])\n        self.assertEqual(None, values['DEMOASIN03']['medium_image_url'])\n        self.assertEqual(None, values['DEMOASIN03']['small_image_url'])\n        self.assertEqual('http://www.amazon.de/dp/DEMOASIN03/?tag=sample-assoc-tag', values['DEMOASIN03']['offer_url'])\n        self.assertEqual('Freigegeben ab 16 Jahren', values['DEMOASIN03']['audience_rating'])\n        self.assertEqual(17.99, values['DEMOASIN03']['price'])\n        self.assertEqual('EUR', values['DEMOASIN03']['currency'])\n\n        # check log output\n        lc.check(\n            ('price_monitor.product_advertising_api', 'INFO', 'starting lookup for ASINs DEMOASIN03')\n        )\n\n    @patch.object(ProductAdvertisingAPI, 'lookup_at_amazon')\n    @patch.object(ProductAdvertisingAPI, '__init__')\n    @log_capture()\n    def test_item_lookup_artist(self, product_api_init, product_api_lookup, lc):\n        \"\"\"\n        Test for a normal bluray.\n        :param product_api_init: mockup for ProductAdvertisingAPI.__init__\n        :type product_api_init: unittest.mock.MagicMock\n        :param product_api_lookup: mockup for ProductAdvertisingAPI.lookup_at_amazon\n        :type product_api_lookup: unittest.mock.MagicMock\n        :param lc: log capture instance\n        :type lc: testfixtures.logcapture.LogCaptureForDecorator\n        \"\"\"\n        product_api_init.return_value = None\n        product_api_lookup.return_value = self.__get_product_bs(product_sample_with_artist)\n\n        api = ProductAdvertisingAPI()\n        values = api.item_lookup(['DEMOASIN19'])\n\n        # ensure the mocks were called\n        self.assertTrue(product_api_init.called)\n        self.assertTrue(product_api_lookup.called)\n\n        self.assertNotEqual(None, values)\n        self.assertEqual(type(dict()), type(values))\n        self.assertEqual(len(values), 1)\n\n        self.assertTrue('DEMOASIN19' in values)\n        self.assertEqual('DEMOASIN19', values['DEMOASIN19']['asin'])\n        self.assertEqual('The CD Title', values['DEMOASIN19']['title'])\n        self.assertEqual('The Artist', values['DEMOASIN19']['artist'])\n        self.assertEqual(None, values['DEMOASIN19']['isbn'])\n        self.assertEqual(None, values['DEMOASIN19']['eisbn'])\n        self.assertEqual('Audio CD', values['DEMOASIN19']['binding'])\n        self.assertEqual(datetime.datetime(2015, datetime.datetime.now().month, datetime.datetime.now().day), values['DEMOASIN19']['date_publication'])\n        self.assertEqual(datetime.datetime(2015, 2, 27), values['DEMOASIN19']['date_release'])\n        self.assertEqual('http://ecx.images-amazon.com/images/I/DEMOASIN19.jpg', values['DEMOASIN19']['large_image_url'])\n        self.assertEqual('http://ecx.images-amazon.com/images/I/DEMOASIN19._SL160_.jpg', values['DEMOASIN19']['medium_image_url'])\n        self.assertEqual('http://ecx.images-amazon.com/images/I/DEMOASIN19._SL75_.jpg', values['DEMOASIN19']['small_image_url'])\n        self.assertEqual('http://www.amazon.de/dp/DEMOASIN19/?tag=sample-assoc-tag', values['DEMOASIN19']['offer_url'])\n        self.assertIsNone(values['DEMOASIN19']['audience_rating'])\n        self.assertEqual(12.99, values['DEMOASIN19']['price'])\n        self.assertEqual('EUR', values['DEMOASIN19']['currency'])\n\n        # check log output\n        lc.check(\n            ('price_monitor.product_advertising_api', 'INFO', 'starting lookup for ASINs DEMOASIN19')\n        )\n\n    def test_format_datetime(self):\n        \"\"\"\n        Tests the datetime formatter.\n        \"\"\"\n        api = ProductAdvertisingAPI()\n        self.assertEqual(api.format_datetime('2014-10-11'), datetime.datetime(2014, 10, 11))\n        self.assertEqual(api.format_datetime('2012-12'), datetime.datetime(2012, 12, datetime.datetime.now().day))\n        self.assertEqual(api.format_datetime('2015-02'), datetime.datetime(2015, 2, datetime.datetime.now().day))\n\n    @patch.object(ProductAdvertisingAPI, 'lookup_at_amazon')\n    @patch.object(ProductAdvertisingAPI, '__init__')\n    @log_capture()\n    def test_invalid_isbn_value(self, product_api_init, product_api_lookup, lc):\n        \"\"\"\n        Test for a product with an ISBN-13 value in the ISBN-10 field (isbn).\n        :param product_api_init: mockup for ProductAdvertisingAPI.__init__\n        :type product_api_init: unittest.mock.MagicMock\n        :param product_api_lookup: mockup for ProductAdvertisingAPI.lookup_at_amazon\n        :type product_api_lookup: unittest.mock.MagicMock\n        :param lc: log capture instance\n        :type lc: testfixtures.logcapture.LogCaptureForDecorator\n        \"\"\"\n        product_api_init.return_value = None\n        product_api_lookup.return_value = self.__get_product_bs(product_sample_isbn_issue)\n\n        api = ProductAdvertisingAPI()\n        values = api.item_lookup(['TESTASIN20'])\n\n        # ensure the mocks were called\n        self.assertTrue(product_api_init.called)\n        self.assertTrue(product_api_lookup.called)\n\n        self.assertNotEqual(None, values)\n        self.assertEqual(type(dict()), type(values))\n        self.assertEqual(len(values), 1)\n\n        self.assertTrue('TESTASIN20' in values)\n        self.assertEqual('TESTASIN20', values['TESTASIN20']['asin'])\n\n        product = values['TESTASIN20']\n\n        # though \"isbn\" contains a ISBN-13 value we save that into \"eisbn\" and clear \"isbn\"\n        self.assertEqual(product['isbn'], None)\n        self.assertEqual(len(product['eisbn']), 13)\n        self.assertEqual(product['eisbn'], '1234567890123')\n\n        # check log output\n        lc.check(\n            ('price_monitor.product_advertising_api', 'INFO', 'starting lookup for ASINs TESTASIN20')\n        )\n"
  },
  {
    "path": "tests/settings.py",
    "content": "import os\n\n\nTEST_DIR = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'tests')\n\nDATABASES = {\n    'default': {\n        'ENGINE': 'django.db.backends.sqlite3',\n        'NAME': ':memory:',\n    }\n}\n\nMIDDLEWARE_CLASSES = [\n    'django.contrib.sessions.middleware.SessionMiddleware',\n    'django.middleware.common.CommonMiddleware',\n    'django.middleware.csrf.CsrfViewMiddleware',\n    'django.contrib.auth.middleware.AuthenticationMiddleware',\n    'django.middleware.clickjacking.XFrameOptionsMiddleware',\n]\n\nINSTALLED_APPS = [\n    'django.contrib.auth',\n    'django.contrib.contenttypes',\n    'django.contrib.sessions',\n    'price_monitor',\n]\n\nSTATIC_URL = '/static/'\n\nSTATIC_ROOT = os.path.join(TEST_DIR, 'static')\n\nSECRET_KEY = os.environ['SECRET_KEY']\n\nROOT_URLCONF = 'price_monitor.urls'\n\nPRICE_MONITOR_AMAZON_PRODUCT_API_REGION = 'DE'\nPRICE_MONITOR_AMAZON_PRODUCT_API_ASSOC_TAG = 'sample-assoc-tag'\n"
  },
  {
    "path": "tests/test_product.py",
    "content": "from django.test import TestCase\n\nfrom price_monitor import app_settings\nfrom price_monitor.models import Product\n\n\nclass ProductTest(TestCase):\n\n    def test_set_failed_to_sync(self):\n        asin = 'ASINASINASIN'\n        p = Product.objects.create(asin=asin)\n        self.assertIsNotNone(p)\n        self.assertEqual(asin, p.asin)\n        self.assertEqual(0, p.status)\n\n        p.set_failed_to_sync()\n        self.assertEqual(2, p.status)\n\n    def test_get_image_urls(self):\n        \"\"\"Tests the Product.get_image_urls method.\"\"\"\n\n        # FIXME usually you would test a HTTP and a HTTPS setup but override_settings does not work with our app_settings (the setting does not get overwritten)\n\n        # no images set\n        p = Product.objects.create(\n            asin='ASIN0000001',\n        )\n        self.assertEqual(3, len(p.get_image_urls()))\n        self.assertTrue('small' in p.get_image_urls())\n        self.assertTrue('medium' in p.get_image_urls())\n        self.assertTrue('large' in p.get_image_urls())\n        self.assertEqual(app_settings.PRICE_MONITOR_AMAZON_SSL_IMAGE_DOMAIN, p.get_image_urls()['small'])\n        self.assertEqual(app_settings.PRICE_MONITOR_AMAZON_SSL_IMAGE_DOMAIN, p.get_image_urls()['medium'])\n        self.assertEqual(app_settings.PRICE_MONITOR_AMAZON_SSL_IMAGE_DOMAIN, p.get_image_urls()['large'])\n\n        # all images set\n        p = Product.objects.create(\n            asin='ASIN0000002',\n            small_image_url='http://github.com/ponyriders/django-amazon-price-monitor/small.png',\n            medium_image_url='http://github.com/ponyriders/django-amazon-price-monitor/medium.png',\n            large_image_url='http://github.com/ponyriders/django-amazon-price-monitor/large.png',\n        )\n        self.assertEqual(3, len(p.get_image_urls()))\n        self.assertTrue('small' in p.get_image_urls())\n        self.assertTrue('medium' in p.get_image_urls())\n        self.assertTrue('large' in p.get_image_urls())\n        self.assertEqual(\n            '{0}{1}'.format(app_settings.PRICE_MONITOR_AMAZON_SSL_IMAGE_DOMAIN, '/ponyriders/django-amazon-price-monitor/small.png'),\n            p.get_image_urls()['small']\n        )\n        self.assertEqual(\n            '{0}{1}'.format(app_settings.PRICE_MONITOR_AMAZON_SSL_IMAGE_DOMAIN, '/ponyriders/django-amazon-price-monitor/medium.png'),\n            p.get_image_urls()['medium']\n        )\n        self.assertEqual(\n            '{0}{1}'.format(app_settings.PRICE_MONITOR_AMAZON_SSL_IMAGE_DOMAIN, '/ponyriders/django-amazon-price-monitor/large.png'),\n            p.get_image_urls()['large']\n        )\n\n    def test_get_detail_url(self):\n        \"\"\"Tests the get_detail_url method\"\"\"\n        p = Product.objects.create(\n            asin='ASIN0054321',\n        )\n        assert p.get_detail_url() == str(app_settings.PRICE_MONITOR_BASE_URL + '/#/products/ASIN0054321')\n"
  },
  {
    "path": "tests/test_utils.py",
    "content": "\"\"\"Tests for the utils module.\"\"\"\nfrom django.test import TestCase\n\nfrom price_monitor import utils\n\n\nclass UtilsTest(TestCase):\n\n    \"\"\"Tests for the utils module.\"\"\"\n\n    def test_get_offer_url(self):\n        \"\"\"Test the offer url function\"\"\"\n        self.assertEqual('http://www.amazon.de/dp/X1234567890/?tag=sample-assoc-tag', utils.get_offer_url('X1234567890'))\n\n    def test_chunk_list(self):\n        \"\"\"Tests the chunk_list function\"\"\"\n        self.assertEqual(\n            [[10, 11, 12, 13], [14, 15, 16, 17], [18, 19]],\n            list(utils.chunk_list(list(range(10, 20)), 4))\n        )\n        self.assertEqual(\n            [[1]],\n            list(utils.chunk_list([1], 7))\n        )\n        self.assertEqual(\n            [['L', 'o', 'r'], ['e', 'm', ' '], ['I', 'p', 's'], ['u', 'm']],\n            list(utils.chunk_list(list('Lorem Ipsum'), 3))\n        )\n"
  },
  {
    "path": "tox.ini",
    "content": "[tox]\nenvlist =\n    py34-django1.8,\n    py34-django1.9,\n    py34-django1.10,\n    py34-django1.11,\n    py35-django1.9,\n    py35-django1.10,\n    py35-django1.11,\n    py36-django1.11\n\n[testenv]\nsetenv =\n    STAGE = TravisCI\n    DJANGO_SETTINGS_MODULE = tests.settings\n    PYTHONPATH = {toxinidir}:{toxinidir}/price_monitor\n    SECRET_KEY = 'F(fxm_9aKa9F_7e$!U1can%;%qc9A[.Jcx2lVCwWo3}*DL,y?H'\n    AWS_ACCESS_KEY_ID = ''\n    AWS_SECRET_ACCESS_KEY = ''\ncommands =\n    py.test --cov=price_monitor --ds tests.settings\n\n[base]\ndeps =\n    pytest<=3.2\n    pytest-cov<=2.5\n    pytest-pep8<1.1\n    pytest-flakes<=2.0\n    pytest-sugar<=0.9\n    pytest-django<=3.1\n    testfixtures<=5.2\n\n[testenv:py34-django1.8]\nbasepython = python3.4\ndeps =\n    django>=1.8,<1.9\n    {[base]deps}\n\n[testenv:py34-django1.9]\nbasepython = python3.4\ndeps =\n    django>=1.9,<1.10\n    {[base]deps}\n\n[testenv:py34-django1.10]\nbasepython = python3.4\ndeps =\n    django>=1.10,<1.11\n    {[base]deps}\n\n[testenv:py34-django1.11]\nbasepython = python3.4\ndeps =\n    django>=1.11,<2\n    {[base]deps}\n\n[testenv:py35-django1.9]\nbasepython = python3.5\ndeps =\n    django>=1.9,<1.10\n    {[base]deps}\n\n[testenv:py35-django1.10]\nbasepython = python3.5\ndeps =\n    django>=1.10,<1.11\n    {[base]deps}\n\n[testenv:py35-django1.11]\nbasepython = python3.5\ndeps =\n    django>=1.11,<2\n    {[base]deps}\n\n[testenv:py36-django1.11]\nbasepython = python3.6\ndeps =\n    django>=1.11,<2\n    {[base]deps}\n\n[pytest]\naddopts =\n    --pep8 --flakes\nnorecursedirs =\n    .cache\n    .git\n    .env\n    .tox\n    docker/logs\n    docker/media\n    docker/postgres\n    docs\npep8maxlinelength = 160\npep8ignore =\n    docs/*.py ALL"
  }
]