├── .coveragerc
├── .gitignore
├── .travis.yml
├── LICENSE.txt
├── MANIFEST.in
├── Makefile
├── README.rst
├── favicon
├── __init__.py
├── admin.py
├── apps.py
├── management
│ ├── __init__.py
│ └── commands
│ │ ├── __init__.py
│ │ ├── delete_favicon.py
│ │ └── generate_favicon.py
├── migrations
│ └── __init__.py
├── models.py
├── settings.py
├── templates
│ └── favicon
│ │ ├── favicon.html
│ │ └── ieconfig.xml
├── templatetags
│ ├── __init__.py
│ └── favicon.py
├── tests
│ ├── __init__.py
│ ├── logo.png
│ ├── settings.py
│ ├── test_command.py
│ ├── test_templatetags.py
│ ├── test_utils.py
│ └── utils.py
├── utils.py
└── views.py
├── requirements-tests.txt
├── requirements.txt
├── runtests.py
├── setup.cfg
├── setup.py
└── tox.ini
/.coveragerc:
--------------------------------------------------------------------------------
1 | # .coveragerc to control coverage.py
2 | [run]
3 | branch = True
4 | source = favicon
5 | omit =
6 | favicon/tests/*
7 | favicon/apps.py
8 | favicon/models.py
9 | favicon/views.py
10 | favicon/migrations*
11 | favicon/management/__init__.py
12 | favicon/management/commands/__init__.py
13 |
14 | [report]
15 | # Regexes for lines to exclude from consideration
16 | exclude_lines =
17 | # Have to re-enable the standard pragma
18 | pragma: no cover
19 | noqa:
20 |
21 | # Don't complain about missing debug-only code:
22 | def __repr__
23 | def __str__
24 | if self\.debug
25 |
26 | # Don't complain if tests don't hit defensive assertion code:
27 | raise AssertionError
28 | raise NotImplementedError
29 |
30 | # Don't complain if non-runnable code isn't run:
31 | if 0:
32 | if __name__ == .__main__.:
33 | __all__
34 | import
35 | deprecated_warning
36 | in_development_warning
37 |
38 | ignore_errors = True
39 |
40 | [html]
41 | directory = coverage_html_report
42 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 |
5 | # C extensions
6 | *.so
7 |
8 | # Distribution / packaging
9 | .Python
10 | env/
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | *.egg-info/
23 | .installed.cfg
24 | *.egg
25 |
26 | # PyInstaller
27 | # Usually these files are written by a python script from a template
28 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
29 | *.manifest
30 | *.spec
31 |
32 | # Installer logs
33 | pip-log.txt
34 | pip-delete-this-directory.txt
35 |
36 | # Unit test / coverage reports
37 | htmlcov/
38 | .tox/
39 | .coverage
40 | .coverage.*
41 | .cache
42 | nosetests.xml
43 | coverage.xml
44 | *,cover
45 | tests/media/
46 | coverage_html_report/
47 |
48 | # Translations
49 | *.mo
50 | *.pot
51 |
52 | # Django stuff:
53 | *.log
54 |
55 | # Sphinx documentation
56 | docs/_build/
57 |
58 | # PyBuilder
59 | target/
60 |
61 | # IDEs
62 | .idea/
63 | *.sw[po]
64 | test-sqlite
65 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 |
3 | python:
4 | - "3.6"
5 | - "3.7"
6 | - "3.8"
7 |
8 | env:
9 | matrix:
10 | - DJANGO=3.0
11 |
12 | install:
13 | - TOX_ENV=py${TRAVIS_PYTHON_VERSION}-django${DJANGO}
14 | - pip install tox
15 | - tox -e $TOX_ENV --notest
16 |
17 | script:
18 | - tox -e $TOX_ENV
19 |
20 | after_success:
21 | - tox -e $TOX_ENV -- pip install coveralls
22 | - tox -e $TOX_ENV -- coveralls $COVERALLS_OPTION
23 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) 2016, Anthony Monthe (ZuluPro)
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without modification,
5 | are permitted provided that the following conditions are met:
6 |
7 | * Redistributions of source code must retain the above copyright notice,
8 | this list of conditions and the following disclaimer.
9 | * Redistributions in binary form must reproduce the above copyright notice,
10 | this list of conditions and the following disclaimer in the documentation
11 | and/or other materials provided with the distribution.
12 | * Neither the name django-dbbackup nor the names of its contributors
13 | may be used to endorse or promote products derived from this software without
14 | specific prior written permission.
15 |
16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
20 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
21 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
22 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
23 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
25 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | recursive-include favicon * *.css *.html *.xml
2 | include requirements*.txt
3 |
4 | global-exclude *.pyc *.pyo
5 | global-exclude .git
6 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: all test clean upload
2 |
3 | clean:
4 | find . -name "*.pyc" -type f -delete
5 | find . -name "__pycache__" -type d -exec rm -rf {} \;
6 | find . -name "*.egg-info" -type d -exec rm -rf {} \; || true
7 | rm -rf build/ dist/ \
8 | coverage_html_report .coverage \
9 | *.egg
10 |
11 | test:
12 | python runtests.py
13 |
14 | install:
15 | python setup.py install
16 |
17 | build:
18 | python setup.py build
19 |
20 | upload:
21 | make clean
22 | python setup.py sdist
23 | python setup.py sdist upload
24 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | Django Super Favicon
2 | ====================
3 |
4 | .. image:: https://api.travis-ci.org/ZuluPro/django-super-favicon.svg
5 | :target: https://travis-ci.org/ZuluPro/django-super-favicon
6 |
7 | .. image:: https://coveralls.io/repos/ZuluPro/django-super-favicon/badge.svg?branch=master&service=github
8 | :target: https://coveralls.io/github/ZuluPro/django-super-favicon?branch=master
9 |
10 | Django Super Favicon is a project that aiming to replace external solutions
11 | like `realfavicongenerator.net`_: Create favicon for all kind of client
12 | platform.
13 |
14 | Super Favicon does:
15 |
16 | - Creates icons in various size
17 | - Uploads them in static file storage (or other)
18 | - Creates HTML headers tags for use them
19 |
20 | Why
21 | ===
22 |
23 | It could sound useless, but hold a website identity in browsers' favorites or
24 | iOS/Android/Windows home screen is a pretty good thing.
25 |
26 | I often see that Django dev used to create a view for serve favicon.ico, I
27 | think this is summum of bad pratices: File must be served by a dedicated
28 | server. I designed this project to use Django Storage API and make generated
29 | files deployment agnostic.
30 |
31 | There are other Django projects in the same topic:
32 |
33 | - `django-favicon`_ : A view for serve favicon (Ouch)
34 | - `django-favicon-plus`_ : Make the same than mine, but through models and ImageField
35 |
36 | That's why *super* ...
37 |
38 | Install & usage
39 | ===============
40 |
41 | ::
42 |
43 | pip install django-super-favicon
44 |
45 | Add the following things in your ``settings.py``: ::
46 |
47 | INSTALLED_APPS = (
48 | ...
49 | 'favicon',
50 | ...
51 | )
52 |
53 | Upload them to your storage (by default your filesystem): ::
54 |
55 | ./manage.py generate_favicon your_icon.png
56 |
57 | And put this in your templates: ::
58 |
59 | {% load favicon %}
60 | ...
61 |
62 | ...
63 | {% get_favicons %}
64 |
65 |
66 | It will produce something like: ::
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 | Settings
95 | ========
96 |
97 | Super Favicon can be configured with the followings constants in
98 | ``settings.py``:
99 |
100 | **FAVICON_STORAGE**: Storage class used for store favicons,
101 | default: ``settings.STATICFILES_STORAGE``
102 |
103 | **FAVICON_STORAGE_OPTIONS**: Options used for instanciate the custom storage.
104 | default: ``{}``
105 |
106 |
107 | Management Commands
108 | ===================
109 |
110 | generate_favicon
111 | ----------------
112 |
113 | Create favicons in different formats.
114 |
115 | generate_favicon
116 |
117 | delete_favicon
118 | --------------
119 |
120 | Delete previously created favicon
121 |
122 | delete_favicon
123 |
124 | Contributing
125 | ============
126 |
127 | All contribution are very welcomed, propositions, problems, bugs and
128 | enhancement are tracked with `GitHub issues`_ system and patch are submitted
129 | via `pull requests`_.
130 |
131 | We use `Travis`_ coupled with `Coveralls`_ as continious integration tools.
132 |
133 | .. _`realfavicongenerator.net`: https://realfavicongenerator.net/
134 | .. _`django-favicon`: https://pypi.python.org/pypi/django-favicon
135 | .. _`django-favicon-plus`: https://github.com/arteria/django-favicon-plus
136 | .. _`Read The Docs`: http://django-super-favicon.readthedocs.org/
137 | .. _`GitHub issues`: https://github.com/ZuluPro/django-super-favicon/issues
138 | .. _`pull requests`: https://github.com/ZuluPro/django-super-favicon/pulls
139 | .. _Travis: https://travis-ci.org/ZuluPro/django-super-favicon
140 | .. _Coveralls: https://coveralls.io/github/ZuluPro/django-super-favicon
141 |
--------------------------------------------------------------------------------
/favicon/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | Django app for:
3 | - Generate favicon in multiple format
4 | - Put in a storage backend
5 | - Include HTML tags for use favicon
6 | """
7 | VERSION = (0, 7, 1)
8 | __version__ = '.'.join([str(i) for i in VERSION])
9 | __author__ = 'Anthony Monthe (ZuluPro)'
10 | __email__ = 'anthony.monthe@gmail.com'
11 | __url__ = 'https://github.com/ZuluPro/django-super-favicon'
12 |
--------------------------------------------------------------------------------
/favicon/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 | # Register your models here.
4 |
--------------------------------------------------------------------------------
/favicon/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class FaviconConfig(AppConfig):
5 | name = 'favicon'
6 |
--------------------------------------------------------------------------------
/favicon/management/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ZuluPro/django-super-favicon/a0e054915c582d56d79703d57754f8b50ad67672/favicon/management/__init__.py
--------------------------------------------------------------------------------
/favicon/management/commands/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ZuluPro/django-super-favicon/a0e054915c582d56d79703d57754f8b50ad67672/favicon/management/commands/__init__.py
--------------------------------------------------------------------------------
/favicon/management/commands/delete_favicon.py:
--------------------------------------------------------------------------------
1 | """Delete favicons from storage."""
2 | from django.core.management.base import BaseCommand, CommandError
3 | from django.core.files.storage import get_storage_class
4 | from django.utils import six
5 | from favicon import settings
6 | from favicon.utils import delete
7 |
8 | input = raw_input if six.PY2 else input
9 |
10 |
11 | class Command(BaseCommand):
12 | def add_arguments(self, parser):
13 | parser.add_argument('--prefix', '-p', default=None,
14 | help="Prefix included in files' names")
15 | parser.add_argument('--noinput', action='store_true', default=False,
16 | help="Do NOT prompt the user for input of any kind.")
17 | parser.add_argument('--dry-run', '-n', action='store_true', default=False,
18 | help="Do everything except modify the filesystem.")
19 |
20 | def handle(self, *args, **options):
21 | prefix = options['prefix']
22 | storage = get_storage_class(settings.STORAGE)(**settings.STORAGE_OPTIONS)
23 |
24 | if not options['noinput']:
25 | answer = input("Are you sure you want to continue? [Y/n]")
26 | if answer.lower().startswith('n'):
27 | self.stdout.write('Quitting')
28 | return
29 |
30 | self.stdout.write('Launch favicon deleting')
31 | if options['dry_run']:
32 | self.stdout.write('No operation launched')
33 | else:
34 | delete(storage, prefix)
35 |
--------------------------------------------------------------------------------
/favicon/management/commands/generate_favicon.py:
--------------------------------------------------------------------------------
1 | """Create favicons and upload into storage."""
2 | import io
3 | import re
4 | from shutil import copyfileobj
5 | from django.core.management.base import BaseCommand, CommandError
6 | from django.core.files.storage import get_storage_class
7 | from favicon import settings
8 | from favicon.utils import generate
9 |
10 | try:
11 | from urllib.request import urlopen
12 | except ImportError:
13 | from urllib2 import urlopen
14 |
15 |
16 | SOURCE_FILE_HELP = """Input file used to generate favicons, example:
17 | '/path/to/myfile.png' : Get from local filesystem root
18 | 'path/to/myfile.png' : Get from local filesystem relative path
19 | 'file://myfile.png' : Get from static file storage
20 | 'http://example.com/myfile.png' : Get from HTTP server
21 | """
22 |
23 |
24 | class Command(BaseCommand):
25 | def add_arguments(self, parser):
26 | parser.add_argument('source_file', nargs=1, type=str,
27 | help=SOURCE_FILE_HELP)
28 | parser.add_argument('--prefix', '-p', default=None,
29 | help="Prefix included in new files' names")
30 | parser.add_argument('--noinput', '-i', action='store_true', default=False,
31 | help="Do NOT prompt the user for input of any kind.")
32 | parser.add_argument('--post-process', action='store_true', default=False,
33 | help="Do post process collected files.")
34 | parser.add_argument('--replace', '-r', action='store_true', default=False,
35 | help="Delete file if already existing.")
36 | parser.add_argument('--dry-run', '-n', action='store_true', default=False,
37 | help="Do everything except modify the filesystem.")
38 |
39 | def handle(self, *args, **options):
40 | source_filename = options['source_file'][0]
41 | prefix = options['prefix']
42 |
43 | storage = get_storage_class(settings.STORAGE)(**settings.STORAGE_OPTIONS)
44 |
45 | if source_filename.startswith('file://'):
46 | source_filename = source_filename.replace('file://', '')
47 | source_file = storage.open(source_filename)
48 | elif re.match(r'^https?://.*$', source_filename):
49 | response = urlopen(source_filename)
50 | source_file = io.BytesIO()
51 | copyfileobj(response.fp, source_file)
52 | source_file.seek(0)
53 | else:
54 | source_file = source_filename
55 |
56 | if not options['noinput']:
57 | answer = input("Are you sure you want to continue? [Y/n]")
58 | if answer.lower().startswith('n'):
59 | self.stdout.write('Quitting')
60 | return
61 |
62 | self.stdout.write('Launch favicon generation and uploading')
63 | if options['dry_run']:
64 | self.stdout.write('No operation launched')
65 | else:
66 | generate(source_file, storage, prefix, options['replace'], settings.PRECOMPOSED_BGCOLOR)
67 |
68 | if options['post_process']:
69 | self.stdout.write('Launch post process')
70 | if options['dry_run']:
71 | self.stdout.write('No operation launched')
72 | else:
73 | storage.post_process()
74 |
--------------------------------------------------------------------------------
/favicon/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ZuluPro/django-super-favicon/a0e054915c582d56d79703d57754f8b50ad67672/favicon/migrations/__init__.py
--------------------------------------------------------------------------------
/favicon/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 |
3 | # Create your models here.
4 |
--------------------------------------------------------------------------------
/favicon/settings.py:
--------------------------------------------------------------------------------
1 | """Parameters for :mod:`favicon`."""
2 | from django.conf import settings
3 |
4 |
5 | STORAGE = getattr(settings, 'FAVICON_STORAGE', settings.STATICFILES_STORAGE)
6 | STORAGE_OPTIONS = getattr(settings, 'FAVICON_STORAGE_OPTIONS', {})
7 | PRECOMPOSED_BGCOLOR = getattr(settings, 'FAVICON_PRECOMPOSED_BG_COLOR', (255, 255, 255))
8 |
--------------------------------------------------------------------------------
/favicon/templates/favicon/favicon.html:
--------------------------------------------------------------------------------
1 | {% load favicon %}
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/favicon/templates/favicon/ieconfig.xml:
--------------------------------------------------------------------------------
1 | {% load favicon %}
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | #{{ tile_color }}
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/favicon/templatetags/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ZuluPro/django-super-favicon/a0e054915c582d56d79703d57754f8b50ad67672/favicon/templatetags/__init__.py
--------------------------------------------------------------------------------
/favicon/templatetags/favicon.py:
--------------------------------------------------------------------------------
1 | from django import template
2 | from django.template.loader import get_template
3 | from django.core.files.storage import get_storage_class
4 | from .. import settings
5 |
6 | register = template.Library()
7 |
8 |
9 | @register.simple_tag
10 | def get_favicons(prefix=None):
11 | """
12 | Generate HTML to include in headers for get all favicons url.
13 |
14 | :param prefix: Prefix of files' names
15 | :type prefix: str
16 | :return: HTML link and meta
17 | :rtype: str
18 | """
19 | return get_template('favicon/favicon.html').render({
20 | 'prefix': prefix
21 | })
22 |
23 |
24 | @register.simple_tag
25 | def favicon_url(filename, prefix=None):
26 | """
27 | Generate URL for find a single file. It uses :meth:`url()` of storage
28 | defined in ``settings.FAVICON_STORAGE``.
29 |
30 | :param filename: Filename
31 | :type filename: str
32 | :param prefix: Prefix of filename
33 | :type prefix: str
34 | :return: File's URL
35 | :rtype: str
36 | """
37 | storage = get_storage_class(settings.STORAGE)(**settings.STORAGE_OPTIONS)
38 | prefix = prefix or ''
39 | name = prefix + filename
40 | return storage.url(name)
41 |
--------------------------------------------------------------------------------
/favicon/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ZuluPro/django-super-favicon/a0e054915c582d56d79703d57754f8b50ad67672/favicon/tests/__init__.py
--------------------------------------------------------------------------------
/favicon/tests/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ZuluPro/django-super-favicon/a0e054915c582d56d79703d57754f8b50ad67672/favicon/tests/logo.png
--------------------------------------------------------------------------------
/favicon/tests/settings.py:
--------------------------------------------------------------------------------
1 | SECRET_KEY = '&qaeg(mBecauseitsmandatoryv@@n$if67ba-4e9&kk+j$$c+'
2 |
3 | DATABASES = {
4 | 'default': {
5 | 'ENGINE': 'django.db.backends.sqlite3',
6 | },
7 | }
8 |
9 | INSTALLED_APPS = [
10 | 'django.contrib.staticfiles',
11 | 'favicon',
12 | ]
13 |
14 | STATIC_URL = '/static/'
15 | STATIC_ROOT = '/tmp'
16 | FAVICON_STORAGE = 'favicon.tests.utils.FakeStorage'
17 |
--------------------------------------------------------------------------------
/favicon/tests/test_command.py:
--------------------------------------------------------------------------------
1 | from mock import patch, Mock
2 | from django.test import TestCase
3 | from django.core.management import execute_from_command_line
4 | from favicon.management.commands.generate_favicon import Command as Generate
5 | from favicon.management.commands.delete_favicon import Command as Delete
6 | from favicon.tests.utils import HANDLED_FILES, BASE_IMG, EXPECTED_FILES
7 | from favicon.utils import generate
8 | from favicon.tests.utils import FakeStorage
9 |
10 |
11 | @patch('favicon.management.commands.generate_favicon.input',
12 | return_value='Yes')
13 | class GenerateFaviconCommandTest(TestCase):
14 | def setUp(self):
15 | self.command = Generate()
16 |
17 | def tearDown(self):
18 | HANDLED_FILES.clean()
19 |
20 | def test_execute_from_command_line(self, *mocks):
21 | execute_from_command_line(['', 'generate_favicon', BASE_IMG])
22 | self.assertTrue(HANDLED_FILES['written_files'])
23 | for name, content in HANDLED_FILES['written_files'].items():
24 | self.assertIn(name, EXPECTED_FILES)
25 | self.assertTrue(content.size)
26 |
27 | @patch('favicon.tests.utils.FakeStorage.post_process')
28 | def test_post_process(self, *mocks):
29 | execute_from_command_line(['', 'generate_favicon', BASE_IMG,
30 | '--post-process'])
31 | self.assertTrue(HANDLED_FILES['written_files'])
32 | self.assertTrue(mocks[0].called)
33 |
34 | def test_no_input(self, *mocks):
35 | execute_from_command_line(['', 'generate_favicon', BASE_IMG,
36 | '--noinput'])
37 | self.assertTrue(HANDLED_FILES['written_files'])
38 | self.assertFalse(mocks[0].called)
39 |
40 | @patch('favicon.management.commands.generate_favicon.input',
41 | return_value='No')
42 | def test_dry_run(self, *mocks):
43 | execute_from_command_line(['', 'generate_favicon', BASE_IMG,
44 | '--dry-run'])
45 | self.assertFalse(HANDLED_FILES['written_files'])
46 |
47 | @patch('favicon.tests.utils.FakeStorage.post_process')
48 | def test_dry_run_post_process(self, *mocks):
49 | execute_from_command_line(['', 'generate_favicon', BASE_IMG,
50 | '--post-process', '--dry-run'])
51 | self.assertFalse(HANDLED_FILES['written_files'])
52 | self.assertFalse(mocks[0].called)
53 |
54 | def test_prefix(self, *mocks):
55 | prefix = 'foo/'
56 | expected_files = [prefix+fi for fi in EXPECTED_FILES]
57 | execute_from_command_line(['', 'generate_favicon', BASE_IMG,
58 | '--prefix=foo/'])
59 | self.assertTrue(HANDLED_FILES['written_files'])
60 | for name, content in HANDLED_FILES['written_files'].items():
61 | self.assertIn(name, expected_files)
62 | self.assertTrue(content.size)
63 |
64 | def test_source_file_from_storage(self, *mocks):
65 | HANDLED_FILES['written_files']['logo.png'] = open(BASE_IMG, 'rb')
66 | execute_from_command_line(['', 'generate_favicon', 'file://logo.png'])
67 | self.assertTrue(HANDLED_FILES['written_files'])
68 | for name, content in HANDLED_FILES['written_files'].items():
69 | if 'logo.png' == name:
70 | continue
71 | self.assertIn(name, EXPECTED_FILES)
72 | self.assertTrue(content.size)
73 |
74 | @patch('favicon.management.commands.generate_favicon.urlopen',
75 | return_value=Mock(fp=open(BASE_IMG, 'rb')))
76 | def test_source_file_from_http(self, *mocks):
77 | execute_from_command_line(['', 'generate_favicon',
78 | 'http://example.com/logo.png'])
79 | self.assertTrue(HANDLED_FILES['written_files'])
80 | for name, content in HANDLED_FILES['written_files'].items():
81 | self.assertIn(name, EXPECTED_FILES)
82 | self.assertTrue(content.size)
83 | self.assertTrue(mocks[0].called)
84 |
85 |
86 | @patch('favicon.management.commands.delete_favicon.input',
87 | return_value='Yes')
88 | class DeleteFaviconCommandTest(TestCase):
89 | def setUp(self):
90 | self.command = Delete()
91 | self.storage = FakeStorage()
92 | generate(BASE_IMG, self.storage)
93 |
94 | def tearDown(self):
95 | HANDLED_FILES.clean()
96 |
97 | def test_execute_from_command_line(self, *mocks):
98 | execute_from_command_line(['', 'delete_favicon'])
99 | self.assertTrue(HANDLED_FILES['deleted_files'])
100 |
101 | def test_dry_run(self, *mocks):
102 | execute_from_command_line(['', 'delete_favicon', '--dry-run'])
103 | self.assertFalse(HANDLED_FILES['deleted_files'])
104 |
105 | def test_no_input(self, *mocks):
106 | execute_from_command_line(['', 'delete_favicon', '--noinput'])
107 | self.assertTrue(HANDLED_FILES['deleted_files'])
108 | self.assertFalse(mocks[0].called)
109 |
110 | def test_prefix(self, *mocks):
111 | prefix = 'foo/'
112 | expected_files = [prefix+fi for fi in EXPECTED_FILES]
113 |
114 | generate(BASE_IMG, self.storage, prefix)
115 | execute_from_command_line(['', 'delete_favicon', '--prefix=foo/'])
116 |
117 | for name, content in HANDLED_FILES['written_files'].items():
118 | self.assertIn(name, EXPECTED_FILES)
119 | self.assertNotIn(name, expected_files)
120 | for name, content in HANDLED_FILES['deleted_files'].items():
121 | self.assertIn(name, expected_files)
122 | self.assertNotIn(name, EXPECTED_FILES)
123 |
--------------------------------------------------------------------------------
/favicon/tests/test_templatetags.py:
--------------------------------------------------------------------------------
1 | import re
2 | from django.test import TestCase
3 | from favicon.templatetags.favicon import get_favicons, favicon_url
4 | from favicon.tests.utils import HANDLED_FILES, BASE_IMG, FakeStorage, BASE_URL
5 | from favicon.utils import generate
6 |
7 | SRC_REG = re.compile(r'(?:href|content|src)="%s([^"]*)"' % BASE_URL)
8 |
9 |
10 | class GetFaviconsTest(TestCase):
11 | def setUp(self):
12 | self.storage = FakeStorage()
13 |
14 | def tearDown(self):
15 | HANDLED_FILES.clean()
16 |
17 | def test_get_favicons(self):
18 | generate(BASE_IMG, self.storage)
19 | html = get_favicons()
20 | urls = SRC_REG.findall(html)
21 | self.assertTrue(urls)
22 | for name in urls:
23 | self.assertTrue(self.storage.exists(name))
24 |
25 | def test_get_favicons_with_prefix(self):
26 | prefix = 'foo/'
27 | generate(BASE_IMG, self.storage, prefix)
28 | html = get_favicons(prefix)
29 | urls = SRC_REG.findall(html)
30 | self.assertTrue(urls)
31 | for name in urls:
32 | self.assertTrue(self.storage.exists(name))
33 |
34 |
35 | class FaviconUrlTest(TestCase):
36 | def test_favicon_url(self):
37 | url = favicon_url('foo.png')
38 | self.assertEqual(FakeStorage().url('foo.png'), url)
39 |
40 | def test_favicon_url_with_prefix(self):
41 | url = favicon_url('foo.png', 'bar/')
42 | self.assertEqual(FakeStorage().url('bar/foo.png'), url)
43 |
--------------------------------------------------------------------------------
/favicon/tests/test_utils.py:
--------------------------------------------------------------------------------
1 | import re
2 | from django.test import TestCase
3 | from PIL import Image
4 | from favicon.tests.utils import HANDLED_FILES, BASE_IMG, EXPECTED_FILES,\
5 | FakeStorage
6 | from favicon.utils import generate, delete, PNG_SIZES, WINDOWS_PNG_SIZES
7 |
8 | SRC_REG = re.compile(r'src="/static/([^"]*)"')
9 |
10 |
11 | class GenerateTest(TestCase):
12 | def setUp(self):
13 | self.storage = FakeStorage()
14 |
15 | def tearDown(self):
16 | HANDLED_FILES.clean()
17 |
18 | def test_generate(self):
19 | generate(BASE_IMG, self.storage)
20 | for name, content in HANDLED_FILES['written_files'].items():
21 | self.assertIn(name, EXPECTED_FILES)
22 | self.assertTrue(content.size)
23 | # Test ICO file
24 | ico = self.storage._open('favicon.ico')
25 | self.assertEqual(Image.open(ico).format, 'ICO')
26 | # Test PNG
27 | for size in PNG_SIZES:
28 | name = 'favicon-%d.png' % size
29 | self.assertTrue(self.storage.exists(name))
30 | png = self.storage._open(name)
31 | img = Image.open(png)
32 | self.assertEqual(img.format, 'PNG')
33 | self.assertEqual(img.size, (size, size))
34 | # Test Windows PNG
35 | for size, name in WINDOWS_PNG_SIZES:
36 | self.assertTrue(self.storage.exists(name))
37 | png = self.storage._open(name)
38 | img = Image.open(png)
39 | self.assertEqual(img.format, 'PNG')
40 | if size[0] != size[1] or size[0] > 440:
41 | continue
42 | self.assertEqual(img.size, size)
43 | # Test ieconfig.xml
44 | ieconfig = self.storage._open('ieconfig.xml').read()
45 | for name in SRC_REG.findall(ieconfig):
46 | self.assertTrue(self.storage.exists(name))
47 |
48 | def test_generate_with_prefix(self):
49 | prefix = 'foo/'
50 | expected_files = [prefix+fi for fi in EXPECTED_FILES]
51 |
52 | generate(BASE_IMG, self.storage, prefix)
53 | for name, content in HANDLED_FILES['written_files'].items():
54 | self.assertIn(name, expected_files)
55 | self.assertTrue(content.size)
56 |
57 |
58 | class DeleteTest(TestCase):
59 | def setUp(self):
60 | self.storage = FakeStorage()
61 | generate(BASE_IMG, self.storage)
62 |
63 | def tearDown(self):
64 | HANDLED_FILES.clean()
65 |
66 | def test_delete(self):
67 | delete(self.storage)
68 | self.assertFalse(HANDLED_FILES['written_files'])
69 | self.assertTrue(HANDLED_FILES['deleted_files'])
70 |
71 | def test_delete_not_existing(self):
72 | delete(self.storage)
73 | delete(self.storage)
74 | self.assertFalse(HANDLED_FILES['written_files'])
75 | self.assertTrue(HANDLED_FILES['deleted_files'])
76 |
77 | def test_delete_with_prefix(self):
78 | prefix = 'foo/'
79 | expected_files = [prefix+fi for fi in EXPECTED_FILES]
80 |
81 | generate(BASE_IMG, self.storage, prefix)
82 | delete(self.storage, prefix)
83 | for name, content in HANDLED_FILES['written_files'].items():
84 | self.assertIn(name, EXPECTED_FILES)
85 | self.assertNotIn(name, expected_files)
86 | for name, content in HANDLED_FILES['deleted_files'].items():
87 | self.assertIn(name, expected_files)
88 | self.assertNotIn(name, EXPECTED_FILES)
89 |
--------------------------------------------------------------------------------
/favicon/tests/utils.py:
--------------------------------------------------------------------------------
1 | import os
2 | from django.core.files.storage import Storage
3 |
4 | TEST_DIR = os.path.dirname(__file__)
5 | BASE_IMG = os.path.join(TEST_DIR, 'logo.png')
6 | BASE_URL = 'https://example.com/'
7 |
8 | EXPECTED_FILES = (
9 | 'favicon.ico',
10 | 'ieconfig.xml',
11 | 'smalltile.png', 'mediumtile.png', 'largetile.png', 'widetile.png',
12 | 'favicon-32.png', 'favicon-57.png', 'favicon-76.png', 'favicon-96.png',
13 | 'favicon-120.png', 'favicon-128.png', 'favicon-144.png', 'favicon-152.png',
14 | 'favicon-180.png', 'favicon-195.png', 'favicon-196.png', 'favicon-228.png',
15 | 'favicon-precomposed-152.png',
16 | )
17 |
18 |
19 | class handled_files(dict):
20 | """
21 | Dict for gather information about fake storage and clean between tests.
22 | You should use the constant instance ``HANDLED_FILES`` and clean it
23 | before tests.
24 | """
25 | def __init__(self):
26 | super(handled_files, self).__init__()
27 | self.clean()
28 |
29 | def clean(self):
30 | self['written_files'] = {}
31 | self['deleted_files'] = {}
32 | HANDLED_FILES = handled_files()
33 |
34 |
35 | class FakeStorage(Storage):
36 | def _save(self, name, content):
37 | HANDLED_FILES['written_files'][name] = content
38 |
39 | def _open(self, name, mode='rb'):
40 | HANDLED_FILES['written_files'][name].seek(0)
41 | return HANDLED_FILES['written_files'][name]
42 |
43 | def exists(self, name):
44 | return name in HANDLED_FILES['written_files'].keys()
45 |
46 | def delete(self, name):
47 | fi = HANDLED_FILES['written_files'].pop(name, None)
48 | if fi:
49 | HANDLED_FILES['deleted_files'][name] = fi
50 |
51 | def post_process(self):
52 | pass
53 |
54 | def url(self, name):
55 | return BASE_URL + name
56 |
--------------------------------------------------------------------------------
/favicon/utils.py:
--------------------------------------------------------------------------------
1 | """Utilities for :mod:`favicon`."""
2 | import io
3 | from django.template.loader import get_template
4 | from django.core.files import File
5 | from PIL import Image
6 |
7 | ICO_SIZES = [(16, 16), (32, 32), (48, 48), (64, 64)]
8 | PNG_SIZES = (32, 57, 76, 96, 120, 128, 144, 152, 180, 195, 196, 228)
9 | WINDOWS_PNG_SIZES = (
10 | ((128, 128), 'smalltile.png'),
11 | ((270, 270), 'mediumtile.png'),
12 | ((558, 270), 'widetile.png'),
13 | ((558, 558), 'largetile.png'),
14 | )
15 | FILLED_SIZES = (152,)
16 |
17 | try:
18 | RESAMPLE = Image.ANTIALIAS
19 | except AttributeError:
20 | RESAMPLE = Image.LANCZOS
21 |
22 | def alpha_to_color(image, color):
23 | color = color or (255, 255, 255)
24 | bg = Image.new('RGBA', image.size, color)
25 | try:
26 | bg.paste(image, image)
27 | except ValueError:
28 | return image
29 | return color and bg or image
30 |
31 | def generate(source_file, storage, prefix=None, replace=False, fill=None):
32 | """
33 | Creates favicons from a source file and upload into storage.
34 | This also create the ieconfig.xml file.
35 |
36 | :param source_file: File to use as string (local path) or filelike object
37 | :type source_file: str or file
38 | :param storage: Storage where upload files
39 | :type storage: :class:`django.core.files.storage.Storage`
40 | :param prefix: Prefix included in new files' names
41 | :type prefix: str
42 | :param replace: Delete file is already existing.
43 | :type replace: bool
44 | :param fill: Background color for generated precomposed-* icons
45 | :type fill: tuple of length 3, as returned by PIL.ImageColor.getrgb(color)
46 | """
47 | prefix = prefix or ''
48 |
49 | def write_file(output_file, name, replace=False):
50 | """Upload to storage."""
51 | name = prefix + name
52 | if storage.exists(name):
53 | if replace:
54 | storage.delete(name)
55 | else:
56 | return
57 | content = File(output_file, name)
58 | storage._save(name, content)
59 |
60 | def save_png(img, output_name, size):
61 | img.thumbnail(size=size, resample=RESAMPLE)
62 | output_file = io.BytesIO()
63 | img.save(output_file, format='PNG')
64 | write_file(output_file, output_name)
65 | # Save ICO
66 | img = Image.open(source_file)
67 | output_file = io.BytesIO()
68 | img.save(fp=output_file, format='ICO', sizes=ICO_SIZES)
69 | write_file(output_file, 'favicon.ico')
70 | # Save PNG
71 | for size in PNG_SIZES:
72 | img = Image.open(source_file)
73 | save_png(img, 'favicon-%s.png' % size, (size, size))
74 | for size, output_name in WINDOWS_PNG_SIZES:
75 | img = Image.open(source_file)
76 | save_png(img, output_name, size)
77 | for size in FILLED_SIZES:
78 | img = alpha_to_color(Image.open(source_file), fill)
79 | save_png(img, 'favicon-precomposed-%s.png' % size, (size, size))
80 | # Create ieconfig.xml
81 | output_name = 'ieconfig.xml'
82 | output_file = io.StringIO()
83 | template = get_template('favicon/ieconfig.xml')
84 | output_content = template.render({'tile_color': 'FFFFFF'})
85 | output_file.write(output_content)
86 | write_file(output_file, 'ieconfig.xml')
87 |
88 |
89 | def delete(storage, prefix=None):
90 | """
91 | Delete favicons from storage.
92 |
93 | :param storage: Storage where delete files
94 | :type storage: :class:`django.core.files.storage.Storage`
95 | :param prefix: Prefix included in files' names
96 | :type prefix: str
97 | """
98 | prefix = prefix or ''
99 |
100 | def delete_file(name):
101 | name = prefix + name
102 | storage.delete(name)
103 |
104 | delete_file('favicon.ico')
105 | for size in PNG_SIZES:
106 | name = 'favicon-%s.png' % size
107 | delete_file(name)
108 | for _, name in WINDOWS_PNG_SIZES:
109 | delete_file(name)
110 | for size in FILLED_SIZES:
111 | name = 'favicon-precomposed-%s.png' % size
112 | delete_file(name)
113 | delete_file('ieconfig.xml')
114 |
--------------------------------------------------------------------------------
/favicon/views.py:
--------------------------------------------------------------------------------
1 | from django.shortcuts import render
2 |
3 | # Create your views here.
4 |
--------------------------------------------------------------------------------
/requirements-tests.txt:
--------------------------------------------------------------------------------
1 | coverage<4
2 | mock
3 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | Pillow
2 |
--------------------------------------------------------------------------------
/runtests.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import os
3 | import sys
4 |
5 | import django
6 | from django.conf import settings
7 | from django.test.utils import get_runner
8 |
9 |
10 | def main():
11 | from django.core.management import execute_from_command_line
12 | execute_from_command_line(sys.argv)
13 |
14 | if __name__ == "__main__":
15 | os.environ['DJANGO_SETTINGS_MODULE'] = 'favicon.tests.settings'
16 | if len(sys.argv) == 1:
17 | django.setup()
18 | TestRunner = get_runner(settings)
19 | test_runner = TestRunner()
20 | failures = test_runner.run_tests(["favicon.tests"])
21 | sys.exit(bool(failures))
22 | main()
23 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [flake8]
2 | max-line-length = 99
3 | exclude = tests,settings
4 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | from setuptools import setup, find_packages
3 | import favicon
4 |
5 |
6 | def read_file(name):
7 | with open(name) as fd:
8 | return fd.read()
9 |
10 | keywords = ['django', 'web', 'favicon', 'html']
11 |
12 | setup(
13 | name='django-super-favicon',
14 | version=favicon.__version__,
15 | description=favicon.__doc__,
16 | long_description=read_file('README.rst'),
17 | author=favicon.__author__,
18 | author_email=favicon.__email__,
19 | install_requires=read_file('requirements.txt'),
20 | license='BSD',
21 | url=favicon.__url__,
22 | keywords=keywords,
23 | packages=find_packages(exclude=[]),
24 | include_package_data=True,
25 | test_suite='runtests.main',
26 | tests_require=read_file('requirements-tests.txt'),
27 | classifiers=[
28 | 'Development Status :: 4 - Beta',
29 | 'Environment :: Web Environment',
30 | 'Environment :: Console',
31 | 'Framework :: Django',
32 | 'Intended Audience :: Developers',
33 | 'License :: OSI Approved :: BSD License',
34 | 'Natural Language :: English',
35 | 'Operating System :: OS Independent',
36 | 'Programming Language :: Python',
37 | 'Programming Language :: Python :: 2',
38 | 'Programming Language :: Python :: 2.7',
39 | 'Programming Language :: Python :: 3',
40 | 'Programming Language :: Python :: 3.3',
41 | 'Programming Language :: Python :: 3.4',
42 | 'Programming Language :: Python :: 3.5',
43 | ],
44 | )
45 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | envlist = py{3.6,3.7,3.8,3.9}-django{3.0},lint
3 |
4 | [testenv]
5 | passenv = *
6 | basepython =
7 | py3.6: python3.6
8 | py3.7: python3.7
9 | py3.8: python3.8
10 | py3.9: python3.9
11 | deps =
12 | -rrequirements-tests.txt
13 | commands = {posargs:coverage run runtests.py}
14 |
15 | [testenv:lint]
16 | basepython = python
17 | deps =
18 | prospector
19 | commands = prospector favicon -0
20 |
--------------------------------------------------------------------------------