Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
cltrudeau committed Mar 27, 2017
0 parents commit d24232b
Show file tree
Hide file tree
Showing 35 changed files with 1,184 additions and 0 deletions.
27 changes: 27 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# git ls-files --others --exclude-from=.git/info/exclude
# Lines that start with '#' are comments.
# For a project mostly in C, the following would be a good set of
# exclude patterns (uncomment them if you want to use them):
# *.[oa]
# *~
.DS_Store
*.pyc
*.o
*.so
*.swp
*~
.coverage*
htmlcov/
foo.py
debug.log
#*.ini
db.sqlite3
logs/*
*.egg
*.eggs
*.egg-info
build/
dist/
docs/_build/
extras/sample_site/uploads/
.tox/
4 changes: 4 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
0.1
===

* initial commit to pypi
3 changes: 3 additions & 0 deletions CREDITS.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Gabriel Gabster wrote a similar project taking a different approach. Some of
the internal code is inspired by his:
- https://github.com/gabegaster/django-offlinecdn
22 changes: 22 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
The MIT License (MIT)

Copyright (c) 2017 Christopher Trudeau

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

2 changes: 2 additions & 0 deletions MANIFEST.IN
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
include LICENSE
include README.rst
80 changes: 80 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
django-airplane
***************

This app is to help in those situations where you can't get on the network but
you want to write some Django code. Surround your static CDN references (like
jquery and the like) with this template tag and when you turn it on the URLs
will be re-written from a local copy.

Installation
============

In your settings file, add 'airplane' to your ``settings.INSTALLED_APPS`` field
and make the following additions:

.. code-block:: python
import airplane
STATICFILES_DIRS = (
os.path.join(BASE_DIR, airplane.CACHE_DIR),
)
AIRPLANE_MODE = airplane.BUILD_CACHE
#AIRPLANE_MODE = airplane.USE_CACHE
Now use the ``airplane`` tag in your templates

.. code-block:: html

{% load airplanetags %}

<html>
<head>
<link rel="stylesheet"
href="{% airplane 'https://maxcdn.bootstrapcdn.com/bootstrap.min.css' %}">
</head>
</html>

Change the ``AIRPLANE_MODE`` setting to ``airplane.USE_CACHE`` and subsequent
calls to the ``{% airplane %}`` tag will return a reference to the locally
cached version.


Settings
========

Airplane only does something if ``DEBUG=True`` and if you have an
``AIRPLANE_MODE`` value set to either ``airplane.BUILD_CACHE`` or
``airplane.USE_CACHE``. If one of these conditions is not met, the tag simply
returns the value passed in.

For example, if ``DEBUG=False`` and your template contains:

.. code-block:: html

<link rel="stylesheet"
href="{% airplane 'https://maxcdn.bootstrapcdn.com/bootstrap.min.css' %}">


Then the above snippet renders as:

.. code-block:: html

<link rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap.min.css">


When ``AIRPLANE_MODE`` is set to ``airplane.BUILD_CACHE`` any URLs passed in
are fetched and their contents added to a local cache. The default local
cache is ``.airport_cache`` relative to the base directory of your project.

You can change the location of the cache by setting ``AIRPLANE_CACHE``. The
setting accepts either fully qualified paths or paths relative to the
project's base directory.

Once you have cached all the files you are using, switch ``AIRPLANE_MODE`` to
``airplane.USE_CACHE``. All URLs are now re-written to point to the contents
of the local cache.


6 changes: 6 additions & 0 deletions airplane/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
__version__ = '0.1.0'

CACHE_DIR = '.airplane_cache'

USE_CACHE = 1
BUILD_CACHE = 2
1 change: 1 addition & 0 deletions airplane/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# empty
Empty file.
66 changes: 66 additions & 0 deletions airplane/templatetags/airplanetags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# airplane.templatetags.airplanetags.py
import os

from django import template
from django.conf import settings
from django.utils.http import urlquote_plus

import requests

import airplane as package

register = template.Library()

# ============================================================================

def _convert_url(url):
converted = urlquote_plus(url)
converted = converted.replace('%', '')
return converted


@register.simple_tag
def airplane(url):
"""This template tag modifies a URL depending on the values in settings.
It either returns the URL as is, returns the URL as is and caches a copy,
or returns a re-written URL pointing to the cache.
Example::
{% airplane 'https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css' %}
"""
debug = getattr(settings, 'DEBUG', False)
if not debug:
# we are not in debug mode, just pass through the URL
return url

conf = getattr(settings, 'AIRPLANE_MODE', 0)
if conf == 0:
# not in AIRPLANE_MODE, pass through
return url

# convert url to local path
filename = _convert_url(url)
dirname = getattr(settings, 'AIRPLANE_CACHE', package.CACHE_DIR)

if os.path.isabs(dirname):
dir_path = dirname
else:
dir_path = os.path.join(getattr(settings, 'BASE_DIR'), dirname)

file_path = os.path.join(dir_path, filename)

if conf == package.BUILD_CACHE:
# fetch the content for caching
if not os.path.exists(dir_path):
os.mkdir(dir_path)

response = requests.get(url, stream=True)
if not response.ok:
raise IOError('Unable to fetch %s' % url)
with open(file_path, 'wb') as stream:
for chunk in response.iter_content(chunk_size=128):
stream.write(chunk)

# we're caching, return the re-written static URL, need to encode
return '/static/%s' % filename
Empty file added airplane/tests/__init__.py
Empty file.
123 changes: 123 additions & 0 deletions airplane/tests/test_airplane.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import os, shutil

from django.conf import settings
from django.template import Context, Template
from django.test import TestCase, override_settings
import six

import airplane
from airplane.templatetags.airplanetags import _convert_url
from mock import patch
from wrench.contexts import temp_directory

# ============================================================================
# Test Objects
# ============================================================================

class FakeRequests(object):
ok = True
content = [six.b('pretend content'), ]

def iter_content(self, **kwargs):
return iter(self.content)


def fake_get(*args, **kwargs):
return FakeRequests()


def fake_bad_get(*args, **kwargs):
r = FakeRequests()
r.ok = False
return r


class AirplaneTests(TestCase):
def setUp(self):
self.cache1 = '.airplane_cache'
self.cache1_path = os.path.join(settings.BASE_DIR, self.cache1)
self.cache2 = '.airplane_cache2'
self.cache2_path = os.path.join(settings.BASE_DIR, self.cache2)

def tearDown(self):
self._remove_local_caches()

def _remove_local_caches(self):
if os.path.exists(self.cache1_path):
shutil.rmtree(self.cache1_path)

if os.path.exists(self.cache2_path):
shutil.rmtree(self.cache2_path)

def _render(self,url, expected):
# renders a template with our keyword in it
t = """{% load airplanetags %}{% airplane '""" + url + """' %}"""

template = Template(t)
context = Context({})
result = template.render(context)
self.assertEqual(expected, result.strip())

def _check_build_cache(self, url, expected, dir_name):
# test with a mocked request that works
with patch('requests.get') as mock_requests:
mock_requests.side_effect = fake_get

# render the tag
self._render(url, expected)

# check that the directory got a file
os.path.exists(os.path.join(dir_name, expected))

def test_airplane(self):
url = 'http://foo.com'
expected = '/static/' + _convert_url(url)

# test pass through, DEBUG=False, no AIRPLANE_MODE set
self._render(url, url)

# test pass through, DEBUG=True, no AIRPLANE_MODE set
with override_settings(DEBUG=True):
self._render(url, url)

# test absolute path cache creation and fetching
with temp_directory() as td:
with override_settings(
DEBUG=True,
AIRPLANE_CACHE=td,
AIRPLANE_MODE=airplane.BUILD_CACHE):

# test with a mocked request that works
self._check_build_cache(url, expected, td)

# test with a mocked request that fails (404s etc)
with patch('requests.get') as mock_requests:
mock_requests.side_effect = fake_bad_get

# render the tag
with self.assertRaises(IOError):
self._render(url, expected)

# test cache creation with a local directory that needs creating
self._remove_local_caches()

# test with the default cache name
with override_settings(
DEBUG=True,
AIRPLANE_MODE=airplane.BUILD_CACHE):

self._check_build_cache(url, expected, self.cache1)

# check that the directory got a file
os.path.exists(os.path.join(self.cache1, expected))

# test with a non-default cache name that is local
with override_settings(
DEBUG=True,
AIRPLANE_CACHE=self.cache2,
AIRPLANE_MODE=airplane.BUILD_CACHE):

self._check_build_cache(url, expected, self.cache2)

# check that the directory got a file
os.path.exists(os.path.join(self.cache1, expected))
22 changes: 22 additions & 0 deletions clean_build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/bin/bash

version=`grep "__version__ = " airplane/__init__.py | cut -d "'" -f 2`

git tag "$version"

if [ "$?" != "0" ] ; then
exit $?
fi

rm -rf build
rm -rf dist
python setup.py sdist
python setup.py bdist_wheel

echo "------------------------"
echo
echo "Built version: $version"
echo
echo "now do:"
echo " twine upload dist/*"
echo
Loading

0 comments on commit d24232b

Please sign in to comment.