├── .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 | --------------------------------------------------------------------------------