├── .github └── workflows │ ├── build.yml │ ├── lint.yml │ └── pypi.yml ├── .gitignore ├── .yamllint ├── CHANGELOG.md ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── docs └── images │ └── screenshot.png ├── pyproject.toml ├── requirements-dev.txt ├── setup.py ├── tests ├── __init__.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── settings.py └── tests.py └── treasuremap ├── __init__.py ├── apps.py ├── backends ├── __init__.py ├── base.py ├── google.py └── yandex.py ├── fields.py ├── forms.py ├── models.py ├── static └── treasuremap │ └── default │ └── js │ ├── jquery.treasuremap-google.js │ └── jquery.treasuremap-yandex.js ├── templates └── treasuremap │ └── widgets │ └── map.html ├── utils.py └── widgets.py /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "build" 3 | 4 | on: # yamllint disable-line rule:truthy 5 | pull_request: 6 | push: 7 | branches: master 8 | 9 | jobs: 10 | build: 11 | name: Python ${{ matrix.python-version }} | Django ${{ matrix.django-version}} | Ubuntu 12 | runs-on: ubuntu-20.04 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | include: 17 | - python-version: 3.6 18 | django-version: "1.11.*" 19 | - python-version: 3.6 20 | django-version: "2.0.*" 21 | - python-version: 3.6 22 | django-version: "2.1.*" 23 | - python-version: 3.6 24 | django-version: "2.2.*" 25 | - python-version: 3.6 26 | django-version: "3.0.*" 27 | - python-version: 3.6 28 | django-version: "3.1.*" 29 | - python-version: 3.6 30 | django-version: "3.2.*" 31 | 32 | - python-version: 3.7 33 | django-version: "2.0.*" 34 | - python-version: 3.7 35 | django-version: "2.1.*" 36 | - python-version: 3.7 37 | django-version: "2.2.*" 38 | - python-version: 3.7 39 | django-version: "3.0.*" 40 | - python-version: 3.7 41 | django-version: "3.1.*" 42 | - python-version: 3.7 43 | django-version: "3.2.*" 44 | 45 | - python-version: 3.8 46 | django-version: "2.2.*" 47 | - python-version: 3.8 48 | django-version: "3.0.*" 49 | - python-version: 3.8 50 | django-version: "3.1.*" 51 | - python-version: 3.8 52 | django-version: "3.2.*" 53 | - python-version: 3.8 54 | django-version: "4.0.*" 55 | 56 | - python-version: 3.9 57 | django-version: "3.0.*" 58 | - python-version: 3.9 59 | django-version: "3.1.*" 60 | - python-version: 3.9 61 | django-version: "3.2.*" 62 | - python-version: 3.9 63 | django-version: "4.0.*" 64 | - python-version: 3.9 65 | django-version: "4.1.*" 66 | 67 | - python-version: "3.10" 68 | django-version: "3.0.*" 69 | - python-version: "3.10" 70 | django-version: "3.1.*" 71 | - python-version: "3.10" 72 | django-version: "3.2.*" 73 | - python-version: "3.10" 74 | django-version: "4.0.*" 75 | - python-version: "3.10" 76 | django-version: "4.1.*" 77 | coverage: true 78 | 79 | steps: 80 | - name: Checkout 81 | uses: actions/checkout@v3 82 | 83 | - name: Setup Python ${{ matrix.python-version }} 84 | uses: actions/setup-python@v4 85 | with: 86 | python-version: ${{ matrix.python-version }} 87 | 88 | - name: Install Django ${{ matrix.django-version }} 89 | run: | 90 | pip install Django==${{ matrix.django-version }} 91 | pip install coverage 92 | 93 | - name: Run tests 94 | run: | 95 | make test 96 | 97 | - if: ${{ matrix.coverage }} 98 | run: | 99 | make coverage 100 | 101 | - if: ${{ matrix.coverage }} 102 | name: Upload coverage to Codecov 103 | uses: codecov/codecov-action@v3 104 | with: 105 | token: ${{ secrets.CODECOV_TOKEN }} 106 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "Lint" 3 | 4 | on: # yamllint disable-line rule:truthy 5 | pull_request: 6 | push: 7 | branches: master 8 | 9 | jobs: 10 | lint: 11 | name: Lint 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v3 17 | 18 | - name: Setup Python 3.9 19 | uses: actions/setup-python@v4 20 | with: 21 | python-version: 3.9 22 | 23 | - name: Upgrade Setuptools 24 | run: pip install --upgrade setuptools wheel 25 | 26 | - name: Install requirements 27 | run: pip install -r requirements-dev.txt 28 | 29 | - name: Run lint 30 | run: make lint 31 | -------------------------------------------------------------------------------- /.github/workflows/pypi.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "PyPI Release" 3 | 4 | on: # yamllint disable-line rule:truthy 5 | push: 6 | tags: 7 | - 'v*' 8 | 9 | jobs: 10 | publish: 11 | name: PyPI Release 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v3 17 | 18 | - name: Setup Python 3.9 19 | uses: actions/setup-python@v4 20 | with: 21 | python-version: 3.9 22 | 23 | - name: Upgrade Setuptools 24 | run: pip install --upgrade setuptools wheel 25 | 26 | - name: Build Distribution 27 | run: python setup.py sdist bdist_wheel --universal 28 | 29 | - name: Publish to PyPI 30 | uses: pypa/gh-action-pypi-publish@master 31 | with: 32 | user: __token__ 33 | password: ${{ secrets.pypi_password }} 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.pot 50 | 51 | # Django stuff: 52 | *.log 53 | local_settings.py 54 | 55 | # Flask stuff: 56 | instance/ 57 | .webassets-cache 58 | 59 | # Scrapy stuff: 60 | .scrapy 61 | 62 | # Sphinx documentation 63 | docs/_build/ 64 | 65 | # PyBuilder 66 | target/ 67 | 68 | # IPython Notebook 69 | .ipynb_checkpoints 70 | 71 | # pyenv 72 | .python-version 73 | 74 | # celery beat schedule file 75 | celerybeat-schedule 76 | 77 | # dotenv 78 | .env 79 | 80 | # virtualenv 81 | venv/ 82 | ENV/ 83 | 84 | # Spyder project settings 85 | .spyderproject 86 | 87 | # Rope project settings 88 | .ropeproject 89 | 90 | # Translate 91 | .tx 92 | -------------------------------------------------------------------------------- /.yamllint: -------------------------------------------------------------------------------- 1 | --- 2 | extends: default 3 | 4 | rules: 5 | line-length: disable 6 | 7 | ignore: | 8 | *.venv/ 9 | *.mypy_cache/ 10 | *.eggs/ 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [Unreleased] 2 | 3 | ## [0.3.4] - 2020-08-10 4 | ### Added 5 | - Compatibility Django 3.2 6 | 7 | ## [0.3.3] - 2020-08-10 8 | ### Added 9 | - Compatibility Django 3.1 10 | 11 | ## [0.3.2] - 2019-12-05 12 | ### Added 13 | - Compatibility Django 3.0 14 | 15 | ## [0.3.1] - 2019-08-20 16 | ### Fixed 17 | - MANIFEST.in paths 18 | 19 | ## [0.3.0] - 2019-08-20 20 | ### Added 21 | - Compatibility Django 2.2 22 | 23 | ### Deleted 24 | - Support Django < 1.11 25 | 26 | ## [0.2.7] - 2018-10-15 27 | ### Fixed 28 | - Fix context from_db_value 29 | 30 | ## [0.2.6] - 2018-04-23 31 | ### Added 32 | - Django 2.0 support 33 | 34 | ## [0.2.5] - 2017-05-30 35 | ### Added 36 | - Django 1.11 support 37 | 38 | ## [0.2.4] - 2016-01-02 39 | ### Added 40 | - Django 1.11 support 41 | 42 | ## [0.2.3] - 2015-11-12 43 | ### Added 44 | - Test code 45 | 46 | ### Deleted 47 | - LatLong as list 48 | - Required settings 49 | 50 | 51 | [Unreleased]: https://github.com/silentsokolov/django-treasuremap/compare/v0.3.4...HEAD 52 | [0.3.4]: https://github.com/silentsokolov/django-treasuremap/compare/v0.3.3...v0.3.4 53 | [0.3.3]: https://github.com/silentsokolov/django-treasuremap/compare/v0.3.2...v0.3.3 54 | [0.3.2]: https://github.com/silentsokolov/django-treasuremap/compare/v0.3.1...v0.3.2 55 | [0.3.1]: https://github.com/silentsokolov/django-treasuremap/compare/v0.3.0...v0.3.1 56 | [0.3.0]: https://github.com/silentsokolov/django-treasuremap/compare/v0.2.7...v0.3.0 57 | [0.2.7]: https://github.com/silentsokolov/django-treasuremap/compare/v0.2.6...v0.2.7 58 | [0.2.6]: https://github.com/silentsokolov/django-treasuremap/compare/v0.2.5...v0.2.6 59 | [0.2.5]: https://github.com/silentsokolov/django-treasuremap/compare/v0.2.4...v0.2.5 60 | [0.2.4]: https://github.com/silentsokolov/django-treasuremap/compare/v0.2.3...v0.2.4 61 | [0.2.3]: https://github.com/silentsokolov/django-treasuremap/compare/v0.2...v0.2.3 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Dmitriy Sokolov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE CHANGELOG.md README.rst 2 | recursive-include tests/* * 3 | recursive-include treasuremap/static *.js *.css *.png *.eot *.svg *.ttf *.woff 4 | recursive-include treasuremap/templates *.html 5 | recursive-include treasuremap/locale *.mo 6 | global-exclude __pycache__ 7 | global-exclude *.py[co] -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: check-black check-isort check-pylint static-analysis test sdist wheel release pre-release clean 2 | 3 | PATH_DADMIN := $(shell which django-admin) 4 | 5 | sdist: 6 | python setup.py sdist 7 | 8 | wheel: 9 | python setup.py bdist_wheel --universal 10 | 11 | release: clean sdist wheel 12 | twine upload dist/* 13 | 14 | pre-release: sdist wheel 15 | twine upload --repository-url https://test.pypi.org/legacy/ dist/* 16 | 17 | clean: 18 | find . | grep -E '(__pycache__|\.pyc|\.pyo$)' | xargs rm -rf 19 | rm -rf build 20 | rm -rf dist 21 | rm -rf *.egg-info 22 | 23 | check-black: 24 | @echo "--> Running black checks" 25 | @black --check --diff . 26 | 27 | check-isort: 28 | @echo "--> Running isort checks" 29 | @isort --check-only . 30 | 31 | check-pylint: 32 | @echo "--> Running pylint checks" 33 | @pylint `git ls-files '*.py'` 34 | 35 | check-yamllint: 36 | @echo "--> Running yamllint checks" 37 | @yamllint . 38 | 39 | lint: check-black check-isort check-pylint check-yamllint 40 | 41 | # Format code 42 | .PHONY: fmt 43 | 44 | fmt: 45 | @echo "--> Running isort" 46 | @isort . 47 | @echo "--> Running black" 48 | @black . 49 | 50 | # Test 51 | .PHONY: test 52 | 53 | test: 54 | @echo "--> Running tests" 55 | PYTHONWARNINGS=all PYTHONPATH=".:tests:${PYTHONPATH}" django-admin test --settings=tests.settings 56 | 57 | coverage: 58 | @echo "--> Running coverage" 59 | PYTHONWARNINGS=all PYTHONPATH=".:tests:${PYTHONPATH}" coverage run --source='.' $(PATH_DADMIN) test --settings=tests.settings 60 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://github.com/silentsokolov/django-treasuremap/workflows/build/badge.svg?branch=master 2 | :target: https://github.com/silentsokolov/django-treasuremap/actions?query=workflow%3Abuild+branch%3Amaster 3 | 4 | .. image:: https://codecov.io/gh/silentsokolov/django-treasuremap/branch/master/graph/badge.svg 5 | :target: https://codecov.io/gh/silentsokolov/django-treasuremap 6 | 7 | 8 | django-treasuremap 9 | ================== 10 | 11 | django-treasuremap app, makes it easy to store and display the location on the map using different providers (Google, Yandex, etc). 12 | 13 | 14 | Requirements 15 | ------------ 16 | 17 | * Python 2.7+ or Python 3.4+ 18 | * Django 1.11+ 19 | 20 | 21 | Installation 22 | ------------ 23 | 24 | Use your favorite Python package manager to install the app from PyPI, e.g. 25 | 26 | Example: 27 | 28 | ``pip install django-treasuremap`` 29 | 30 | 31 | Add ``treasuremap`` to ``INSTALLED_APPS``: 32 | 33 | Example: 34 | 35 | .. code:: python 36 | 37 | INSTALLED_APPS = ( 38 | ... 39 | 'treasuremap', 40 | ... 41 | ) 42 | 43 | 44 | Configuration 45 | ------------- 46 | 47 | Within your ``settings.py``, you’ll need to add a setting (which backend to use, etc). 48 | 49 | Example: 50 | 51 | .. code:: python 52 | 53 | TREASURE_MAP = { 54 | 'BACKEND': 'treasuremap.backends.google.GoogleMapBackend', 55 | 'API_KEY': 'Your API key', 56 | 'SIZE': (400, 600), 57 | 'MAP_OPTIONS': { 58 | 'zoom': 5 59 | } 60 | } 61 | 62 | 63 | Example usage 64 | ------------- 65 | 66 | In models 67 | ~~~~~~~~~ 68 | 69 | .. code:: python 70 | 71 | from django.db import models 72 | from treasuremap.fields import LatLongField 73 | 74 | class Post(models.Model): 75 | name = models.CharField(max_length=100) 76 | point = LatLongField(blank=True) 77 | 78 | 79 | In admin 80 | ~~~~~~~~~ 81 | 82 | .. code:: python 83 | 84 | from django.contrib import admin 85 | from treasuremap.widgets import AdminMapWidget 86 | 87 | from .models import Post 88 | 89 | @admin.register(Post) 90 | class PostAdmin(admin.ModelAdmin): 91 | def formfield_for_dbfield(self, db_field, **kwargs): 92 | if db_field.name == 'point': 93 | kwargs['widget'] = AdminMapWidget() 94 | return super(PostAdmin,self).formfield_for_dbfield(db_field,**kwargs) 95 | 96 | 97 | In forms 98 | ~~~~~~~~ 99 | 100 | .. code:: python 101 | 102 | from django import forms 103 | from treasuremap.forms import LatLongField 104 | 105 | class PostForm(models.Model): 106 | point = LatLongField() 107 | 108 | 109 | .. code:: html 110 | 111 | 112 | ... 113 | 114 | 115 | ... 116 | 117 | 118 |
119 | {{ form.media }} 120 | {% csrf_token %} 121 | {{ form.as_p }} 122 |
123 | 124 | 125 | Depending on what backend you are using, the correct widget will be displayed 126 | with a marker at the currently position (jQuery is required). 127 | 128 | .. image:: https://raw.githubusercontent.com/silentsokolov/django-treasuremap/master/docs/images/screenshot.png 129 | 130 | 131 | Settings 132 | -------- 133 | 134 | Support map: 135 | ~~~~~~~~~~~~ 136 | 137 | - Google map ``treasuremap.backends.google.GoogleMapBackend`` 138 | - Yandex map ``treasuremap.backends.yandex.YandexMapBackend`` 139 | 140 | 141 | Other settings: 142 | ~~~~~~~~~~~~~~~ 143 | 144 | - ``API_KEY`` - if need, default ``None`` 145 | - ``SIZE`` - tuple with the size of the map, default ``(400, 400)`` 146 | - ``ADMIN_SIZE`` - tuple with the size of the map on the admin panel, default ``(400, 400)`` 147 | - ``ONLY_MAP`` - hide field lat/long, default ``True`` 148 | - ``MAP_OPTIONS`` - dict, used to initialize the map, default ``{'latitude': 51.562519, 'longitude': -1.603156, 'zoom': 5}``. ``latitude`` and ``longitude`` is required, do not use other "LatLong Object". 149 | -------------------------------------------------------------------------------- /docs/images/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silentsokolov/django-treasuremap/9f047a55860368393bdd86ef42e61fd8c0e1ee74/docs/images/screenshot.png -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 100 3 | target-version = ['py38', 'py39'] 4 | exclude = '(\.eggs|\.git|\.mypy_cache|\.venv|venv|env|_build|build|build|dist|eggs)' 5 | 6 | [tool.isort] 7 | line_length = 100 8 | profile = "black" 9 | use_parentheses = true 10 | skip = '.eggs/,.mypy_cache/,.venv/,venv/,env/,eggs/' 11 | 12 | [tool.pylint] 13 | [tool.pylint.master] 14 | py-version = 3.9 15 | 16 | [tool.pylint.messages-control] 17 | disable=[ 18 | 'C', 19 | 'R', 20 | 21 | # Redundant with mypy 22 | 'typecheck', 23 | 24 | # There are many places where we want to catch a maximally generic exception. 25 | 'bare-except', 26 | 'broad-except', 27 | 28 | # Pylint is by default very strict on logging string interpolations, but the 29 | # (performance-motivated) rules do not make sense for infrequent log messages (like error reports) 30 | # and make messages less readable. 31 | 'logging-fstring-interpolation', 32 | 'logging-format-interpolation', 33 | 'logging-not-lazy', 34 | 'too-many-arguments', 35 | 'duplicate-code', 36 | ] 37 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | Django>=4.1 2 | black==22.3.0 3 | isort==5.10.1 4 | pylint==2.14.4 5 | yamllint==1.26.3 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | import re 6 | from os.path import dirname, join 7 | 8 | from setuptools import setup 9 | 10 | 11 | def get_version(package): 12 | init_py = open(os.path.join(package, "__init__.py"), encoding="utf-8").read() 13 | return re.search("__version__ = ['\"]([^'\"]+)['\"]", init_py).group(1) 14 | 15 | 16 | def get_packages(package): 17 | return [ 18 | dirpath 19 | for dirpath, dirnames, filenames in os.walk(package) 20 | if os.path.exists(os.path.join(dirpath, "__init__.py")) 21 | ] 22 | 23 | 24 | def get_package_data(package): 25 | walk = [ 26 | (dirpath.replace(package + os.sep, "", 1), filenames) 27 | for dirpath, dirnames, filenames in os.walk(package) 28 | if not os.path.exists(os.path.join(dirpath, "__init__.py")) 29 | ] 30 | 31 | filepaths = [] 32 | for base, filenames in walk: 33 | filepaths.extend([os.path.join(base, filename) for filename in filenames]) 34 | return {package: filepaths} 35 | 36 | 37 | setup( 38 | name="django-treasuremap", 39 | version=get_version("treasuremap"), 40 | url="https://github.com/silentsokolov/django-treasuremap", 41 | license="MIT", 42 | description="django-treasuremap app, makes it easy to store and display " 43 | "the location on the map using different providers (Google, Yandex).", 44 | long_description=open(join(dirname(__file__), "README.rst"), encoding="utf-8").read(), 45 | author="Dmitriy Sokolov", 46 | author_email="silentsokolov@gmail.com", 47 | packages=get_packages("treasuremap"), 48 | package_data=get_package_data("treasuremap"), 49 | include_package_data=True, 50 | install_requires=[], 51 | python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", 52 | zip_safe=False, 53 | platforms="any", 54 | classifiers=[ 55 | "Development Status :: 5 - Production/Stable", 56 | "Environment :: Web Environment", 57 | "Framework :: Django", 58 | "Intended Audience :: Developers", 59 | "License :: OSI Approved :: MIT License", 60 | "Operating System :: OS Independent", 61 | "Programming Language :: Python", 62 | "Programming Language :: Python :: 2", 63 | "Programming Language :: Python :: 2.7", 64 | "Programming Language :: Python :: 3", 65 | "Programming Language :: Python :: 3.4", 66 | "Programming Language :: Python :: 3.5", 67 | "Programming Language :: Python :: 3.6", 68 | "Programming Language :: Python :: 3.7", 69 | "Programming Language :: Python :: 3.8", 70 | "Programming Language :: Python :: 3.9", 71 | "Topic :: Utilities", 72 | ], 73 | ) 74 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silentsokolov/django-treasuremap/9f047a55860368393bdd86ef42e61fd8c0e1ee74/tests/__init__.py -------------------------------------------------------------------------------- /tests/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.6 on 2020-06-04 09:00 2 | 3 | from django.db import migrations, models 4 | 5 | import treasuremap.fields 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name="MyModel", 17 | fields=[ 18 | ( 19 | "id", 20 | models.AutoField( 21 | auto_created=True, primary_key=True, serialize=False, verbose_name="ID" 22 | ), 23 | ), 24 | ("empty_point", treasuremap.fields.LatLongField(max_length=24)), 25 | ( 26 | "null_point", 27 | treasuremap.fields.LatLongField(blank=True, max_length=24, null=True), 28 | ), 29 | ( 30 | "default_point", 31 | treasuremap.fields.LatLongField( 32 | default=treasuremap.fields.LatLong(33.000000, 44.000000), max_length=24 33 | ), 34 | ), 35 | ], 36 | ), 37 | ] 38 | -------------------------------------------------------------------------------- /tests/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silentsokolov/django-treasuremap/9f047a55860368393bdd86ef42e61fd8c0e1ee74/tests/migrations/__init__.py -------------------------------------------------------------------------------- /tests/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals 4 | 5 | from django.db import models 6 | 7 | from treasuremap import fields 8 | 9 | 10 | class MyModel(models.Model): 11 | empty_point = fields.LatLongField() 12 | null_point = fields.LatLongField(blank=True, null=True) 13 | default_point = fields.LatLongField(default=fields.LatLong(33, 44)) 14 | -------------------------------------------------------------------------------- /tests/settings.py: -------------------------------------------------------------------------------- 1 | SECRET_KEY = "testkey" 2 | 3 | DATABASES = { 4 | "default": { 5 | "ENGINE": "django.db.backends.sqlite3", 6 | "NAME": ":memory:", 7 | } 8 | } 9 | 10 | INSTALLED_APPS = ( 11 | "django.contrib.auth", 12 | "django.contrib.contenttypes", 13 | "django.contrib.sites", 14 | "django.contrib.admin", 15 | "django.contrib.sessions", 16 | "django.contrib.messages", 17 | "treasuremap", 18 | "tests", 19 | ) 20 | 21 | TEST_RUNNER = "django.test.runner.DiscoverRunner" 22 | 23 | USE_TZ = True 24 | TIME_ZONE = "UTC" 25 | 26 | SITE_ID = 1 27 | 28 | STATIC_URL = "/static/" 29 | 30 | TEMPLATES = [ 31 | { 32 | "BACKEND": "django.template.backends.django.DjangoTemplates", 33 | "APP_DIRS": True, 34 | "OPTIONS": { 35 | "debug": True, 36 | "context_processors": [ 37 | "django.contrib.auth.context_processors.auth", 38 | "django.contrib.messages.context_processors.messages", 39 | "django.template.context_processors.request", 40 | ], 41 | }, 42 | }, 43 | ] 44 | 45 | MIDDLEWARE = ( 46 | "django.middleware.common.CommonMiddleware", 47 | "django.contrib.sessions.middleware.SessionMiddleware", 48 | "django.contrib.auth.middleware.AuthenticationMiddleware", 49 | "django.contrib.messages.middleware.MessageMiddleware", 50 | ) 51 | 52 | PASSWORD_HASHERS = ("django.contrib.auth.hashers.MD5PasswordHasher",) 53 | 54 | 55 | TREASURE_MAP = { 56 | "BACKEND": "treasuremap.backends.google.GoogleMapBackend", 57 | } 58 | 59 | DEFAULT_AUTO_FIELD = "django.db.models.AutoField" 60 | -------------------------------------------------------------------------------- /tests/tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals 4 | 5 | from decimal import Decimal, InvalidOperation 6 | 7 | from django.core.exceptions import ImproperlyConfigured, ValidationError 8 | from django.forms.renderers import get_default_renderer 9 | from django.test import TestCase 10 | from django.test.utils import override_settings 11 | 12 | from treasuremap.backends.base import BaseMapBackend 13 | from treasuremap.backends.google import GoogleMapBackend 14 | from treasuremap.backends.yandex import YandexMapBackend 15 | from treasuremap.fields import LatLong, LatLongField 16 | from treasuremap.forms import LatLongField as FormLatLongField 17 | from treasuremap.utils import get_backend, import_class 18 | from treasuremap.widgets import AdminMapWidget, MapWidget 19 | 20 | from .models import MyModel 21 | 22 | 23 | class LatLongObjectTestCase(TestCase): 24 | def test_create_empty_latlong(self): 25 | latlong = LatLong() 26 | self.assertEqual(latlong.latitude, 0.0) 27 | self.assertEqual(latlong.longitude, 0.0) 28 | 29 | def test_create_latlog_with_float(self): 30 | latlong = LatLong(latitude=33.300, longitude=44.440) 31 | self.assertEqual(latlong.latitude, Decimal(33.300)) 32 | self.assertEqual(latlong.longitude, Decimal(44.440)) 33 | 34 | def test_create_latlog_with_string(self): 35 | latlong = LatLong(latitude="33.300", longitude="44.440") 36 | self.assertEqual(latlong.latitude, Decimal("33.300")) 37 | self.assertEqual(latlong.longitude, Decimal("44.440")) 38 | 39 | def test_create_latlog_with_invalid_value(self): 40 | self.assertRaises(InvalidOperation, LatLong, "not int", 44.440) 41 | 42 | def test_latlog_object_str(self): 43 | latlong = LatLong(latitude=33.300, longitude=44.440) 44 | self.assertEqual(str(latlong), "33.300000;44.440000") 45 | 46 | def test_latlog_object_repr(self): 47 | latlong = LatLong(latitude=33.300, longitude=44.440) 48 | self.assertEqual(repr(latlong), "LatLong(33.300000, 44.440000)") 49 | 50 | def test_latlog_object_eq(self): 51 | self.assertEqual( 52 | LatLong(latitude=33.300, longitude=44.440), LatLong(latitude=33.300, longitude=44.440) 53 | ) 54 | 55 | def test_latlog_object_ne(self): 56 | self.assertNotEqual( 57 | LatLong(latitude=33.300, longitude=44.441), LatLong(latitude=22.300, longitude=44.441) 58 | ) 59 | 60 | 61 | class LatLongFieldTestCase(TestCase): 62 | def test_latlog_field_create(self): 63 | m = MyModel.objects.create() 64 | new_m = MyModel.objects.get(pk=m.pk) 65 | 66 | self.assertEqual(new_m.empty_point, LatLong()) 67 | self.assertEqual(new_m.null_point, None) 68 | self.assertEqual(new_m.default_point, LatLong(33, 44)) 69 | 70 | def test_latlog_field_change_with_latlog_obj(self): 71 | m = MyModel.objects.create(empty_point=LatLong(22.123456, 33.654321)) 72 | new_m = MyModel.objects.get(pk=m.pk) 73 | 74 | self.assertEqual(new_m.empty_point, LatLong(22.123456, 33.654321)) 75 | 76 | def test_latlog_field_change_with_string(self): 77 | m = MyModel.objects.create(empty_point="22.123456;33.654321") 78 | new_m = MyModel.objects.get(pk=m.pk) 79 | 80 | self.assertEqual(new_m.empty_point, LatLong(22.123456, 33.654321)) 81 | 82 | def test_latlog_field_change_invalid_value(self): 83 | self.assertRaises(ValidationError, MyModel.objects.create, empty_point="22.12345633.654321") 84 | 85 | def test_get_prep_value_convert(self): 86 | field = LatLongField() 87 | 88 | self.assertEqual(field.get_prep_value(""), "") 89 | self.assertEqual( 90 | field.get_prep_value(LatLong(22.123456, 33.654321)), LatLong(22.123456, 33.654321) 91 | ) 92 | self.assertIsNone(field.get_prep_value(None)) 93 | 94 | def test_get_formfield(self): 95 | field = LatLongField() 96 | form_field = field.formfield() 97 | 98 | self.assertIsInstance(form_field, FormLatLongField) 99 | 100 | def test_deconstruct(self): 101 | field = LatLongField() 102 | _, _, args, kwargs = field.deconstruct() 103 | new_instance = LatLongField(*args, **kwargs) 104 | self.assertEqual(field.max_length, new_instance.max_length) 105 | 106 | 107 | class LoadBackendTestCase(TestCase): 108 | def test_load_google(self): 109 | backend = get_backend({"BACKEND": "treasuremap.backends.google.GoogleMapBackend"}) 110 | self.assertEqual(backend.__class__.__name__, "GoogleMapBackend") 111 | 112 | def test_load_yandex(self): 113 | backend = get_backend({"BACKEND": "treasuremap.backends.yandex.YandexMapBackend"}) 114 | self.assertEqual(backend.__class__.__name__, "YandexMapBackend") 115 | 116 | def test_load_failed(self): 117 | self.assertRaises( 118 | ImportError, get_backend, {"BACKEND": "treasuremap.backends.unknown.UnknownMapBackend"} 119 | ) 120 | 121 | def test_load_without_backend(self): 122 | backend = get_backend({}) 123 | self.assertEqual(backend.__class__.__name__, "GoogleMapBackend") 124 | 125 | def test_load_not_subclass_mapbackend(self): 126 | self.assertRaises(ImproperlyConfigured, get_backend, {"BACKEND": "django.test.TestCase"}) 127 | 128 | 129 | class ImportClassTestCase(TestCase): 130 | def test_import_from_string(self): 131 | c = import_class("django.test.TestCase") 132 | self.assertEqual(c, TestCase) 133 | 134 | def test_import_from_string_none(self): 135 | with self.assertRaises(ImportError): 136 | import_class("django.test.NonModel") 137 | 138 | 139 | class BaseMapBackendTestCase(TestCase): 140 | def test_base_init(self): 141 | backend = BaseMapBackend() 142 | 143 | self.assertEqual(backend.NAME, None) 144 | self.assertEqual(backend.API_URL, None) 145 | 146 | def test_base_get_js(self): 147 | backend = BaseMapBackend() 148 | 149 | self.assertRaises(ImproperlyConfigured, backend.get_js) 150 | 151 | def test_base_get_js_with_name(self): 152 | backend = BaseMapBackend() 153 | backend.NAME = "test" 154 | 155 | self.assertEqual(backend.get_js(), "treasuremap/default/js/jquery.treasuremap-test.js") 156 | 157 | def test_base_get_api_js(self): 158 | backend = BaseMapBackend() 159 | 160 | self.assertRaises(NotImplementedError, backend.get_api_js) 161 | 162 | def test_base_widget_template_default(self): 163 | backend = BaseMapBackend() 164 | 165 | self.assertEqual(backend.get_widget_template(), "treasuremap/widgets/map.html") 166 | 167 | @override_settings(TREASURE_MAP={"WIDGET_TEMPLATE": "template/custom.html"}) 168 | def test_base_widget_template_custom(self): 169 | backend = BaseMapBackend() 170 | 171 | self.assertEqual(backend.get_widget_template(), "template/custom.html") 172 | 173 | def test_base_api_key_default(self): 174 | backend = BaseMapBackend() 175 | 176 | self.assertEqual(backend.API_KEY, None) 177 | 178 | @override_settings(TREASURE_MAP={"API_KEY": "random_string"}) 179 | def test_base_api_key_settings(self): 180 | backend = BaseMapBackend() 181 | 182 | self.assertEqual(backend.API_KEY, "random_string") 183 | 184 | def test_base_only_map_default(self): 185 | backend = BaseMapBackend() 186 | 187 | self.assertEqual(backend.only_map, True) 188 | 189 | @override_settings(TREASURE_MAP={"ONLY_MAP": False}) 190 | def test_base_only_map_settings(self): 191 | backend = BaseMapBackend() 192 | 193 | self.assertEqual(backend.only_map, False) 194 | 195 | def test_base_size_default(self): 196 | backend = BaseMapBackend() 197 | 198 | self.assertEqual(backend.width, 400) 199 | self.assertEqual(backend.height, 400) 200 | 201 | @override_settings(TREASURE_MAP={"SIZE": (500, 500)}) 202 | def test_base_size_settings(self): 203 | backend = BaseMapBackend() 204 | 205 | self.assertEqual(backend.width, 500) 206 | self.assertEqual(backend.height, 500) 207 | 208 | @override_settings(TREASURE_MAP={"SIZE": ("Invalid",)}) 209 | def test_base_size_invalid_settings(self): 210 | self.assertRaises(ImproperlyConfigured, BaseMapBackend) 211 | 212 | @override_settings(TREASURE_MAP={"ADMIN_SIZE": (500, 500)}) 213 | def test_base_admin_size_settings(self): 214 | backend = BaseMapBackend() 215 | 216 | self.assertEqual(backend.admin_width, 500) 217 | self.assertEqual(backend.admin_height, 500) 218 | 219 | @override_settings(TREASURE_MAP={"ADMIN_SIZE": ("Invalid",)}) 220 | def test_base_admin_size_invalid_settings(self): 221 | self.assertRaises(ImproperlyConfigured, BaseMapBackend) 222 | 223 | def test_base_map_options_default(self): 224 | backend = BaseMapBackend() 225 | 226 | self.assertDictEqual( 227 | backend.get_map_options(), {"latitude": 51.562519, "longitude": -1.603156, "zoom": 5} 228 | ) 229 | 230 | @override_settings( 231 | TREASURE_MAP={"MAP_OPTIONS": {"latitude": 44.1, "longitude": -55.1, "zoom": 1}} 232 | ) 233 | def test_base_map_options_settings(self): 234 | backend = BaseMapBackend() 235 | 236 | self.assertDictEqual( 237 | backend.get_map_options(), {"latitude": 44.1, "longitude": -55.1, "zoom": 1} 238 | ) 239 | 240 | 241 | class GoogleMapBackendTestCase(TestCase): 242 | def test_get_api_js_default(self): 243 | backend = GoogleMapBackend() 244 | 245 | self.assertEqual(backend.get_api_js(), "//maps.googleapis.com/maps/api/js?v=3.exp") 246 | 247 | @override_settings(TREASURE_MAP={"API_KEY": "random_string"}) 248 | def test_get_api_js_with_api_key(self): 249 | backend = GoogleMapBackend() 250 | 251 | self.assertEqual( 252 | backend.get_api_js(), "//maps.googleapis.com/maps/api/js?v=3.exp&key=random_string" 253 | ) 254 | 255 | 256 | class YandexMapBackendTestCase(TestCase): 257 | def test_get_api_js(self): 258 | backend = YandexMapBackend() 259 | 260 | self.assertEqual(backend.get_api_js(), "//api-maps.yandex.ru/2.1/?lang=en-us") 261 | 262 | @override_settings(TREASURE_MAP={"API_KEY": "random_string"}) 263 | def test_get_api_js_with_api_key(self): 264 | backend = YandexMapBackend() 265 | 266 | self.assertEqual( 267 | backend.get_api_js(), "//api-maps.yandex.ru/2.1/?lang=en-us&apikey=random_string" 268 | ) 269 | 270 | 271 | class FormTestCase(TestCase): 272 | def test_witget_render(self): 273 | witget = MapWidget() 274 | done_html = """ 275 | {"latitude": 51.562519, "longitude": -1.603156, "zoom": 5} 276 | """ 277 | 278 | out_html = witget.render( 279 | "name", LatLong(22.123456, 33.654321), renderer=get_default_renderer() 280 | ) 281 | self.assertTrue(out_html, done_html) 282 | 283 | def test_witget_render_js(self): 284 | witget = MapWidget() 285 | 286 | out_html = str(witget.media) 287 | 288 | self.assertIn("//maps.googleapis.com/maps/api/js?v=3.exp", out_html) 289 | self.assertIn("/static/treasuremap/default/js/jquery.treasuremap-google.js", out_html) 290 | 291 | def test_admin_witget_render(self): 292 | witget = AdminMapWidget() 293 | done_html = """ 294 | {"latitude": 51.562519, "longitude": -1.603156, "zoom": 5} 295 | """ 296 | 297 | out_html = witget.render( 298 | "name", LatLong(22.123456, 33.654321), renderer=get_default_renderer() 299 | ) 300 | self.assertTrue(out_html, done_html) 301 | 302 | def test_admin_witget_render_js(self): 303 | witget = AdminMapWidget() 304 | 305 | out_html = str(witget.media) 306 | self.assertIn("//maps.googleapis.com/maps/api/js?v=3.exp", out_html) 307 | self.assertIn("/static/treasuremap/default/js/jquery.treasuremap-google.js", out_html) 308 | -------------------------------------------------------------------------------- /treasuremap/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals 4 | 5 | import django 6 | 7 | __author__ = "Dmitriy Sokolov" 8 | __version__ = "0.3.4" 9 | 10 | 11 | if django.VERSION < (3, 2): 12 | default_app_config = "treasuremap.apps.TreasureMapConfig" 13 | 14 | 15 | VERSION = __version__ 16 | -------------------------------------------------------------------------------- /treasuremap/apps.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals 4 | 5 | from django.apps import AppConfig 6 | from django.utils.translation import gettext_lazy as _ 7 | 8 | 9 | class TreasureMapConfig(AppConfig): 10 | name = "treasuremap" 11 | verbose_name = _("Treasure Map") 12 | -------------------------------------------------------------------------------- /treasuremap/backends/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /treasuremap/backends/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals 4 | 5 | from collections import OrderedDict 6 | 7 | from django.conf import settings 8 | from django.core.exceptions import ImproperlyConfigured 9 | 10 | 11 | class BaseMapBackend(object): 12 | """ 13 | Base map backend 14 | """ 15 | 16 | NAME = None 17 | API_URL = None 18 | 19 | def __init__(self): 20 | self.options = getattr(settings, "TREASURE_MAP", {}) 21 | self.API_KEY = self.options.get("API_KEY", None) 22 | 23 | try: 24 | self.width = int(self.options.get("SIZE", (400, 400))[0]) 25 | self.height = int(self.options.get("SIZE", (400, 400))[1]) 26 | except (IndexError, ValueError): 27 | raise ImproperlyConfigured( # pylint: disable=raise-missing-from 28 | "Invalid SIZE parameter, use: (width, height)." 29 | ) 30 | 31 | try: 32 | self.admin_width = int(self.options.get("ADMIN_SIZE", (400, 400))[0]) 33 | self.admin_height = int(self.options.get("ADMIN_SIZE", (400, 400))[1]) 34 | except (IndexError, ValueError): 35 | raise ImproperlyConfigured( # pylint: disable=raise-missing-from 36 | "Invalid ADMIN SIZE parameter, use: (width, height)." 37 | ) 38 | 39 | def get_js(self): 40 | """ 41 | Get jQuery plugin 42 | """ 43 | if self.NAME is None: 44 | raise ImproperlyConfigured("Your use abstract class.") 45 | return "treasuremap/default/js/jquery.treasuremap-{}.js".format(self.NAME) 46 | 47 | def get_api_js(self): 48 | """ 49 | Get javascript libraries 50 | """ 51 | raise NotImplementedError() 52 | 53 | def get_widget_template(self): 54 | return self.options.get("WIDGET_TEMPLATE", "treasuremap/widgets/map.html") 55 | 56 | @property 57 | def only_map(self): 58 | return self.options.get("ONLY_MAP", True) 59 | 60 | def get_map_options(self): 61 | map_options = self.options.get("MAP_OPTIONS", {}) 62 | map_options = OrderedDict(sorted(map_options.items(), key=lambda x: x[1], reverse=True)) 63 | 64 | if not map_options.get("latitude"): 65 | map_options["latitude"] = 51.562519 66 | 67 | if not map_options.get("longitude"): 68 | map_options["longitude"] = -1.603156 69 | 70 | if not map_options.get("zoom"): 71 | map_options["zoom"] = 5 72 | 73 | return map_options 74 | -------------------------------------------------------------------------------- /treasuremap/backends/google.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals 4 | 5 | from collections import OrderedDict 6 | 7 | try: 8 | from urllib import urlencode 9 | except ImportError: 10 | from urllib.parse import urlencode 11 | 12 | from .base import BaseMapBackend 13 | 14 | 15 | class GoogleMapBackend(BaseMapBackend): 16 | NAME = "google" 17 | API_URL = "//maps.googleapis.com/maps/api/js" 18 | 19 | def get_api_js(self): 20 | params = OrderedDict() 21 | params["v"] = "3.exp" 22 | 23 | if self.API_KEY: 24 | params["key"] = self.API_KEY 25 | 26 | return "{js_lib}?{params}".format(js_lib=self.API_URL, params=urlencode(params)) 27 | -------------------------------------------------------------------------------- /treasuremap/backends/yandex.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals 4 | 5 | from collections import OrderedDict 6 | 7 | try: 8 | from urllib import urlencode 9 | except ImportError: 10 | from urllib.parse import urlencode 11 | 12 | from django.conf import settings 13 | 14 | from .base import BaseMapBackend 15 | 16 | 17 | class YandexMapBackend(BaseMapBackend): 18 | NAME = "yandex" 19 | API_URL = "//api-maps.yandex.ru/2.1/" 20 | 21 | def get_api_js(self): 22 | params = OrderedDict() 23 | params["lang"] = settings.LANGUAGE_CODE 24 | 25 | if self.API_KEY: 26 | params["apikey"] = self.API_KEY 27 | 28 | return "{js_lib}?{params}".format(js_lib=self.API_URL, params=urlencode(params)) 29 | -------------------------------------------------------------------------------- /treasuremap/fields.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals 4 | 5 | from decimal import Decimal 6 | 7 | from django.core.exceptions import ValidationError 8 | from django.db import models 9 | from django.utils.deconstruct import deconstructible 10 | from django.utils.translation import gettext_lazy as _ 11 | 12 | from .forms import LatLongField as FormLatLongField 13 | 14 | 15 | @deconstructible 16 | class LatLong(object): 17 | def __init__(self, latitude=0.0, longitude=0.0): 18 | self.latitude = Decimal(latitude) 19 | self.longitude = Decimal(longitude) 20 | 21 | @staticmethod 22 | def _equals_to_the_cent(a, b): 23 | return round(a, 6) == round(b, 6) 24 | 25 | @staticmethod 26 | def _no_equals_to_the_cent(a, b): 27 | return round(a, 6) != round(b, 6) 28 | 29 | @property 30 | def format_latitude(self): 31 | return "{:.6f}".format(self.latitude) 32 | 33 | @property 34 | def format_longitude(self): 35 | return "{:.6f}".format(self.longitude) 36 | 37 | def __repr__(self): 38 | return "{}({:.6f}, {:.6f})".format(self.__class__.__name__, self.latitude, self.longitude) 39 | 40 | def __str__(self): 41 | return "{:.6f};{:.6f}".format(self.latitude, self.longitude) 42 | 43 | def __eq__(self, other): 44 | return isinstance(other, LatLong) and ( 45 | self._equals_to_the_cent(self.latitude, other.latitude) 46 | and self._equals_to_the_cent(self.longitude, other.longitude) 47 | ) 48 | 49 | def __ne__(self, other): 50 | return isinstance(other, LatLong) and ( 51 | self._no_equals_to_the_cent(self.latitude, other.latitude) 52 | or self._no_equals_to_the_cent(self.longitude, other.longitude) 53 | ) 54 | 55 | 56 | class LatLongField(models.Field): 57 | description = _("Geographic coordinate system fields") 58 | default_error_messages = { 59 | "invalid": _("'%(value)s' both values must be a decimal number or integer."), 60 | "invalid_separator": _("As the separator value '%(value)s' must be ';'"), 61 | } 62 | 63 | def __init__(self, *args, **kwargs): 64 | kwargs["max_length"] = 24 65 | super(LatLongField, self).__init__(*args, **kwargs) 66 | 67 | def get_internal_type(self): 68 | return "CharField" 69 | 70 | def to_python(self, value): 71 | if value is None: 72 | return None 73 | elif not value: 74 | return LatLong() 75 | elif isinstance(value, LatLong): 76 | return value 77 | else: 78 | if isinstance(value, (list, tuple, set)): 79 | args = value 80 | else: 81 | args = value.split(";") 82 | 83 | if len(args) != 2: 84 | raise ValidationError( 85 | self.error_messages["invalid_separator"], 86 | code="invalid", 87 | params={"value": value}, 88 | ) 89 | 90 | return LatLong(*args) 91 | 92 | def get_db_prep_value( 93 | self, value, connection, prepared=False # pylint: disable=unused-argument 94 | ): 95 | value = super(LatLongField, self).get_prep_value(value) 96 | if value is None: 97 | return None 98 | 99 | value = self.to_python(value) 100 | 101 | return str(value) 102 | 103 | def from_db_value( 104 | self, value, expression, connection, *args, **kwargs 105 | ): # pylint: disable=unused-argument 106 | return self.to_python(value) 107 | 108 | def formfield(self, _form_class=None, choices_form_class=None, **kwargs): 109 | return super(LatLongField, self).formfield( 110 | form_class=FormLatLongField, choices_form_class=choices_form_class, **kwargs 111 | ) 112 | -------------------------------------------------------------------------------- /treasuremap/forms.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals 4 | 5 | from django import forms 6 | from django.core.exceptions import ValidationError 7 | from django.utils.translation import gettext_lazy as _ 8 | 9 | from .widgets import MapWidget 10 | 11 | 12 | class LatLongField(forms.MultiValueField): 13 | widget = MapWidget 14 | default_error_messages = { 15 | "invalid_coordinates": _("Enter a valid coordinate."), 16 | } 17 | 18 | def __init__(self, *args, **kwargs): 19 | errors = self.default_error_messages.copy() 20 | if "error_messages" in kwargs: 21 | errors.update(kwargs["error_messages"]) 22 | 23 | fields = ( 24 | forms.DecimalField(label=_("latitude")), 25 | forms.DecimalField(label=_("longitude")), 26 | ) 27 | 28 | super(LatLongField, self).__init__(fields, *args, **kwargs) 29 | 30 | def compress(self, data_list): 31 | if data_list: 32 | if data_list[0] in self.empty_values: 33 | raise ValidationError(self.error_messages["invalid_coordinates"], code="invalid") 34 | if data_list[1] in self.empty_values: 35 | raise ValidationError(self.error_messages["invalid_coordinates"], code="invalid") 36 | return data_list 37 | return None 38 | -------------------------------------------------------------------------------- /treasuremap/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals 4 | -------------------------------------------------------------------------------- /treasuremap/static/treasuremap/default/js/jquery.treasuremap-google.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | function addMarker(position, map, latinput, lnginput, markers) { 3 | // del markers 4 | deleteMarkers(null, markers); 5 | 6 | // create new marker 7 | var marker = new google.maps.Marker({ 8 | position: position, 9 | map: map 10 | }); 11 | 12 | // update value 13 | latinput.val(marker.getPosition().lat().toFixed(6)); 14 | lnginput.val(marker.getPosition().lng().toFixed(6)); 15 | 16 | // save marker 17 | markers.push(marker); 18 | 19 | // move map to new position in center 20 | map.panTo(position); 21 | } 22 | 23 | function deleteMarkers(map, markers) { 24 | for (var i = 0; i < markers.length; i++) { 25 | markers[i].setMap(map); 26 | } 27 | markers = []; 28 | } 29 | 30 | $(document).ready(function() { 31 | $('.treasure-map').each(function (index, element) { 32 | var map_element = $(element).children('.map').get(0); 33 | var latitude_input = $(element).children('input:eq(0)'); 34 | var longitude_input = $(element).children('input:eq(1)'); 35 | 36 | var options = $.parseJSON($(element).children('script').text()) || {}; 37 | var markers = []; 38 | 39 | // var zoom = options.zoom || 4; 40 | 41 | var defaultMapOptions = {}; 42 | 43 | // init default map options 44 | defaultMapOptions.center = new google.maps.LatLng( 45 | parseFloat(latitude_input.val()) || options.latitude, 46 | parseFloat(longitude_input.val()) || options.longitude 47 | ); 48 | 49 | // merge user and default options 50 | var mapOptions = $.extend(defaultMapOptions, options); 51 | 52 | // create map 53 | var map = new google.maps.Map(map_element, mapOptions); 54 | 55 | // add default marker 56 | if (latitude_input.val() && longitude_input.val()) { 57 | addMarker(defaultMapOptions.center, map, latitude_input, longitude_input, markers); 58 | } 59 | 60 | // init listener 61 | google.maps.event.addListener(map, 'click', function (e) { 62 | addMarker(e.latLng, map, latitude_input, longitude_input, markers); 63 | }); 64 | }); 65 | }); 66 | })((typeof window.jQuery == 'undefined' && typeof window.django != 'undefined') ? django.jQuery : jQuery); 67 | -------------------------------------------------------------------------------- /treasuremap/static/treasuremap/default/js/jquery.treasuremap-yandex.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | function addMarker(position, map, latinput, lnginput, markers) { 3 | // del markers 4 | deleteMarkers(map, markers); 5 | 6 | // create new placemark 7 | var placemark = new ymaps.Placemark(position, {}); 8 | 9 | // add placemark to map 10 | map.geoObjects.add(placemark); 11 | 12 | // update value 13 | latinput.val(placemark.geometry.getCoordinates()[0].toFixed(6)); 14 | lnginput.val(placemark.geometry.getCoordinates()[1].toFixed(6)); 15 | 16 | // save marker 17 | markers.push(placemark); 18 | 19 | // move map to new position in center 20 | map.panTo(position); 21 | } 22 | 23 | function deleteMarkers(map, markers) { 24 | for (var i = 0; i < markers.length; i++) { 25 | map.geoObjects.remove(markers[i]); 26 | } 27 | markers = []; 28 | } 29 | 30 | $(document).ready(function() { 31 | return ymaps.ready(function() { 32 | $('.treasure-map').each(function (index, element) { 33 | var map_element = $(element).children('.map').get(0); 34 | var latitude_input = $(element).children('input:eq(0)'); 35 | var longitude_input = $(element).children('input:eq(1)'); 36 | 37 | var options = $.parseJSON($(element).children('script').text()) || {}; 38 | var markers = []; 39 | 40 | var defaultMapOptions = {}; 41 | 42 | // init default map options 43 | defaultMapOptions.center = [ 44 | parseFloat(latitude_input.val()) || options.latitude, 45 | parseFloat(longitude_input.val()) || options.longitude 46 | ]; 47 | 48 | // merge user and default options 49 | var mapOptions = $.extend(defaultMapOptions, options); 50 | 51 | // create map 52 | var map = new ymaps.Map(map_element, mapOptions); 53 | 54 | // add default marker 55 | if (latitude_input.val() && longitude_input.val()) { 56 | addMarker(defaultMapOptions.center, map, latitude_input, longitude_input, markers); 57 | } 58 | 59 | // init listener 60 | return map.events.add("click", 61 | function(e) { 62 | addMarker(e.get('coords'), map, latitude_input, longitude_input, markers); 63 | } 64 | ); 65 | }); 66 | }); 67 | }); 68 | })((typeof window.jQuery == 'undefined' && typeof window.django != 'undefined') ? django.jQuery : jQuery); 69 | -------------------------------------------------------------------------------- /treasuremap/templates/treasuremap/widgets/map.html: -------------------------------------------------------------------------------- 1 | 2 | {% spaceless %}{% for widget in widget.subwidgets %}{% include widget.template_name %}{% endfor %}{% endspaceless %} 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /treasuremap/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals 4 | 5 | import importlib 6 | 7 | from django.core.exceptions import ImproperlyConfigured 8 | 9 | from .backends.base import BaseMapBackend 10 | 11 | 12 | def import_class(path): 13 | path_bits = path.split(".") 14 | class_name = path_bits.pop() 15 | module_path = ".".join(path_bits) 16 | module_itself = importlib.import_module(module_path) 17 | 18 | if not hasattr(module_itself, class_name): 19 | raise ImportError("The Python module {} has no {} class.".format(module_path, class_name)) 20 | 21 | return getattr(module_itself, class_name) 22 | 23 | 24 | def get_backend(map_config): 25 | if not map_config.get("BACKEND"): 26 | map_config["BACKEND"] = "treasuremap.backends.google.GoogleMapBackend" 27 | 28 | backend = import_class(map_config["BACKEND"]) 29 | 30 | if not issubclass(backend, BaseMapBackend): 31 | raise ImproperlyConfigured("Is backend {} is not instance BaseMapBackend.".format(backend)) 32 | 33 | return backend() 34 | -------------------------------------------------------------------------------- /treasuremap/widgets.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals 4 | 5 | import json 6 | 7 | from django import forms 8 | from django.conf import settings 9 | from django.forms import MultiWidget 10 | from django.utils.safestring import mark_safe 11 | 12 | from .utils import get_backend 13 | 14 | 15 | class MapWidget(MultiWidget): 16 | def __init__(self, attrs=None): 17 | self.map_backend = get_backend(settings.TREASURE_MAP) 18 | 19 | if self.map_backend.only_map: 20 | widgets = ( 21 | forms.HiddenInput(attrs=attrs), 22 | forms.HiddenInput(attrs=attrs), 23 | ) 24 | else: 25 | widgets = ( 26 | forms.NumberInput(attrs=attrs), 27 | forms.NumberInput(attrs=attrs), 28 | ) 29 | 30 | super(MapWidget, self).__init__(widgets, attrs) 31 | 32 | def decompress(self, value): 33 | if value: 34 | return [value.format_latitude, value.format_longitude] 35 | return [None, None] 36 | 37 | @property 38 | def is_hidden(self): 39 | return False 40 | 41 | def get_context_widgets(self): 42 | context = { 43 | "map_options": json.dumps(self.map_backend.get_map_options()), 44 | "width": self.map_backend.width, 45 | "height": self.map_backend.height, 46 | "only_map": self.map_backend.only_map, 47 | } 48 | return context 49 | 50 | def render(self, name, value, attrs=None, renderer=None): 51 | context = self.get_context(name, value, attrs) 52 | context.update(self.get_context_widgets()) 53 | return mark_safe(renderer.render(self.map_backend.get_widget_template(), context)) 54 | 55 | def _get_media(self): 56 | media = forms.Media() 57 | for w in self.widgets: 58 | media = media + w.media 59 | 60 | media += forms.Media(js=(self.map_backend.get_api_js(), self.map_backend.get_js())) 61 | return media 62 | 63 | media = property(_get_media) 64 | 65 | 66 | class AdminMapWidget(MapWidget): 67 | def get_context_widgets(self): 68 | context = super(AdminMapWidget, self).get_context_widgets() 69 | 70 | context["width"] = self.map_backend.admin_width 71 | context["height"] = self.map_backend.admin_height 72 | 73 | return context 74 | --------------------------------------------------------------------------------