├── .gitignore ├── LICENSE ├── README.md ├── aoe2map ├── __init__.py ├── deployment.py.template ├── imagestorage.py.template ├── settings.py ├── urls.py └── wsgi.py ├── development.txt ├── django └── forms │ └── widgets │ ├── checkbox_option.html │ └── checkbox_select.html ├── manage.py ├── mapsapp ├── __init__.py ├── admin.py ├── api.py ├── api_urls.py ├── apps.py ├── decorators.py ├── forms.py ├── helpers.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20180813_2035.py │ ├── 0003_rms_newer_version.py │ ├── 0004_auto_20180823_2351.py │ ├── 0005_collection_owner.py │ ├── 0006_rms_information.py │ ├── 0007_auto_20180907_2253.py │ ├── 0008_sitesettings.py │ ├── 0009_auto_20180922_1934.py │ ├── 0010_image_preview.py │ ├── 0011_auto_20181025_2007.py │ ├── 0012_add-filename.py │ ├── 0013_auto_20181222_2231.py │ ├── 0014_rms_archived.py │ ├── 0015_add_rms_collection_class.py │ ├── 0016_migrate_rms_collection_relation.py │ ├── 0017_remove_collection_rms.py │ ├── 0018_collection_rms.py │ ├── 0019_auto_20200401_2003.py │ ├── 0020_auto_20210101_1504.py │ ├── 0021_rms_id.py │ ├── 0022_collection_id.py │ └── __init__.py ├── models.py ├── static │ └── mapsapp │ │ ├── css │ │ ├── aoe2map.css │ │ ├── bootstrap.css │ │ ├── bootstrap.min.css │ │ ├── railscasts.css │ │ └── tagsinput.css │ │ ├── fonts │ │ ├── lato-v14-latin-700.eot │ │ ├── lato-v14-latin-700.svg │ │ ├── lato-v14-latin-700.ttf │ │ ├── lato-v14-latin-700.woff │ │ ├── lato-v14-latin-700.woff2 │ │ ├── lato-v14-latin-italic.eot │ │ ├── lato-v14-latin-italic.svg │ │ ├── lato-v14-latin-italic.ttf │ │ ├── lato-v14-latin-italic.woff │ │ ├── lato-v14-latin-italic.woff2 │ │ ├── lato-v14-latin-regular.eot │ │ ├── lato-v14-latin-regular.svg │ │ ├── lato-v14-latin-regular.ttf │ │ ├── lato-v14-latin-regular.woff │ │ └── lato-v14-latin-regular.woff2 │ │ ├── images │ │ ├── HillFort_Preview.jpg │ │ ├── banner.svg │ │ ├── empty-de-0.png │ │ ├── empty-de-1.png │ │ ├── empty.png │ │ ├── load.gif │ │ ├── map.svg │ │ ├── maps.svg │ │ └── tile.png │ │ └── js │ │ ├── FileSaver.js │ │ ├── add-to-collection.js │ │ ├── aoe2map.js │ │ ├── bootstrap.js │ │ ├── bootstrap.js.map │ │ ├── bootstrap.min.js │ │ ├── bootstrap.min.js.map │ │ ├── collection.js │ │ ├── custom-versions.js │ │ ├── fileinputs-drop.js │ │ ├── highlight.js │ │ ├── highlight.js-rms.js │ │ ├── highlightjs-line-numbers.min.js │ │ ├── jquery-3.3.1.min.js │ │ ├── jszip.min.js │ │ ├── list-filter.js │ │ ├── mappack.js │ │ ├── maps.js │ │ ├── markdown-it.min.js │ │ ├── markdown.js │ │ ├── popper.min.js │ │ ├── tags.js │ │ ├── tagsinput.js │ │ ├── typeahead.bundle.js │ │ ├── view-code.js │ │ └── vote.js ├── templates │ ├── email_verification_email.html │ ├── mapsapp │ │ ├── base.html │ │ ├── collection.html │ │ ├── collections.html │ │ ├── editcollection.html │ │ ├── editmap.html │ │ ├── email_verification_invalid.html │ │ ├── email_verification_sent.html │ │ ├── email_verification_valid.html │ │ ├── index.html │ │ ├── info.html │ │ ├── login.html │ │ ├── map.html │ │ ├── map_archive.html │ │ ├── mappack.html │ │ ├── maps.html │ │ ├── mycollections.html │ │ ├── mymaps.html │ │ ├── newmap.html │ │ ├── register.html │ │ ├── search.html │ │ ├── settings.html │ │ ├── snippets │ │ │ ├── archived_alert.html │ │ │ ├── bootstrap-form.html │ │ │ ├── changelog_item.html │ │ │ ├── filter.html │ │ │ ├── latest_version_alert.html │ │ │ ├── loading_animation.html │ │ │ ├── maps_js_block.html │ │ │ ├── newer_version.html │ │ │ └── older_versions.html │ │ ├── tags.html │ │ └── version.html │ └── registration │ │ ├── password_reset_complete.html │ │ ├── password_reset_confirm.html │ │ ├── password_reset_done.html │ │ ├── password_reset_email.html │ │ ├── password_reset_form.html │ │ └── password_reset_subject.txt ├── templatetags │ ├── __init__.py │ ├── not_empty_choices.py │ └── startswith.py ├── tests │ ├── __init__.py │ ├── aoe2maptest.py │ ├── snapshots │ │ └── validation │ │ │ ├── test_latest_rms_.html │ │ │ └── test_latest_updated_rms_.html │ ├── test_api.py │ ├── test_authentification.py │ ├── test_changelog.py │ ├── test_homepagetest.py │ ├── test_models.py │ ├── test_smoketest.py │ ├── test_votes.py │ └── testdata │ │ ├── relic_nothing.png │ │ └── relic_nothing.rms ├── tokens.py ├── urls.py └── views.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | output/ 2 | .directory 3 | *.bak 4 | .idea 5 | media/* 6 | images/* 7 | aoe2map/deployment.py 8 | aoe2map/imagestorage.py 9 | 10 | # Byte-compiled / optimized / DLL files 11 | __pycache__/ 12 | *.py[cod] 13 | *$py.class 14 | 15 | # C extensions 16 | *.so 17 | 18 | # Distribution / packaging 19 | .Python 20 | build/ 21 | develop-eggs/ 22 | dist/ 23 | downloads/ 24 | eggs/ 25 | .eggs/ 26 | lib/ 27 | lib64/ 28 | parts/ 29 | sdist/ 30 | var/ 31 | wheels/ 32 | *.egg-info/ 33 | .installed.cfg 34 | *.egg 35 | MANIFEST 36 | 37 | # PyInstaller 38 | # Usually these files are written by a python script from a template 39 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 40 | *.manifest 41 | *.spec 42 | 43 | # Installer logs 44 | pip-log.txt 45 | pip-delete-this-directory.txt 46 | 47 | # Unit test / coverage reports 48 | htmlcov/ 49 | .tox/ 50 | .coverage 51 | .coverage.* 52 | .cache 53 | nosetests.xml 54 | coverage.xml 55 | *.cover 56 | .hypothesis/ 57 | .pytest_cache/ 58 | 59 | # Translations 60 | *.mo 61 | *.pot 62 | 63 | # Django stuff: 64 | *.log 65 | local_settings.py 66 | db.sqlite3 67 | 68 | # Flask stuff: 69 | instance/ 70 | .webassets-cache 71 | 72 | # Scrapy stuff: 73 | .scrapy 74 | 75 | # Sphinx documentation 76 | docs/_build/ 77 | 78 | # PyBuilder 79 | target/ 80 | 81 | # Jupyter Notebook 82 | .ipynb_checkpoints 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # celery beat schedule file 88 | celerybeat-schedule 89 | 90 | # SageMath parsed files 91 | *.sage.py 92 | 93 | # Environments 94 | .env 95 | .venv 96 | env/ 97 | venv/ 98 | ENV/ 99 | env.bak/ 100 | venv.bak/ 101 | 102 | # Spyder project settings 103 | .spyderproject 104 | .spyproject 105 | 106 | # Rope project settings 107 | .ropeproject 108 | 109 | # mkdocs documentation 110 | /site 111 | 112 | # mypy 113 | .mypy_cache/ 114 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # aoe2map 2 | [aoe2map.net](https://aoe2map.net) is a website to share and find Age of Empires II Random Map Scripts. 3 | 4 | This is a Django application. 5 | 6 | ## Changelog 7 | 8 | See [https://github.com/SiegeEngineers/aoe2map/releases](https://github.com/SiegeEngineers/aoe2map/releases) 9 | 10 | ## Development setup 11 | 12 | Let's assume you are on linux. 13 | 14 | You should have installed: 15 | - A recent version of Python 3 (Version 3.6 or above) 16 | - `virtualenv` 17 | - `pip3` 18 | - `git` 19 | 20 | You can check your versions like this: 21 | 22 | ``` 23 | $ python3 --version 24 | Python 3.6.5 25 | $ virtualenv --version 26 | 15.1.0 27 | $ pip3 --version 28 | pip 9.0.1 from /usr/lib/python3/dist-packages (python 3.6) 29 | $ git --version 30 | git version 2.17.1 31 | ``` 32 | 33 | Clone this repository into a folder of your choice, 34 | this will create the `aoe2map` folder with all the relevant files: 35 | ``` 36 | git clone https://github.com/SiegeEngineers/aoe2map.git 37 | ``` 38 | Now we set up a virtual python environment inside that folder 39 | ``` 40 | cd aoe2map 41 | virtualenv -p python3 venv 42 | ``` 43 | A folder `venv` is created that contains the virtual python environment. 44 | 45 | Now we activate that environment: 46 | ``` 47 | source venv/bin/activate 48 | ``` 49 | Your command line prompt should now show `(venv) ` at the beginning. 50 | 51 | We install the required dependencies into our environment. 52 | They have been written down into `dependencies.txt`, so we just have to execute: 53 | ``` 54 | pip install -r dependencies.txt 55 | ``` 56 | 57 | We also want to install the development dependencies from `development.txt`: 58 | ``` 59 | pip install -r development.txt 60 | ``` 61 | 62 | Before we can start the application, we have to add configuration files. 63 | We copy the templates in the aoe2map _subfolder_, but for development, 64 | we do not have to edit anything inside. 65 | ``` 66 | cp aoe2map/deployment.py.template aoe2map/deployment.py 67 | cp aoe2map/imagestorage.py.template aoe2map/imagestorage.py 68 | ``` 69 | 70 | 71 | Now we get to work with Django itself. We initialize the database: 72 | ``` 73 | ./manage.py migrate 74 | ``` 75 | 76 | … add a superuser: 77 | ``` 78 | ./manage.py createsuperuser --username admin --email admin@example.org 79 | ``` 80 | 81 | … and can run the development server: 82 | 83 | ``` 84 | ./manage.py runserver 85 | ``` 86 | 87 | It should say something like: 88 | ``` 89 | Performing system checks... 90 | 91 | System check identified no issues (0 silenced). 92 | September 10, 2018 - 19:03:25 93 | Django version 2.1, using settings 'aoe2map.settings' 94 | Starting development server at http://127.0.0.1:8000/ 95 | Quit the server with CONTROL-C. 96 | ``` 97 | 98 | Open your browser and go to `http://localhost:8000` and you should see an 99 | empty aoe2map.net instance where you can login with the superuser we created earlier. 100 | 101 | Further useful information: 102 | 103 | - The admin interface is then available at `http://localhost:8000/admin` 104 | - In the default development configuration, all emails the system would send 105 | get logged to the console instead 106 | - The development server reloads automatically when files change 107 | - I use PyCharm for development 108 | 109 | ## License 110 | aoe2map.net 111 | Copyright © 2018-2019 hszemi 112 | 113 | This program is free software: you can redistribute it and/or modify 114 | it under the terms of the GNU General Public License as published by 115 | the Free Software Foundation, either version 3 of the License, or 116 | (at your option) any later version. 117 | 118 | This program is distributed in the hope that it will be useful, 119 | but WITHOUT ANY WARRANTY; without even the implied warranty of 120 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 121 | GNU General Public License for more details. 122 | 123 | You should have received a copy of the GNU General Public License 124 | along with this program. If not, see . 125 | -------------------------------------------------------------------------------- /aoe2map/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiegeEngineers/aoe2map/d7d72597ec0c9a4bbf6eb83f3161335730d8daca/aoe2map/__init__.py -------------------------------------------------------------------------------- /aoe2map/deployment.py.template: -------------------------------------------------------------------------------- 1 | DJANGO_TOP_URL = 'https://aoe2map.net' 2 | 3 | # SECURITY WARNING: keep the secret key used in production secret! 4 | SECRET_KEY = 'secret-key' 5 | 6 | # SECURITY WARNING: don't run with debug turned on in production! 7 | DEBUG = True 8 | 9 | EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' 10 | #EMAIL_HOST = 'smtp.example.org' 11 | #EMAIL_PORT = 587 12 | #EMAIL_HOST_USER = 'user' 13 | #EMAIL_HOST_PASSWORD = 'password' 14 | #EMAIL_USE_TLS = True 15 | 16 | STATIC_ROOT = 'mapsapp/static' 17 | MEDIA_ROOT = 'media' 18 | # # # # # # # # # # 19 | # local config # 20 | # # # # # # # # # # 21 | IMAGE_URL = '/images/' # attention, also configure this in imagestorage.py! 22 | 23 | # # # # # # # # # # 24 | # azure config # 25 | # # # # # # # # # # 26 | # AZURE_ACCOUNT_NAME = ''' 27 | # AZURE_ACCOUNT_KEY = '' 28 | # AZURE_CONTAINER = '' 29 | # AZURE_URL_EXPIRATION_SECS = None 30 | # AZURE_CUSTOM_DOMAIN = f'{AZURE_ACCOUNT_NAME}.blob.core.windows.net' 31 | # IMAGE_URL = f'https://{AZURE_CUSTOM_DOMAIN}/{AZURE_CONTAINER}/' 32 | 33 | # SECURE_SSL_REDIRECT = True 34 | # CSRF_COOKIE_SECURE = True 35 | # SESSION_COOKIE_SECURE = True 36 | -------------------------------------------------------------------------------- /aoe2map/imagestorage.py.template: -------------------------------------------------------------------------------- 1 | from django.core.files.storage import FileSystemStorage 2 | from storages.backends.azure_storage import AzureStorage 3 | 4 | # # # # # # # # # # 5 | # local config # 6 | # # # # # # # # # # 7 | IMAGE_URL = '/images/' # attention, also configure this in settings.py! 8 | IMAGE_ROOT = 'images' 9 | IMAGE_STORAGE = FileSystemStorage(location=IMAGE_ROOT, base_url=IMAGE_URL) 10 | 11 | 12 | # # # # # # # # # # 13 | # remote config # 14 | # # # # # # # # # # 15 | # IMAGE_ROOT = 'images' 16 | # IMAGE_STORAGE = AzureStorage() 17 | -------------------------------------------------------------------------------- /aoe2map/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for aoe2map project. 3 | 4 | Generated by 'django-admin startproject' using Django 2.0.7. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/2.0/ref/settings/ 11 | """ 12 | 13 | import os 14 | from .deployment import * 15 | 16 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 17 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/ 21 | 22 | 23 | ALLOWED_HOSTS = ['aoe2map.net', 'www.aoe2map.net', 'aoe2map.uber.space', 'localhost'] 24 | 25 | # Application definition 26 | 27 | INSTALLED_APPS = [ 28 | 'mapsapp.apps.MapsappConfig', 29 | 'django.contrib.admin', 30 | 'django.contrib.auth', 31 | 'django.contrib.contenttypes', 32 | 'django.contrib.sessions', 33 | 'django.contrib.messages', 34 | 'django.contrib.staticfiles', 35 | 'django.forms', 36 | 'widget_tweaks', 37 | 'storages' 38 | ] 39 | 40 | MIDDLEWARE = [ 41 | 'django.middleware.security.SecurityMiddleware', 42 | 'django.contrib.sessions.middleware.SessionMiddleware', 43 | 'django.middleware.common.CommonMiddleware', 44 | 'django.middleware.csrf.CsrfViewMiddleware', 45 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 46 | 'django.contrib.messages.middleware.MessageMiddleware', 47 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 48 | ] 49 | 50 | ROOT_URLCONF = 'aoe2map.urls' 51 | 52 | FORM_RENDERER = 'django.forms.renderers.TemplatesSetting' 53 | 54 | TEMPLATES = [ 55 | { 56 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 57 | 'DIRS': [BASE_DIR, 'templates'], 58 | 'APP_DIRS': True, 59 | 'OPTIONS': { 60 | 'context_processors': [ 61 | 'django.template.context_processors.debug', 62 | 'django.template.context_processors.request', 63 | 'django.contrib.auth.context_processors.auth', 64 | 'django.contrib.messages.context_processors.messages', 65 | ], 66 | }, 67 | }, 68 | ] 69 | 70 | WSGI_APPLICATION = 'aoe2map.wsgi.application' 71 | 72 | # Database 73 | # https://docs.djangoproject.com/en/2.0/ref/settings/#databases 74 | 75 | DATABASES = { 76 | 'default': { 77 | 'ENGINE': 'django.db.backends.sqlite3', 78 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 79 | } 80 | } 81 | 82 | # Password validation 83 | # https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators 84 | 85 | AUTH_PASSWORD_VALIDATORS = [] 86 | 87 | AUTH_USER_MODEL = 'auth.User' 88 | 89 | # Internationalization 90 | # https://docs.djangoproject.com/en/2.0/topics/i18n/ 91 | 92 | LANGUAGE_CODE = 'en-us' 93 | 94 | TIME_ZONE = 'UTC' 95 | 96 | USE_I18N = True 97 | 98 | USE_L10N = True 99 | 100 | USE_TZ = True 101 | 102 | # Static files (CSS, JavaScript, Images) 103 | # https://docs.djangoproject.com/en/2.0/howto/static-files/ 104 | 105 | STATIC_URL = '/static/' 106 | MEDIA_URL = '/media/' 107 | LOGIN_URL = '/login' 108 | 109 | FILE_UPLOAD_PERMISSIONS = 0o644 110 | SECURE_CONTENT_TYPE_NOSNIFF = True 111 | SECURE_BROWSER_XSS_FILTER = True 112 | CSRF_TRUSTED_ORIGINS = ['https://aoe2map.uber.space', 'https://aoe2map.net', 'http://localhost'] 113 | X_FRAME_OPTIONS = 'DENY' 114 | DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' 115 | -------------------------------------------------------------------------------- /aoe2map/urls.py: -------------------------------------------------------------------------------- 1 | """aoe2map URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/2.0/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.conf.urls.static import static 18 | from django.urls import path, include 19 | 20 | from . import imagestorage 21 | from aoe2map import settings 22 | 23 | urlpatterns = [ 24 | path('', include('mapsapp.urls')), 25 | path('api/', include('mapsapp.api_urls')), 26 | path('admin/', admin.site.urls), 27 | ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + static(settings.IMAGE_URL, document_root=imagestorage.IMAGE_ROOT) 28 | -------------------------------------------------------------------------------- /aoe2map/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for aoe2map project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.0/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "aoe2map.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /development.txt: -------------------------------------------------------------------------------- 1 | selenium==4.11.2 2 | -------------------------------------------------------------------------------- /django/forms/widgets/checkbox_option.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
-------------------------------------------------------------------------------- /django/forms/widgets/checkbox_select.html: -------------------------------------------------------------------------------- 1 | {% with id=widget.attrs.id %}{% for group, options, index in widget.optgroups %}{% if group %} 2 | {{ group }}{% endif %}{% for option in options %} 3 | {% include option.template_name with widget=option %}{% endfor %}{% if group %} 4 | {% endif %}{% endfor %} 5 | {% endwith %} -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "aoe2map.settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError as exc: 10 | raise ImportError( 11 | "Couldn't import Django. Are you sure it's installed and " 12 | "available on your PYTHONPATH environment variable? Did you " 13 | "forget to activate a virtual environment?" 14 | ) from exc 15 | execute_from_command_line(sys.argv) 16 | -------------------------------------------------------------------------------- /mapsapp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiegeEngineers/aoe2map/d7d72597ec0c9a4bbf6eb83f3161335730d8daca/mapsapp/__init__.py -------------------------------------------------------------------------------- /mapsapp/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from mapsapp.models import Image, Rms, Tag, VersionTag, Collection, RmsCollection, Profile, SiteSettings, Vote 4 | 5 | admin.site.register(VersionTag) 6 | admin.site.register(Tag) 7 | admin.site.register(Rms) 8 | admin.site.register(Image) 9 | admin.site.register(Collection) 10 | admin.site.register(RmsCollection) 11 | admin.site.register(Profile) 12 | admin.site.register(SiteSettings) 13 | admin.site.register(Vote) 14 | -------------------------------------------------------------------------------- /mapsapp/api.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | 3 | from django.db.models import Q 4 | from django.http import JsonResponse, HttpResponseForbidden, Http404 5 | from django.shortcuts import get_object_or_404 6 | from django.urls import reverse 7 | from django.utils.html import escape, format_html 8 | from django.views.decorators.cache import cache_page 9 | from django.views.decorators.http import require_POST 10 | 11 | from mapsapp.decorators import ajax_login_required 12 | from mapsapp.helpers import count_voters, get_all_rms_instances, get_latest_version 13 | from mapsapp.models import Rms, VersionTag, Tag, Image, Collection, Vote 14 | 15 | 16 | @cache_page(60 * 60 * 24) 17 | def version(request): 18 | try: 19 | label = subprocess.check_output(["git", "describe", "--tags"]).decode('utf-8').strip() 20 | except subprocess.CalledProcessError: 21 | label = 'undefined' 22 | return JsonResponse({"version": label}) 23 | 24 | 25 | def status(request): 26 | return JsonResponse({ 27 | "population": { 28 | "rms": Rms.objects.count(), 29 | "tag": Tag.objects.count(), 30 | "versiontag": VersionTag.objects.count(), 31 | "image": Image.objects.count(), 32 | } 33 | }) 34 | 35 | 36 | def allmaps(request): 37 | retval = [] 38 | map_objects = Rms.objects.filter(newer_version=None, archived=False) 39 | for map_object in map_objects: 40 | retval.append({ 41 | 'uuid': map_object.uuid, 42 | 'name': map_object.name, 43 | 'authors': map_object.authors, 44 | 'version': map_object.version 45 | }) 46 | 47 | return JsonResponse({"allmaps": retval}) 48 | 49 | 50 | def maps(request): 51 | objects = maps2json(Rms.objects.filter(newer_version=None, archived=False).order_by('?')[0:12]) 52 | 53 | return JsonResponse({"maps": objects}) 54 | 55 | 56 | def rms(request, rms_id): 57 | rms_instance = get_object_or_404(Rms, pk=rms_id) 58 | objects = maps2json([rms_instance]) 59 | 60 | return JsonResponse({"maps": objects}) 61 | 62 | 63 | def rms_by_name(request, name): 64 | items = name.split(' ') 65 | query = Q(name__icontains=items[0]) | Q(authors__icontains=items[0]) 66 | for item in items[1:]: 67 | query &= (Q(name__icontains=item) | Q(authors__icontains=item)) 68 | rms_objects = Rms.objects.filter(query, newer_version=None, archived=False) 69 | objects = maps2json(rms_objects) 70 | 71 | return JsonResponse({"maps": objects}) 72 | 73 | 74 | def rms_by_file(request, filename): 75 | rms = Rms.objects.filter(original_filename=filename, archived=False) 76 | objects = maps2json(rms) 77 | 78 | return JsonResponse({"maps": objects}) 79 | 80 | 81 | def mymaps(request): 82 | if not request.user.is_authenticated: 83 | return HttpResponseForbidden('You must be logged in to access this url') 84 | 85 | objects = maps2json(Rms.objects.filter(owner=request.user, archived=False)) 86 | 87 | return JsonResponse({"maps": objects}) 88 | 89 | 90 | def collection(request, collection_id): 91 | c = get_object_or_404(Collection, pk=collection_id) 92 | objects = maps2json(c.rms.order_by('rmscollection__order', 'rmscollection__rms__name')) 93 | 94 | return JsonResponse({ 95 | "maps": objects, 96 | "uuid": c.uuid, 97 | "name": escape(c.name), 98 | "description": escape(c.description), 99 | "authors": escape(c.authors) 100 | }) 101 | 102 | 103 | @ajax_login_required 104 | def modifycollection(request): 105 | if 'action' not in request.POST: 106 | return JsonResponse({"status": "ERROR", "message": "'action' is missing", "class": "danger"}) 107 | 108 | if request.POST['action'] not in ['add']: 109 | return JsonResponse({"status": "ERROR", "message": "Invalid action", "class": "danger"}) 110 | 111 | if request.POST['action'] == 'add': 112 | missing_properties = [] 113 | for prop in ['rms_id', 'collection_id']: 114 | if prop not in request.POST: 115 | missing_properties.append(prop) 116 | if len(missing_properties) > 0: 117 | return JsonResponse({ 118 | "status": "ERROR", 119 | "message": f"The following mandatory properties are missing: {missing_properties}", 120 | "class": "danger" 121 | }) 122 | 123 | rms_id = request.POST['rms_id'] 124 | collection_id = request.POST['collection_id'] 125 | rms_instance = Rms.objects.filter(pk=rms_id, archived=False).first() 126 | collection_instance = Collection.objects.filter(pk=collection_id).first() 127 | 128 | if rms_instance is None: 129 | return JsonResponse({"status": "ERROR", "message": "Map not found", "class": "warning"}) 130 | 131 | if collection_instance is None: 132 | return JsonResponse({"status": "ERROR", "message": "Collection not found", "class": "warning"}) 133 | 134 | if rms_instance.newer_version is not None: 135 | return JsonResponse({ 136 | "status": "ERROR", 137 | "message": "You can only add the latest version of a map to a collection", 138 | "class": "warning" 139 | }) 140 | 141 | collection_instance.rms.add(rms_instance) 142 | 143 | return JsonResponse({ 144 | "status": "OK", 145 | "message": format_html( 146 | """The map {mapname} has been added 147 | to your collection {collectionname}.""", 148 | mapname=rms_instance.name, collectionname=collection_instance.name, 149 | collectionurl=reverse('collection_uuid', kwargs={"collection_id": collection_instance.uuid})), 150 | "class": "success" 151 | }) 152 | 153 | return JsonResponse({"status": "ERROR", "message": "Unknown action", "class": "danger"}) 154 | 155 | 156 | def maps2json(maps): 157 | objects = [] 158 | for o in maps: 159 | images = [] 160 | map_tags = [] 161 | version_tags = [] 162 | collections = [] 163 | for i in o.image_set.all(): 164 | preview_name = None 165 | preview_url = None 166 | if i.preview: 167 | preview_name = i.preview.name 168 | preview_url = i.preview.url 169 | images.append({ 170 | "name": i.file.name, 171 | "url": i.file.url, 172 | "preview_name": preview_name, 173 | "preview_url": preview_url 174 | }) 175 | for t in o.tags.all(): 176 | map_tags.append({"name": escape(t.name), "id": t.id}) 177 | for vt in o.versiontags.all(): 178 | version_tags.append(vt.name) 179 | for c in o.collection_set.all(): 180 | collections.append(c.uuid) 181 | newer_version = None 182 | latest_version = None 183 | if o.newer_version: 184 | newer_version = reverse('map', kwargs={'rms_id': o.newer_version.id, 'slug': o.newer_version.slug}) 185 | latest_version_rms = get_latest_version(o.newer_version) 186 | latest_version = reverse('map', kwargs={'rms_id': latest_version_rms.uuid, 'slug': latest_version_rms.slug}) 187 | objects.append({ 188 | "uuid": o.uuid, 189 | "name": escape(o.name), 190 | "version": escape(o.version), 191 | "authors": escape(o.authors), 192 | "description": escape(o.description), 193 | "pageurl": reverse('map', kwargs={'rms_id': o.id, 'slug': o.slug}), 194 | "newer_version": newer_version, 195 | "latest_version": latest_version, 196 | "url": escape(o.url), 197 | "file": o.file.name, 198 | "original_filename": o.original_filename, 199 | "fileurl": o.file.url, 200 | "tags": map_tags, 201 | "versiontags": version_tags, 202 | "collections": collections, 203 | "images": images, 204 | "votes": count_voters(o) 205 | }) 206 | return objects 207 | 208 | 209 | def tags(request, tag): 210 | if tag == '': 211 | return JsonResponse({"maps": []}) 212 | 213 | resultset = Rms.objects.filter(newer_version=None, archived=False).filter(tags=tag) 214 | objects = maps2json(resultset) 215 | 216 | return JsonResponse({"maps": objects}) 217 | 218 | 219 | def versiontag(request, version_name): 220 | versiontag_instance = get_object_or_404(VersionTag, name=version_name) 221 | resultset = Rms.objects.filter(newer_version=None, archived=False, versiontags=versiontag_instance) 222 | objects = maps2json(resultset) 223 | 224 | return JsonResponse({"maps": objects}) 225 | 226 | 227 | def alltags(request): 228 | tags = [] 229 | for tag in Tag.objects.order_by('name'): 230 | tags.append(tag.name) 231 | return JsonResponse({"tags": tags}) 232 | 233 | 234 | def mapsbyname(request, searchstring=None): 235 | maps = [] 236 | if not searchstring: 237 | rms_objects = Rms.objects.filter(newer_version=None, archived=False, owner=request.user).order_by('name') 238 | else: 239 | rms_objects = Rms.objects.filter(newer_version=None, archived=False, name__icontains=searchstring) 240 | for rms_object in rms_objects: 241 | maps.append({ 242 | 'name': f'{rms_object.name} by {rms_object.authors}', 243 | 'uuid': rms_object.uuid 244 | }) 245 | return JsonResponse({"maps": maps}) 246 | 247 | 248 | def namebyid(request, rms_id): 249 | rms_instance = get_object_or_404(Rms, pk=rms_id) 250 | return JsonResponse({"name": rms_instance.name, "authors": rms_instance.authors}) 251 | 252 | 253 | def latest_rms(request, amount): 254 | if amount < 0: 255 | raise Http404 256 | objects = Rms.objects.filter(newer_version=None, archived=False, predecessors=None).order_by('-created')[:amount] 257 | return JsonResponse({"maps": maps2json(objects)}) 258 | 259 | 260 | def latest_updated_rms(request, amount): 261 | if amount < 0: 262 | raise Http404 263 | objects = Rms.objects.filter(newer_version=None, archived=False).exclude(predecessors=None).order_by('-updated')[:amount] 264 | return JsonResponse({"maps": maps2json(objects)}) 265 | 266 | 267 | @ajax_login_required 268 | @require_POST 269 | def add_vote(request, rms_id): 270 | rms_instance = get_object_or_404(Rms, pk=rms_id, archived=False) 271 | Vote.objects.get_or_create(rms=rms_instance, user=request.user) 272 | return JsonResponse({"votes": count_voters(rms_instance), "self_voted": True}) 273 | 274 | 275 | @ajax_login_required 276 | @require_POST 277 | def remove_vote(request, rms_id): 278 | rms_instance = get_object_or_404(Rms, pk=rms_id, archived=False) 279 | all_instances = get_all_rms_instances(rms_instance) 280 | for instance in all_instances: 281 | Vote.objects.filter(rms=instance, user=request.user).delete() 282 | return JsonResponse({"votes": count_voters(rms_instance), "self_voted": False}) 283 | 284 | -------------------------------------------------------------------------------- /mapsapp/api_urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path, re_path 2 | 3 | from . import views, api 4 | 5 | app_name = 'api' 6 | urlpatterns = [ 7 | path('version', api.version, name='version'), 8 | path('status', api.status, name='status'), 9 | path('maps', api.maps, name='maps'), 10 | path('allmaps', api.allmaps, name='allmaps'), 11 | path('mymaps', api.mymaps, name='mymaps'), 12 | path('alltags', api.alltags, name='alltags'), 13 | path('mapsbyname', api.mapsbyname, name='mapsbyname'), 14 | path('mapsbyname/', api.mapsbyname, name='mapsbyname'), 15 | path('rms/', api.rms, name='rms'), 16 | path('rms/s/', api.rms_by_name, name='rms_by_name'), 17 | path('rms/latest/', api.latest_rms, name='latest_rms'), 18 | path('rms/latest_updated/', api.latest_updated_rms, name='latest_updated_rms'), 19 | path('rms/file/', api.rms_by_file, name='rms_by_file'), 20 | path('collection//maps', api.collection, name='collection'), # deprecated 21 | path('collection/', api.collection, name='collection'), 22 | path('modifycollection/', api.modifycollection, name='modifycollection'), 23 | path('version/', api.versiontag, name='version'), 24 | path('tags/', api.tags, name='tags'), 25 | path('vote//add', api.add_vote, name='add_vote'), 26 | path('vote//remove', api.remove_vote, name='remove_vote'), 27 | ] 28 | -------------------------------------------------------------------------------- /mapsapp/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class MapsappConfig(AppConfig): 5 | name = 'mapsapp' 6 | -------------------------------------------------------------------------------- /mapsapp/decorators.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | 3 | from django.http import JsonResponse 4 | 5 | 6 | def ajax_login_required(view_func): 7 | @wraps(view_func) 8 | def wrapper(request, *args, **kwargs): 9 | if request.user.is_authenticated: 10 | return view_func(request, *args, **kwargs) 11 | return JsonResponse({'not_authenticated': True}, status=403) 12 | return wrapper 13 | -------------------------------------------------------------------------------- /mapsapp/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.contrib.auth.forms import UserCreationForm 3 | from django.contrib.auth.hashers import check_password 4 | from django.contrib.auth.models import User 5 | from django.core.exceptions import ValidationError 6 | from django.core.validators import FileExtensionValidator 7 | from django.db import OperationalError 8 | from django.forms import Textarea, ModelForm, CheckboxSelectMultiple 9 | 10 | from mapsapp.models import VersionTag, Rms, Collection, Image 11 | 12 | FORM_HELP_COLLECTION_RMS = "Type the names of the maps you want to add and select the desired map from proposed values!" 13 | 14 | FORM_HELP_COLLECTION_NAME = "The name of the collection" 15 | 16 | FORM_HELP_COLLECTION_AUTHORS = '''The author(s) of the collection - usually the names of the map author(s) 17 | or the name of this collection's curator''' 18 | 19 | FORM_HELP_COLLECTION_MOD_ID = '''The ID of the in-game mod which contains the maps in this collection (optional)''' 20 | 21 | FORM_HELP_COLLECTION_DESCRIPTION = '''Describe this collection: What kind of maps are contained, 22 | what is the overall theme of this collection, …''' 23 | 24 | FORM_HELP_REMOVE_IMAGES = "Check all images that you want to remove" 25 | 26 | FORM_HELP_VERSIONS = "The versions that this map works in" 27 | 28 | FORM_HELP_TAGS = 'Tag your map with up to seven suitable keywords, for example ›4v4‹, ›FFA‹, or ›Nothing‹' 29 | 30 | FORM_HELP_URL = "An (optional) url for this map" 31 | 32 | FORM_HELP_MOD_ID = "The ID of the in-game mod which contains this map (optional)" 33 | 34 | FORM_HELP_MAP_INFORMATION = '''All the information about the map. This will appear only on the single 35 | map page. You can use some Markdown syntax in this field, like **bold**, 36 | _italic_, ~~strikethrough~~, [Link](https://example.org) or # Header''' 37 | 38 | FORM_HELP_MAP_DESCRIPTION = '''Briefly describe the map layout, the setting and/or the idea behind the 39 | map. This will appear on the map cards.''' 40 | 41 | FORM_HELP_MAP_AUTHORS = "Who made this map?" 42 | 43 | FORM_HELP_MAP_VERSION = "Optional version indicator like '1.1' or 'v2'" 44 | 45 | FORM_HELP_MAP_CHANGELOG = '''Optional text describing the changes in this version of the map 46 | compared to the previous version''' 47 | 48 | FORM_HELP_MAP_NAME = "The name of the map" 49 | 50 | FORM_HELP_IMAGES = '''Preview Images will be 600x311 px, so you should preferrably upload pictures of that size 51 | or aspect ratio. You can also drag+drop images in here.
52 | Maximum image size is 4200x4200 px, allowed image formats are png, jpg, and bmp. Images not in one of those formats will 53 | be skipped.''' 54 | 55 | FORM_HELP_FILE = "Choose the .rms file you want to share. You can also drag+drop it in here." 56 | 57 | FORM_HELP_IMAGES_TO_COPY = "Select the image files you want to copy to your new map." 58 | 59 | 60 | class MultipleFileInput(forms.ClearableFileInput): 61 | allow_multiple_selected = True 62 | 63 | 64 | class MultipleFileField(forms.FileField): 65 | def __init__(self, *args, **kwargs): 66 | kwargs.setdefault("widget", MultipleFileInput()) 67 | super().__init__(*args, **kwargs) 68 | 69 | def clean(self, data, initial=None): 70 | single_file_clean = super().clean 71 | if isinstance(data, (list, tuple)): 72 | result = [single_file_clean(d, initial) for d in data] 73 | else: 74 | result = single_file_clean(data, initial) 75 | return result 76 | 77 | 78 | def get_version_tag_choices(): 79 | versiontags = [] 80 | try: 81 | for vt in VersionTag.objects.all(): 82 | versiontags.append((vt.id, vt.name)) 83 | except OperationalError: 84 | pass 85 | return versiontags 86 | 87 | 88 | class NewRmsForm(forms.Form): 89 | file = forms.FileField(validators=[FileExtensionValidator(allowed_extensions=['rms'])], 90 | help_text=FORM_HELP_FILE) 91 | 92 | images = MultipleFileField(required=False, 93 | help_text=FORM_HELP_IMAGES) 94 | 95 | name = forms.CharField(max_length=255, 96 | help_text=FORM_HELP_MAP_NAME) 97 | 98 | version = forms.CharField(max_length=255, 99 | required=False, 100 | help_text=FORM_HELP_MAP_VERSION) 101 | 102 | changelog = forms.CharField(widget=Textarea(attrs={'rows': 3}), 103 | required=False, 104 | help_text=FORM_HELP_MAP_CHANGELOG) 105 | 106 | authors = forms.CharField(max_length=255, 107 | help_text=FORM_HELP_MAP_AUTHORS) 108 | 109 | description = forms.CharField(widget=Textarea, 110 | help_text=FORM_HELP_MAP_DESCRIPTION) 111 | 112 | url = forms.CharField(max_length=255, 113 | required=False, 114 | help_text=FORM_HELP_URL) 115 | 116 | mod_id = forms.IntegerField(label='Mod ID', 117 | required=False, 118 | help_text=FORM_HELP_MOD_ID) 119 | 120 | information = forms.CharField(widget=Textarea, 121 | required=False, 122 | help_text=FORM_HELP_MAP_INFORMATION) 123 | 124 | tags = forms.CharField(max_length=255, 125 | help_text=FORM_HELP_TAGS) 126 | 127 | versiontags = forms.MultipleChoiceField(label="Versions", 128 | choices=[], 129 | help_text=FORM_HELP_VERSIONS, 130 | widget=CheckboxSelectMultiple) 131 | 132 | images_to_copy = forms.MultipleChoiceField(label="Copy Images", 133 | required=False, 134 | help_text=FORM_HELP_IMAGES_TO_COPY, 135 | widget=CheckboxSelectMultiple) 136 | 137 | def clean_tags(self): 138 | tags = self.cleaned_data['tags'] 139 | if len(tags.split(',')) > 8: 140 | raise ValidationError("You may add at most 7 tags!") 141 | return tags 142 | 143 | def __init__(self, *args, **kwargs): 144 | super(NewRmsForm, self).__init__(*args, **kwargs) 145 | self.fields['versiontags'].choices = get_version_tag_choices() 146 | if 'initial' in kwargs and 'images_to_copy' in kwargs['initial']: 147 | self.fields['images_to_copy'].choices = kwargs['initial']['images_to_copy'] 148 | 149 | 150 | class EditRmsForm(ModelForm): 151 | tags = forms.CharField(max_length=255, 152 | required=True) 153 | 154 | remove_images = forms.ModelMultipleChoiceField(queryset=None, 155 | required=False, 156 | widget=CheckboxSelectMultiple) 157 | 158 | images = MultipleFileField(required=False, 159 | label='Add images') 160 | 161 | class Meta: 162 | model = Rms 163 | fields = ['name', 'version', 'authors', 'description', 'url', 'mod_id', 'changelog', 'information', 'tags', 'versiontags'] 164 | widgets = { 165 | 'versiontags': CheckboxSelectMultiple, 166 | 'changelog': Textarea(attrs={'rows': 3}), 167 | 'description': Textarea(attrs={'rows': 3}) 168 | } 169 | help_texts = { 170 | 'name': FORM_HELP_MAP_NAME, 171 | 'version': FORM_HELP_MAP_VERSION, 172 | 'changelog': FORM_HELP_MAP_CHANGELOG, 173 | 'authors': FORM_HELP_MAP_AUTHORS, 174 | 'description': FORM_HELP_MAP_DESCRIPTION, 175 | 'url': FORM_HELP_URL, 176 | 'mod_id': FORM_HELP_MOD_ID, 177 | 'information': FORM_HELP_MAP_INFORMATION, 178 | 'tags': FORM_HELP_TAGS, 179 | 'versiontags': FORM_HELP_VERSIONS, 180 | 'remove_images': FORM_HELP_REMOVE_IMAGES, 181 | 'images': FORM_HELP_IMAGES 182 | } 183 | 184 | def __init__(self, *args, **kwargs): 185 | super(EditRmsForm, self).__init__(*args, **kwargs) 186 | self.fields['remove_images'].queryset = Image.objects.filter(rms=self.instance) 187 | 188 | def clean_tags(self): 189 | tags = self.cleaned_data['tags'] 190 | if len(tags.split(',')) > 8: 191 | raise ValidationError("You may add at most 7 tags!") 192 | return tags 193 | 194 | 195 | class CollectionForm(ModelForm): 196 | rms = forms.CharField(required=True, 197 | help_text=FORM_HELP_COLLECTION_RMS, 198 | label='Maps') 199 | 200 | class Meta: 201 | model = Collection 202 | fields = ['name', 'authors', 'mod_id', 'description', 'rms'] 203 | help_texts = { 204 | 'name': FORM_HELP_COLLECTION_NAME, 205 | 'authors': FORM_HELP_COLLECTION_AUTHORS, 206 | 'mod_id': FORM_HELP_COLLECTION_MOD_ID, 207 | 'description': FORM_HELP_COLLECTION_DESCRIPTION 208 | } 209 | 210 | def clean_rms(self): 211 | uuidstring = self.cleaned_data['rms'] 212 | uuids = uuidstring.split(',') 213 | for uuid in uuids: 214 | if not Rms.objects.filter(pk=uuid).exists(): 215 | raise ValidationError('No map exists for value: %(value)s', params={'value': uuid}, code='invalid_uuid') 216 | return Rms.objects.filter(uuid__in=uuids) 217 | 218 | 219 | def validate_daut(value): 220 | if value != 'DauT': 221 | raise ValidationError("DauT is the name of our Lord and Saviour") 222 | 223 | 224 | class SignUpForm(UserCreationForm): 225 | email = forms.EmailField(max_length=254, 226 | help_text='A valid email address.', 227 | required=False) 228 | 229 | daut = forms.CharField(max_length=254, 230 | label='What is the name of our Lord and Saviour?', 231 | required=True, 232 | validators=[validate_daut]) 233 | 234 | class Meta: 235 | model = User 236 | fields = ('username', 'email', 'password1', 'password2', 'daut',) 237 | 238 | 239 | class SettingsForm(forms.Form): 240 | email = forms.EmailField(max_length=254, 241 | help_text='A valid email address.', 242 | required=False) 243 | 244 | new_password = forms.CharField(widget=forms.PasswordInput, 245 | help_text="Enter a new password if you want to change it. " + 246 | "If you want to keep your old password, leave this field blank.", 247 | required=False) 248 | current_password = forms.CharField(widget=forms.PasswordInput, 249 | help_text="Enter your current password to confirm the changes") 250 | 251 | def __init__(self, *args, **kwargs): 252 | self.user = kwargs.pop('user', None) 253 | super(SettingsForm, self).__init__(*args, **kwargs) 254 | 255 | def clean(self): 256 | cleaned_data = super(SettingsForm, self).clean() 257 | current_password = cleaned_data.get('current_password') 258 | if self.user and not check_password(current_password, self.user.password): 259 | self.add_error('current_password', 'Current password does not match.') 260 | -------------------------------------------------------------------------------- /mapsapp/helpers.py: -------------------------------------------------------------------------------- 1 | def count_voters(rms_instance): 2 | voters = None 3 | all_instances = get_all_rms_instances(rms_instance) 4 | for instance in all_instances: 5 | queryset = instance.vote_set.all().values_list('user_id', flat=True) 6 | if voters: 7 | voters |= queryset 8 | else: 9 | voters = queryset 10 | return voters.distinct().count() 11 | 12 | 13 | def has_self_voted(rms_instance, user_id): 14 | all_instances = get_all_rms_instances(rms_instance) 15 | for instance in all_instances: 16 | if instance.vote_set.filter(user_id=user_id).exists(): 17 | return True 18 | return False 19 | 20 | 21 | def get_successors(rms_instance, predecessors): 22 | if rms_instance.newer_version and rms_instance.newer_version not in predecessors: 23 | predecessors.add(rms_instance.newer_version) 24 | return get_successors(rms_instance, predecessors) 25 | else: 26 | return predecessors 27 | 28 | 29 | def get_predecessors(rms_instance): 30 | if rms_instance.predecessors.count() > 0: 31 | predecessors = {rms_instance} 32 | for predecessor in rms_instance.predecessors.all(): 33 | predecessors |= get_predecessors(predecessor) 34 | return predecessors 35 | else: 36 | a = set() 37 | a.add(rms_instance) 38 | return a 39 | 40 | 41 | def get_all_rms_instances(rms_instance): 42 | return get_successors(rms_instance, {rms_instance}) | get_predecessors(rms_instance) 43 | 44 | 45 | def get_latest_version(map_version, depth=100): 46 | if depth < 0: 47 | return map_version 48 | else: 49 | depth -= 1 50 | if map_version.newer_version is None: 51 | return map_version 52 | else: 53 | return get_latest_version(map_version.newer_version, depth) 54 | -------------------------------------------------------------------------------- /mapsapp/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.7 on 2018-08-10 23:10 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | import django.utils.timezone 7 | import mapsapp.models 8 | import uuid 9 | 10 | 11 | class Migration(migrations.Migration): 12 | 13 | initial = True 14 | 15 | dependencies = [ 16 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 17 | ] 18 | 19 | operations = [ 20 | migrations.CreateModel( 21 | name='Image', 22 | fields=[ 23 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 24 | ('filename', models.CharField(max_length=255)), 25 | ], 26 | ), 27 | migrations.CreateModel( 28 | name='Rms', 29 | fields=[ 30 | ('uuid', models.UUIDField(primary_key=True, serialize=False)), 31 | ('name', models.CharField(max_length=255)), 32 | ('version', models.CharField(max_length=255)), 33 | ('url', models.CharField(max_length=255)), 34 | ('filename', models.CharField(max_length=255)), 35 | ('file', models.CharField(max_length=255)), 36 | ], 37 | ), 38 | migrations.CreateModel( 39 | name='Tag', 40 | fields=[ 41 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 42 | ('name', models.CharField(max_length=255)), 43 | ], 44 | ), 45 | migrations.CreateModel( 46 | name='VersionTag', 47 | fields=[ 48 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 49 | ('name', models.CharField(max_length=255)), 50 | ], 51 | ), 52 | migrations.AddField( 53 | model_name='rms', 54 | name='tags', 55 | field=models.ManyToManyField(to='mapsapp.Tag'), 56 | ), 57 | migrations.AddField( 58 | model_name='rms', 59 | name='versiontags', 60 | field=models.ManyToManyField(to='mapsapp.VersionTag'), 61 | ), 62 | migrations.AddField( 63 | model_name='image', 64 | name='rms', 65 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='mapsapp.Rms'), 66 | ), 67 | migrations.AddField( 68 | model_name='rms', 69 | name='description', 70 | field=models.TextField(default=''), 71 | preserve_default=False, 72 | ), 73 | migrations.AlterField( 74 | model_name='rms', 75 | name='uuid', 76 | field=models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False), 77 | ), 78 | migrations.RemoveField( 79 | model_name='rms', 80 | name='filename', 81 | ), 82 | migrations.RenameField( 83 | model_name='image', 84 | old_name='filename', 85 | new_name='file', 86 | ), 87 | migrations.AlterField( 88 | model_name='image', 89 | name='file', 90 | field=models.ImageField(upload_to=mapsapp.models.rms_image_path), 91 | ), 92 | migrations.AlterField( 93 | model_name='rms', 94 | name='file', 95 | field=models.FileField(upload_to=mapsapp.models.rms_image_path), 96 | ), 97 | migrations.AlterField( 98 | model_name='rms', 99 | name='file', 100 | field=models.FileField(upload_to=mapsapp.models.rms_path), 101 | ), 102 | migrations.AddField( 103 | model_name='rms', 104 | name='authors', 105 | field=models.CharField(default='', max_length=255), 106 | preserve_default=False, 107 | ), 108 | migrations.AddField( 109 | model_name='rms', 110 | name='owner', 111 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), 112 | ), 113 | migrations.AlterField( 114 | model_name='rms', 115 | name='url', 116 | field=models.CharField(blank=True, max_length=255), 117 | ), 118 | migrations.AlterField( 119 | model_name='rms', 120 | name='version', 121 | field=models.CharField(blank=True, max_length=255), 122 | ), 123 | migrations.CreateModel( 124 | name='Collection', 125 | fields=[ 126 | ('name', models.CharField(max_length=255)), 127 | ('rms', models.ManyToManyField(blank=True, to='mapsapp.Rms')), 128 | ('authors', models.CharField(default='', max_length=255)), 129 | ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), 130 | ('description', models.TextField(default='')), 131 | ], 132 | ), 133 | migrations.AddField( 134 | model_name='rms', 135 | name='created', 136 | field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), 137 | preserve_default=False, 138 | ), 139 | migrations.AddField( 140 | model_name='rms', 141 | name='updated', 142 | field=models.DateTimeField(auto_now=True), 143 | ), 144 | ] 145 | -------------------------------------------------------------------------------- /mapsapp/migrations/0002_auto_20180813_2035.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.7 on 2018-08-13 20:35 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 12 | ('mapsapp', '0001_initial'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Profile', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('email_confirmed', models.BooleanField(default=False)), 21 | ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 22 | ], 23 | ), 24 | migrations.AlterField( 25 | model_name='collection', 26 | name='authors', 27 | field=models.CharField(max_length=255), 28 | ), 29 | migrations.AlterField( 30 | model_name='collection', 31 | name='description', 32 | field=models.TextField(), 33 | ), 34 | ] 35 | -------------------------------------------------------------------------------- /mapsapp/migrations/0003_rms_newer_version.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.7 on 2018-08-21 16:11 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('mapsapp', '0002_auto_20180813_2035'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='rms', 16 | name='newer_version', 17 | field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='mapsapp.Rms'), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /mapsapp/migrations/0004_auto_20180823_2351.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1 on 2018-08-23 23:51 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('mapsapp', '0003_rms_newer_version'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='tag', 15 | name='name', 16 | field=models.CharField(max_length=255, unique=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /mapsapp/migrations/0005_collection_owner.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.7 on 2018-09-02 16:42 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 12 | ('mapsapp', '0004_auto_20180823_2351'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AddField( 17 | model_name='collection', 18 | name='owner', 19 | field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), 20 | preserve_default=False, 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /mapsapp/migrations/0006_rms_information.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.7 on 2018-09-07 22:43 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('mapsapp', '0005_collection_owner'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='rms', 15 | name='information', 16 | field=models.TextField(default=''), 17 | preserve_default=False, 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /mapsapp/migrations/0007_auto_20180907_2253.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.7 on 2018-09-07 22:53 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('mapsapp', '0006_rms_information'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='rms', 15 | name='information', 16 | field=models.TextField(blank=True, default=None), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /mapsapp/migrations/0008_sitesettings.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.7 on 2018-09-09 16:54 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('mapsapp', '0007_auto_20180907_2253'), 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name='SiteSettings', 15 | fields=[ 16 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 17 | ('contact', models.TextField(default='')), 18 | ], 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /mapsapp/migrations/0009_auto_20180922_1934.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1 on 2018-09-22 19:34 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('mapsapp', '0008_sitesettings'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='rms', 16 | name='changelog', 17 | field=models.TextField(blank=True, default=''), 18 | ), 19 | migrations.AlterField( 20 | model_name='rms', 21 | name='information', 22 | field=models.TextField(blank=True, default=''), 23 | ), 24 | migrations.AlterField( 25 | model_name='rms', 26 | name='newer_version', 27 | field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='predecessors', to='mapsapp.Rms'), 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /mapsapp/migrations/0010_image_preview.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1 on 2018-10-14 17:50 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('mapsapp', '0009_auto_20180922_1934'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='image', 15 | name='preview', 16 | field=models.ImageField(blank=True, null=True, upload_to=''), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /mapsapp/migrations/0011_auto_20181025_2007.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.2 on 2018-10-25 20:07 2 | 3 | from django.db import migrations, models 4 | import mapsapp.models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('mapsapp', '0010_image_preview'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='image', 16 | name='preview', 17 | field=models.ImageField(blank=True, null=True, upload_to=mapsapp.models.rms_image_path), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /mapsapp/migrations/0012_add-filename.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.2 on 2018-12-19 17:44 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | def extract_filename(apps, _): 7 | Rms = apps.get_model('mapsapp', 'Rms') 8 | for rms in Rms.objects.all(): 9 | rms.original_filename = rms.file.path.split('/')[-1] 10 | rms.save() 11 | 12 | 13 | class Migration(migrations.Migration): 14 | 15 | dependencies = [ 16 | ('mapsapp', '0011_auto_20181025_2007'), 17 | ] 18 | 19 | operations = [ 20 | migrations.AddField( 21 | model_name='rms', 22 | name='original_filename', 23 | field=models.CharField(default='', max_length=255), 24 | preserve_default=False, 25 | ), 26 | migrations.RunPython(extract_filename) 27 | ] 28 | -------------------------------------------------------------------------------- /mapsapp/migrations/0013_auto_20181222_2231.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.2 on 2018-12-22 22:31 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 12 | ('mapsapp', '0012_add-filename'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Vote', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('rms', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='mapsapp.Rms')), 21 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 22 | ], 23 | ), 24 | migrations.AlterUniqueTogether( 25 | name='vote', 26 | unique_together={('rms', 'user')}, 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /mapsapp/migrations/0014_rms_archived.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.10 on 2019-07-07 21:40 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('mapsapp', '0013_auto_20181222_2231'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='rms', 15 | name='archived', 16 | field=models.BooleanField(default=False), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /mapsapp/migrations/0015_add_rms_collection_class.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.11 on 2019-10-02 21:48 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | import mapsapp.models 6 | import storages.backends.azure_storage 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('mapsapp', '0014_rms_archived'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='RmsCollection', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('order', models.IntegerField(default=0)), 21 | ('collection', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='mapsapp.Collection')), 22 | ('rms', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='mapsapp.Rms')), 23 | ], 24 | ), 25 | migrations.AlterField( 26 | model_name='image', 27 | name='file', 28 | field=models.ImageField(storage=storages.backends.azure_storage.AzureStorage(), upload_to=mapsapp.models.rms_image_path), 29 | ), 30 | migrations.AlterField( 31 | model_name='image', 32 | name='preview', 33 | field=models.ImageField(blank=True, null=True, storage=storages.backends.azure_storage.AzureStorage(), upload_to=mapsapp.models.rms_image_path), 34 | ), 35 | ] 36 | -------------------------------------------------------------------------------- /mapsapp/migrations/0016_migrate_rms_collection_relation.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.11 on 2019-10-02 21:48 2 | 3 | from django.db import migrations 4 | 5 | 6 | def create_through_relations(apps, schema_editor): 7 | RmsCollection = apps.get_model('mapsapp', 'RmsCollection') 8 | Collection = apps.get_model('mapsapp', 'Collection') 9 | for collection in Collection.objects.all(): 10 | for rms in collection.rms.all(): 11 | RmsCollection( 12 | rms=rms, 13 | collection=collection 14 | ).save() 15 | 16 | 17 | class Migration(migrations.Migration): 18 | dependencies = [ 19 | ('mapsapp', '0015_add_rms_collection_class'), 20 | ] 21 | 22 | operations = [ 23 | migrations.RunPython(create_through_relations, reverse_code=migrations.RunPython.noop), 24 | ] 25 | -------------------------------------------------------------------------------- /mapsapp/migrations/0017_remove_collection_rms.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.11 on 2019-10-02 21:53 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('mapsapp', '0016_migrate_rms_collection_relation'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='collection', 15 | name='rms', 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /mapsapp/migrations/0018_collection_rms.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.11 on 2019-10-02 21:53 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('mapsapp', '0017_remove_collection_rms'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='collection', 15 | name='rms', 16 | field=models.ManyToManyField(blank=True, through='mapsapp.RmsCollection', to='mapsapp.Rms'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /mapsapp/migrations/0019_auto_20200401_2003.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.12 on 2020-04-01 20:03 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('mapsapp', '0018_collection_rms'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name='rms', 15 | options={'ordering': ['-updated']}, 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /mapsapp/migrations/0020_auto_20210101_1504.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.4 on 2021-01-01 15:04 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('mapsapp', '0019_auto_20200401_2003'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='collection', 15 | name='mod_id', 16 | field=models.IntegerField(blank=True, null=True), 17 | ), 18 | migrations.AddField( 19 | model_name='rms', 20 | name='mod_id', 21 | field=models.IntegerField(blank=True, null=True), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /mapsapp/migrations/0021_rms_id.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.1 on 2022-01-09 16:07 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('mapsapp', '0020_auto_20210101_1504'), 10 | ] 11 | 12 | @staticmethod 13 | def calculate_id(rms_uuid, rms_model): 14 | uuid_str = str(rms_uuid) 15 | for strlen in range(6, len(uuid_str)-1): 16 | candidate = uuid_str[:strlen] 17 | if not rms_model.objects.filter(id=candidate): 18 | return candidate 19 | 20 | def calculate_defaults(apps, schema_editor): 21 | Rms = apps.get_model('mapsapp', 'Rms') 22 | for rms in Rms.objects.all().iterator(): 23 | rms.id = Migration.calculate_id(rms.uuid, Rms) 24 | rms.save() 25 | 26 | operations = [ 27 | migrations.AddField( 28 | model_name='rms', 29 | name='id', 30 | field=models.CharField(null=True, max_length=255), 31 | ), 32 | migrations.RunPython(calculate_defaults, migrations.RunPython.noop), 33 | migrations.AlterField( 34 | model_name='rms', 35 | name='id', 36 | field=models.CharField(max_length=255, unique=True), 37 | ), 38 | ] 39 | -------------------------------------------------------------------------------- /mapsapp/migrations/0022_collection_id.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.1 on 2022-01-09 16:07 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('mapsapp', '0021_rms_id'), 10 | ] 11 | 12 | @staticmethod 13 | def calculate_id(collection_uuid, collection_model): 14 | uuid_str = str(collection_uuid) 15 | for strlen in range(6, len(uuid_str)-1): 16 | candidate = uuid_str[:strlen] 17 | if not collection_model.objects.filter(id=candidate): 18 | return candidate 19 | 20 | def calculate_defaults(apps, schema_editor): 21 | Collection = apps.get_model('mapsapp', 'Collection') 22 | for collection in Collection.objects.all().iterator(): 23 | collection.id = Migration.calculate_id(collection.uuid, Collection) 24 | collection.save() 25 | 26 | operations = [ 27 | migrations.AddField( 28 | model_name='collection', 29 | name='id', 30 | field=models.CharField(null=True, max_length=255), 31 | ), 32 | migrations.RunPython(calculate_defaults, migrations.RunPython.noop), 33 | migrations.AlterField( 34 | model_name='collection', 35 | name='id', 36 | field=models.CharField(max_length=255, unique=True), 37 | ), 38 | ] 39 | -------------------------------------------------------------------------------- /mapsapp/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiegeEngineers/aoe2map/d7d72597ec0c9a4bbf6eb83f3161335730d8daca/mapsapp/migrations/__init__.py -------------------------------------------------------------------------------- /mapsapp/models.py: -------------------------------------------------------------------------------- 1 | import math 2 | import os 3 | import sys 4 | import uuid as uuid 5 | from io import BytesIO 6 | 7 | from PIL import Image as PilImage 8 | from django.contrib.auth.models import User 9 | from django.core.files.uploadedfile import InMemoryUploadedFile 10 | from django.db import models 11 | from django.db.models.signals import post_save, post_delete 12 | from django.dispatch import receiver 13 | from django.utils.text import slugify 14 | 15 | from aoe2map import settings 16 | from aoe2map import imagestorage 17 | 18 | MAX_IMAGE_WIDTH = 4200 19 | MAX_IMAGE_HEIGHT = 4200 20 | PREVIEW_WIDTH = 600 21 | PREVIEW_HEIGHT = 311 22 | 23 | 24 | def rms_image_path(instance, filename): 25 | return os.path.join(str(instance.rms.uuid), filename) 26 | 27 | 28 | def rms_path(instance, filename): 29 | return os.path.join(str(instance.uuid), filename) 30 | 31 | 32 | class VersionTag(models.Model): 33 | name = models.CharField(max_length=255) 34 | 35 | def __str__(self): 36 | return self.name 37 | 38 | 39 | class Tag(models.Model): 40 | name = models.CharField(max_length=255, unique=True) 41 | 42 | def __str__(self): 43 | return self.name 44 | 45 | 46 | class Rms(models.Model): 47 | uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) 48 | id = models.CharField(max_length=255, unique=True) 49 | owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) 50 | name = models.CharField(max_length=255) 51 | version = models.CharField(max_length=255, blank=True) 52 | changelog = models.TextField(blank=True, default='') 53 | authors = models.CharField(max_length=255) 54 | description = models.TextField() 55 | information = models.TextField(blank=True, default='') 56 | url = models.CharField(max_length=255, blank=True) 57 | mod_id = models.IntegerField(null=True, blank=True) 58 | file = models.FileField(upload_to=rms_path) 59 | original_filename = models.CharField(max_length=255) 60 | tags = models.ManyToManyField(Tag) 61 | versiontags = models.ManyToManyField(VersionTag) 62 | newer_version = models.ForeignKey('self', on_delete=models.SET_NULL, blank=True, null=True, default=None, 63 | related_name='predecessors') 64 | created = models.DateTimeField(auto_now_add=True) 65 | updated = models.DateTimeField(auto_now=True) 66 | archived = models.BooleanField(default=False) 67 | 68 | class Meta: 69 | ordering = ["-updated"] 70 | 71 | def __str__(self): 72 | return f"{self.name} ({self.version})" 73 | 74 | @property 75 | def slug(self): 76 | return slugify(self.name) 77 | 78 | def calculate_id(self): 79 | uuid_str = str(self.uuid) 80 | for strlen in range(6, len(uuid_str)-1): 81 | candidate = uuid_str[:strlen] 82 | if not Rms.objects.filter(id=candidate): 83 | return candidate 84 | 85 | def save(self, *args, **kwargs): 86 | if not self.id: 87 | self.id = self.uuid 88 | super(Rms, self).save(*args, **kwargs) 89 | self.id = self.calculate_id() 90 | super(Rms, self).save(*args, **kwargs) 91 | 92 | 93 | class Image(models.Model): 94 | rms = models.ForeignKey(Rms, on_delete=models.CASCADE) 95 | file = models.ImageField(upload_to=rms_image_path, storage=imagestorage.IMAGE_STORAGE) 96 | preview = models.ImageField(upload_to=rms_image_path, storage=imagestorage.IMAGE_STORAGE, null=True, blank=True) 97 | 98 | def save(self): 99 | # Opening the uploaded image 100 | uploaded_image = PilImage.open(self.file) 101 | uploaded_image = uploaded_image.convert('RGBA') 102 | 103 | factor = 1 104 | if uploaded_image.width > MAX_IMAGE_WIDTH: 105 | factor = min(factor, MAX_IMAGE_WIDTH / uploaded_image.width) 106 | if uploaded_image.height > MAX_IMAGE_HEIGHT: 107 | factor = min(factor, MAX_IMAGE_HEIGHT / uploaded_image.height) 108 | 109 | output = BytesIO() 110 | 111 | new_width = math.floor(factor * uploaded_image.width) 112 | new_height = math.floor(factor * uploaded_image.height) 113 | 114 | # Resize/modify the image 115 | resized_image = uploaded_image.resize((new_width, new_height), resample=PilImage.BILINEAR) 116 | 117 | # after modifications, save it to the output 118 | resized_image.save(output, format='PNG', quality=100) 119 | output.seek(0) 120 | 121 | # change the image field value to be the newly modifed image value 122 | self.file = InMemoryUploadedFile(output, 'ImageField', f"{os.path.splitext(self.file.name)[0]}.png", 123 | 'image/png', sys.getsizeof(output), None) 124 | 125 | if uploaded_image.width != PREVIEW_WIDTH or uploaded_image.height != PREVIEW_HEIGHT: 126 | width_factor = min(factor, PREVIEW_WIDTH / uploaded_image.width) 127 | height_factor = min(factor, PREVIEW_HEIGHT / uploaded_image.height) 128 | preview_factor = max(width_factor, height_factor) 129 | 130 | preview_width = math.floor(preview_factor * uploaded_image.width) 131 | preview_height = math.floor(preview_factor * uploaded_image.height) 132 | 133 | uncropped_preview = uploaded_image.resize((preview_width, preview_height), resample=PilImage.BILINEAR) 134 | crop_left = math.floor((preview_width - PREVIEW_WIDTH) / 2) 135 | crop_upper = math.floor((preview_height - PREVIEW_HEIGHT) / 2) 136 | crop_right = crop_left + PREVIEW_WIDTH 137 | crop_lower = crop_upper + PREVIEW_HEIGHT 138 | preview = uncropped_preview.crop((crop_left, crop_upper, crop_right, crop_lower)) 139 | preview_output = BytesIO() 140 | preview.save(preview_output, format='PNG', quality=100) 141 | preview_output.seek(0) 142 | self.preview = InMemoryUploadedFile(preview_output, 'ImageField', "{}.preview.png".format( 143 | os.path.splitext(self.file.name)[0]), 'image/png', sys.getsizeof(output), None) 144 | 145 | super(Image, self).save() 146 | 147 | def __str__(self): 148 | items = self.file.name.split('/') 149 | return f"{items[-1]}" 150 | 151 | 152 | @receiver(post_delete, sender=Image) 153 | def submission_delete(sender, instance, **kwargs): 154 | instance.file.delete(False) 155 | if instance.preview: 156 | instance.preview.delete(False) 157 | 158 | 159 | class Collection(models.Model): 160 | uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) 161 | id = models.CharField(max_length=255, unique=True) 162 | owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) 163 | name = models.CharField(max_length=255) 164 | authors = models.CharField(max_length=255) 165 | mod_id = models.IntegerField(null=True, blank=True) 166 | description = models.TextField() 167 | rms = models.ManyToManyField(Rms, through='RmsCollection', blank=True) 168 | 169 | def __str__(self): 170 | return self.name 171 | 172 | @property 173 | def slug(self): 174 | return slugify(self.name) 175 | 176 | def calculate_id(self): 177 | uuid_str = str(self.uuid) 178 | for strlen in range(6, len(uuid_str)-1): 179 | candidate = uuid_str[:strlen] 180 | if not Collection.objects.filter(id=candidate): 181 | return candidate 182 | 183 | def save(self, *args, **kwargs): 184 | if not self.id: 185 | self.id = self.uuid 186 | super(Collection, self).save(*args, **kwargs) 187 | self.id = self.calculate_id() 188 | super(Collection, self).save(*args, **kwargs) 189 | 190 | 191 | class RmsCollection(models.Model): 192 | collection = models.ForeignKey(Collection, on_delete=models.CASCADE) 193 | rms = models.ForeignKey(Rms, on_delete=models.CASCADE) 194 | order = models.IntegerField(default=0) 195 | 196 | def __str__(self): 197 | return f'{self.collection.name}-{self.rms.name}-{self.order}' 198 | 199 | 200 | class Profile(models.Model): 201 | user = models.OneToOneField(User, on_delete=models.CASCADE) 202 | email_confirmed = models.BooleanField(default=False) 203 | 204 | def __str__(self): 205 | return self.user.username 206 | 207 | 208 | @receiver(post_save, sender=User) 209 | def update_user_profile(sender, instance, created, **kwargs): 210 | if created: 211 | Profile.objects.create(user=instance) 212 | instance.profile.save() 213 | 214 | 215 | class SiteSettings(models.Model): 216 | contact = models.TextField(default='') 217 | 218 | def save(self, *args, **kwargs): 219 | self.pk = 1 220 | super(SiteSettings, self).save(*args, **kwargs) 221 | 222 | def delete(self, *args, **kwargs): 223 | pass 224 | 225 | @classmethod 226 | def load(cls): 227 | obj, created = cls.objects.get_or_create(pk=1) 228 | return obj 229 | 230 | 231 | class Vote(models.Model): 232 | rms = models.ForeignKey(Rms, on_delete=models.CASCADE) 233 | user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) 234 | 235 | class Meta: 236 | unique_together = ('rms', 'user',) 237 | -------------------------------------------------------------------------------- /mapsapp/static/mapsapp/css/aoe2map.css: -------------------------------------------------------------------------------- 1 | .card { 2 | margin-bottom: 1rem; 3 | } 4 | 5 | .form-control { 6 | height: initial !important; 7 | } 8 | 9 | .mapscreenshot { 10 | max-width: 600px; 11 | } 12 | 13 | .mapscreenshot-container { 14 | display: flex; 15 | justify-content: space-around; 16 | align-items: flex-start; 17 | } 18 | 19 | .singlemap-area { 20 | background-image: url("../images/tile.png"); 21 | } 22 | 23 | #banner { 24 | max-height: 100px; 25 | max-width: 100%; 26 | } 27 | 28 | .user-nav { 29 | background: linear-gradient(#fc0, #fa0); 30 | border-radius: .5rem; 31 | color: black; 32 | } 33 | 34 | .user-nav a { 35 | color: black; 36 | } 37 | 38 | .user-nav a:hover { 39 | color: #333; 40 | } 41 | 42 | hr { 43 | border-color: #fc0; 44 | } 45 | 46 | ul.maplist, 47 | ul.mapslist { 48 | list-style: none; 49 | padding: 0; 50 | margin: 0; 51 | } 52 | 53 | ul.maplist li:before { 54 | content: url('../images/map.svg'); 55 | } 56 | 57 | ul.mapslist li:before { 58 | content: url('../images/maps.svg'); 59 | } 60 | 61 | ul.maplist li:before, 62 | ul.mapslist li:before { 63 | display: inline-block;; 64 | width: 1rem; 65 | height: 1rem; 66 | margin-right: .2rem; 67 | } 68 | 69 | .alltags, 70 | .allversions { 71 | text-align: center; 72 | padding-bottom: 1rem; 73 | } 74 | 75 | #drop-info { 76 | position: fixed; 77 | top: 0; 78 | left: 0; 79 | right: 0; 80 | bottom: 0; 81 | z-index: 500; 82 | width: 100%; 83 | height: 100%; 84 | background-color: rgba(222, 222, 222, 0.3); 85 | display: none; 86 | justify-content: center; 87 | align-items: center; 88 | } 89 | 90 | #drop-info span { 91 | background-color: #777; 92 | font-size: 4rem; 93 | font-weight: bold; 94 | padding-left: 1rem; 95 | padding-right: 1rem; 96 | } 97 | 98 | .dragging #drop-info { 99 | display: flex; 100 | } 101 | 102 | .map-description { 103 | max-height: 9.75rem; 104 | overflow: hidden; 105 | text-overflow: ellipsis; 106 | transition: max-height 0.3s ease-out; 107 | margin-bottom: 0.5rem; 108 | padding-bottom: 1rem; 109 | position: relative; 110 | } 111 | 112 | .map-description:after{ 113 | content: ""; 114 | position: absolute; top: 0; bottom: 0; left: -1rem; right: -1rem; 115 | box-shadow: inset #303030 0 -1rem 0.9rem; 116 | } 117 | 118 | .map-description.full { 119 | max-height: none; 120 | } 121 | 122 | .modal-xl { 123 | max-width: 1140px; 124 | } 125 | 126 | .hljs-ln td.hljs-ln-numbers { 127 | text-align: right; 128 | padding-right: 1rem; 129 | color: gray; 130 | } 131 | 132 | .heart { 133 | font-style: normal; 134 | font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji", sans-serif; 135 | } 136 | 137 | .btn-secondary:hover .heart { 138 | color: #fc0; 139 | } 140 | 141 | #nav-link-donate { 142 | font-weight: bold; 143 | line-height: 20pt; 144 | } -------------------------------------------------------------------------------- /mapsapp/static/mapsapp/css/railscasts.css: -------------------------------------------------------------------------------- 1 | .hljs { 2 | display: block; 3 | overflow-x: auto; 4 | padding: 0.5em; 5 | background: #232323; 6 | color: #e6e1dc 7 | } 8 | 9 | .hljs-comment, .hljs-quote { 10 | color: #bc9458; 11 | font-style: italic 12 | } 13 | 14 | .hljs-keyword, .hljs-selector-tag { 15 | color: #c26230 16 | } 17 | 18 | .hljs-string, .hljs-number, .hljs-regexp, .hljs-variable, .hljs-template-variable { 19 | color: #a5c261 20 | } 21 | 22 | .hljs-subst { 23 | color: #519f50 24 | } 25 | 26 | .hljs-tag, .hljs-name { 27 | color: #e8bf6a 28 | } 29 | 30 | .hljs-type { 31 | color: #da4939 32 | } 33 | 34 | .hljs-symbol, .hljs-bullet, .hljs-built_in, .hljs-builtin-name, .hljs-attr, .hljs-link { 35 | color: #6d9cbe 36 | } 37 | 38 | .hljs-params { 39 | color: #d0d0ff 40 | } 41 | 42 | .hljs-attribute { 43 | color: #cda869 44 | } 45 | 46 | .hljs-meta { 47 | color: #9b859d 48 | } 49 | 50 | .hljs-title, .hljs-section { 51 | color: #ffc66d 52 | } 53 | 54 | .hljs-addition { 55 | background-color: #144212; 56 | color: #e6e1dc; 57 | display: inline-block; 58 | width: 100% 59 | } 60 | 61 | .hljs-deletion { 62 | background-color: #600; 63 | color: #e6e1dc; 64 | display: inline-block; 65 | width: 100% 66 | } 67 | 68 | .hljs-selector-class { 69 | color: #9b703f 70 | } 71 | 72 | .hljs-selector-id { 73 | color: #8b98ab 74 | } 75 | 76 | .hljs-emphasis { 77 | font-style: italic 78 | } 79 | 80 | .hljs-strong { 81 | font-weight: bold 82 | } 83 | 84 | .hljs-link { 85 | text-decoration: underline 86 | } -------------------------------------------------------------------------------- /mapsapp/static/mapsapp/css/tagsinput.css: -------------------------------------------------------------------------------- 1 | /* 2 | * bootstrap-tagsinput v0.8.0 3 | * 4 | */ 5 | 6 | .twitter-typeahead .tt-query, 7 | .twitter-typeahead .tt-hint { 8 | margin-bottom: 0; 9 | } 10 | 11 | .twitter-typeahead .tt-hint 12 | { 13 | display: none; 14 | } 15 | 16 | .tt-menu { 17 | position: absolute; 18 | top: 100%; 19 | left: 0; 20 | z-index: 1000; 21 | display: none; 22 | float: left; 23 | min-width: 160px; 24 | padding: 5px 0; 25 | margin: 2px 0 0; 26 | list-style: none; 27 | font-size: 14px; 28 | background-color: #ffffff; 29 | border: 1px solid #cccccc; 30 | border: 1px solid rgba(0, 0, 0, 0.15); 31 | border-radius: 4px; 32 | -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); 33 | box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); 34 | background-clip: padding-box; 35 | cursor: pointer; 36 | } 37 | 38 | .tt-suggestion { 39 | display: block; 40 | padding: 3px 20px; 41 | clear: both; 42 | font-weight: normal; 43 | line-height: 1.428571429; 44 | color: #333333; 45 | white-space: nowrap; 46 | } 47 | 48 | .tt-suggestion:hover, 49 | .tt-suggestion:focus { 50 | color: #ffffff; 51 | text-decoration: none; 52 | outline: 0; 53 | background-color: #428bca; 54 | } 55 | 56 | .bootstrap-tagsinput { 57 | background-color: #fff; 58 | border: 1px solid #ccc; 59 | box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); 60 | display: inline-block; 61 | padding: 4px 6px; 62 | color: #555; 63 | vertical-align: middle; 64 | border-radius: 4px; 65 | width: 100%; 66 | line-height: 22px; 67 | cursor: text; 68 | } 69 | 70 | .bootstrap-tagsinput input { 71 | border: none; 72 | box-shadow: none; 73 | outline: none; 74 | background-color: transparent; 75 | padding: 0 6px; 76 | margin: 0; 77 | width: auto; 78 | max-width: inherit; 79 | } 80 | 81 | .bootstrap-tagsinput.form-control input::-moz-placeholder { 82 | color: #777; 83 | opacity: 1; 84 | } 85 | 86 | .bootstrap-tagsinput.form-control input:-ms-input-placeholder { 87 | color: #777; 88 | } 89 | 90 | .bootstrap-tagsinput.form-control input::-webkit-input-placeholder { 91 | color: #777; 92 | } 93 | 94 | .bootstrap-tagsinput input:focus { 95 | border: none; 96 | box-shadow: none; 97 | } 98 | 99 | .bootstrap-tagsinput .badge { 100 | margin-right: 2px; 101 | color: white; 102 | background-color: #0275d8; 103 | padding: 5px 8px; 104 | border-radius: 3px; 105 | border: 1px solid #01649e 106 | } 107 | 108 | .bootstrap-tagsinput .badge [data-role="remove"] { 109 | margin-left: 8px; 110 | cursor: pointer; 111 | } 112 | 113 | .bootstrap-tagsinput .badge [data-role="remove"]:after { 114 | content: "×"; 115 | padding: 0px 4px; 116 | background-color: rgba(0, 0, 0, 0.1); 117 | border-radius: 50%; 118 | font-size: 13px 119 | } 120 | 121 | .bootstrap-tagsinput .badge [data-role="remove"]:hover:after { 122 | 123 | background-color: rgba(0, 0, 0, 0.62); 124 | } 125 | 126 | .bootstrap-tagsinput .badge [data-role="remove"]:hover:active { 127 | box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); 128 | } 129 | -------------------------------------------------------------------------------- /mapsapp/static/mapsapp/fonts/lato-v14-latin-700.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiegeEngineers/aoe2map/d7d72597ec0c9a4bbf6eb83f3161335730d8daca/mapsapp/static/mapsapp/fonts/lato-v14-latin-700.eot -------------------------------------------------------------------------------- /mapsapp/static/mapsapp/fonts/lato-v14-latin-700.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiegeEngineers/aoe2map/d7d72597ec0c9a4bbf6eb83f3161335730d8daca/mapsapp/static/mapsapp/fonts/lato-v14-latin-700.ttf -------------------------------------------------------------------------------- /mapsapp/static/mapsapp/fonts/lato-v14-latin-700.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiegeEngineers/aoe2map/d7d72597ec0c9a4bbf6eb83f3161335730d8daca/mapsapp/static/mapsapp/fonts/lato-v14-latin-700.woff -------------------------------------------------------------------------------- /mapsapp/static/mapsapp/fonts/lato-v14-latin-700.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiegeEngineers/aoe2map/d7d72597ec0c9a4bbf6eb83f3161335730d8daca/mapsapp/static/mapsapp/fonts/lato-v14-latin-700.woff2 -------------------------------------------------------------------------------- /mapsapp/static/mapsapp/fonts/lato-v14-latin-italic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiegeEngineers/aoe2map/d7d72597ec0c9a4bbf6eb83f3161335730d8daca/mapsapp/static/mapsapp/fonts/lato-v14-latin-italic.eot -------------------------------------------------------------------------------- /mapsapp/static/mapsapp/fonts/lato-v14-latin-italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiegeEngineers/aoe2map/d7d72597ec0c9a4bbf6eb83f3161335730d8daca/mapsapp/static/mapsapp/fonts/lato-v14-latin-italic.ttf -------------------------------------------------------------------------------- /mapsapp/static/mapsapp/fonts/lato-v14-latin-italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiegeEngineers/aoe2map/d7d72597ec0c9a4bbf6eb83f3161335730d8daca/mapsapp/static/mapsapp/fonts/lato-v14-latin-italic.woff -------------------------------------------------------------------------------- /mapsapp/static/mapsapp/fonts/lato-v14-latin-italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiegeEngineers/aoe2map/d7d72597ec0c9a4bbf6eb83f3161335730d8daca/mapsapp/static/mapsapp/fonts/lato-v14-latin-italic.woff2 -------------------------------------------------------------------------------- /mapsapp/static/mapsapp/fonts/lato-v14-latin-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiegeEngineers/aoe2map/d7d72597ec0c9a4bbf6eb83f3161335730d8daca/mapsapp/static/mapsapp/fonts/lato-v14-latin-regular.eot -------------------------------------------------------------------------------- /mapsapp/static/mapsapp/fonts/lato-v14-latin-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiegeEngineers/aoe2map/d7d72597ec0c9a4bbf6eb83f3161335730d8daca/mapsapp/static/mapsapp/fonts/lato-v14-latin-regular.ttf -------------------------------------------------------------------------------- /mapsapp/static/mapsapp/fonts/lato-v14-latin-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiegeEngineers/aoe2map/d7d72597ec0c9a4bbf6eb83f3161335730d8daca/mapsapp/static/mapsapp/fonts/lato-v14-latin-regular.woff -------------------------------------------------------------------------------- /mapsapp/static/mapsapp/fonts/lato-v14-latin-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiegeEngineers/aoe2map/d7d72597ec0c9a4bbf6eb83f3161335730d8daca/mapsapp/static/mapsapp/fonts/lato-v14-latin-regular.woff2 -------------------------------------------------------------------------------- /mapsapp/static/mapsapp/images/HillFort_Preview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiegeEngineers/aoe2map/d7d72597ec0c9a4bbf6eb83f3161335730d8daca/mapsapp/static/mapsapp/images/HillFort_Preview.jpg -------------------------------------------------------------------------------- /mapsapp/static/mapsapp/images/empty-de-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiegeEngineers/aoe2map/d7d72597ec0c9a4bbf6eb83f3161335730d8daca/mapsapp/static/mapsapp/images/empty-de-0.png -------------------------------------------------------------------------------- /mapsapp/static/mapsapp/images/empty-de-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiegeEngineers/aoe2map/d7d72597ec0c9a4bbf6eb83f3161335730d8daca/mapsapp/static/mapsapp/images/empty-de-1.png -------------------------------------------------------------------------------- /mapsapp/static/mapsapp/images/empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiegeEngineers/aoe2map/d7d72597ec0c9a4bbf6eb83f3161335730d8daca/mapsapp/static/mapsapp/images/empty.png -------------------------------------------------------------------------------- /mapsapp/static/mapsapp/images/load.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiegeEngineers/aoe2map/d7d72597ec0c9a4bbf6eb83f3161335730d8daca/mapsapp/static/mapsapp/images/load.gif -------------------------------------------------------------------------------- /mapsapp/static/mapsapp/images/tile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiegeEngineers/aoe2map/d7d72597ec0c9a4bbf6eb83f3161335730d8daca/mapsapp/static/mapsapp/images/tile.png -------------------------------------------------------------------------------- /mapsapp/static/mapsapp/js/FileSaver.js: -------------------------------------------------------------------------------- 1 | /* FileSaver.js 2 | * A saveAs() FileSaver implementation. 3 | * 1.3.8 4 | * 2018-03-22 14:03:47 5 | * 6 | * By Eli Grey, https://eligrey.com 7 | * License: MIT 8 | * See https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md 9 | */ 10 | 11 | /*global self */ 12 | /*jslint bitwise: true, indent: 4, laxbreak: true, laxcomma: true, smarttabs: true, plusplus: true */ 13 | 14 | /*! @source http://purl.eligrey.com/github/FileSaver.js/blob/master/src/FileSaver.js */ 15 | 16 | var saveAs = (function(view) { 17 | "use strict"; 18 | // IE <10 is explicitly unsupported 19 | if (typeof view === "undefined" || typeof navigator !== "undefined" && /MSIE [1-9]\./.test(navigator.userAgent)) { 20 | return; 21 | } 22 | var 23 | doc = view.document 24 | // only get URL when necessary in case Blob.js hasn't overridden it yet 25 | , get_URL = function() { 26 | return view.URL || view.webkitURL || view; 27 | } 28 | , save_link = doc.createElementNS("http://www.w3.org/1999/xhtml", "a") 29 | , can_use_save_link = "download" in save_link 30 | , click = function(node) { 31 | var event = new MouseEvent("click"); 32 | node.dispatchEvent(event); 33 | } 34 | , is_safari = /constructor/i.test(view.HTMLElement) || view.safari 35 | , is_chrome_ios =/CriOS\/[\d]+/.test(navigator.userAgent) 36 | , setImmediate = view.setImmediate || view.setTimeout 37 | , throw_outside = function(ex) { 38 | setImmediate(function() { 39 | throw ex; 40 | }, 0); 41 | } 42 | , force_saveable_type = "application/octet-stream" 43 | // the Blob API is fundamentally broken as there is no "downloadfinished" event to subscribe to 44 | , arbitrary_revoke_timeout = 1000 * 40 // in ms 45 | , revoke = function(file) { 46 | var revoker = function() { 47 | if (typeof file === "string") { // file is an object URL 48 | get_URL().revokeObjectURL(file); 49 | } else { // file is a File 50 | file.remove(); 51 | } 52 | }; 53 | setTimeout(revoker, arbitrary_revoke_timeout); 54 | } 55 | , dispatch = function(filesaver, event_types, event) { 56 | event_types = [].concat(event_types); 57 | var i = event_types.length; 58 | while (i--) { 59 | var listener = filesaver["on" + event_types[i]]; 60 | if (typeof listener === "function") { 61 | try { 62 | listener.call(filesaver, event || filesaver); 63 | } catch (ex) { 64 | throw_outside(ex); 65 | } 66 | } 67 | } 68 | } 69 | , auto_bom = function(blob) { 70 | // prepend BOM for UTF-8 XML and text/* types (including HTML) 71 | // note: your browser will automatically convert UTF-16 U+FEFF to EF BB BF 72 | if (/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(blob.type)) { 73 | return new Blob([String.fromCharCode(0xFEFF), blob], {type: blob.type}); 74 | } 75 | return blob; 76 | } 77 | , FileSaver = function(blob, name, no_auto_bom) { 78 | if (!no_auto_bom) { 79 | blob = auto_bom(blob); 80 | } 81 | // First try a.download, then web filesystem, then object URLs 82 | var 83 | filesaver = this 84 | , type = blob.type 85 | , force = type === force_saveable_type 86 | , object_url 87 | , dispatch_all = function() { 88 | dispatch(filesaver, "writestart progress write writeend".split(" ")); 89 | } 90 | // on any filesys errors revert to saving with object URLs 91 | , fs_error = function() { 92 | if ((is_chrome_ios || (force && is_safari)) && view.FileReader) { 93 | // Safari doesn't allow downloading of blob urls 94 | var reader = new FileReader(); 95 | reader.onloadend = function() { 96 | var url = is_chrome_ios ? reader.result : reader.result.replace(/^data:[^;]*;/, 'data:attachment/file;'); 97 | var popup = view.open(url, '_blank'); 98 | if(!popup) view.location.href = url; 99 | url=undefined; // release reference before dispatching 100 | filesaver.readyState = filesaver.DONE; 101 | dispatch_all(); 102 | }; 103 | reader.readAsDataURL(blob); 104 | filesaver.readyState = filesaver.INIT; 105 | return; 106 | } 107 | // don't create more object URLs than needed 108 | if (!object_url) { 109 | object_url = get_URL().createObjectURL(blob); 110 | } 111 | if (force) { 112 | view.location.href = object_url; 113 | } else { 114 | var opened = view.open(object_url, "_blank"); 115 | if (!opened) { 116 | // Apple does not allow window.open, see https://developer.apple.com/library/safari/documentation/Tools/Conceptual/SafariExtensionGuide/WorkingwithWindowsandTabs/WorkingwithWindowsandTabs.html 117 | view.location.href = object_url; 118 | } 119 | } 120 | filesaver.readyState = filesaver.DONE; 121 | dispatch_all(); 122 | revoke(object_url); 123 | } 124 | ; 125 | filesaver.readyState = filesaver.INIT; 126 | 127 | if (can_use_save_link) { 128 | object_url = get_URL().createObjectURL(blob); 129 | setImmediate(function() { 130 | save_link.href = object_url; 131 | save_link.download = name; 132 | click(save_link); 133 | dispatch_all(); 134 | revoke(object_url); 135 | filesaver.readyState = filesaver.DONE; 136 | }, 0); 137 | return; 138 | } 139 | 140 | fs_error(); 141 | } 142 | , FS_proto = FileSaver.prototype 143 | , saveAs = function(blob, name, no_auto_bom) { 144 | return new FileSaver(blob, name || blob.name || "download", no_auto_bom); 145 | } 146 | ; 147 | 148 | // IE 10+ (native saveAs) 149 | if (typeof navigator !== "undefined" && navigator.msSaveOrOpenBlob) { 150 | return function(blob, name, no_auto_bom) { 151 | name = name || blob.name || "download"; 152 | 153 | if (!no_auto_bom) { 154 | blob = auto_bom(blob); 155 | } 156 | return navigator.msSaveOrOpenBlob(blob, name); 157 | }; 158 | } 159 | 160 | // todo: detect chrome extensions & packaged apps 161 | //save_link.target = "_blank"; 162 | 163 | FS_proto.abort = function(){}; 164 | FS_proto.readyState = FS_proto.INIT = 0; 165 | FS_proto.WRITING = 1; 166 | FS_proto.DONE = 2; 167 | 168 | FS_proto.error = 169 | FS_proto.onwritestart = 170 | FS_proto.onprogress = 171 | FS_proto.onwrite = 172 | FS_proto.onabort = 173 | FS_proto.onerror = 174 | FS_proto.onwriteend = 175 | null; 176 | 177 | return saveAs; 178 | }( 179 | typeof self !== "undefined" && self 180 | || typeof window !== "undefined" && window 181 | || this 182 | )); 183 | -------------------------------------------------------------------------------- /mapsapp/static/mapsapp/js/add-to-collection.js: -------------------------------------------------------------------------------- 1 | function addToCollection(rmsId, collectionId) { 2 | $.post(COLLECTION_ENDPOINT, {"rms_id": rmsId, "collection_id": collectionId, "action": "add"}, function (response) { 3 | $('#alert-area').append(""); 9 | }, 'json'); 10 | } 11 | 12 | function csrfSafeMethod(method) { 13 | // these HTTP methods do not require CSRF protection 14 | return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method)); 15 | } 16 | 17 | $.ajaxSetup({ 18 | beforeSend: function (xhr, settings) { 19 | if (!csrfSafeMethod(settings.type) && !this.crossDomain) { 20 | xhr.setRequestHeader("X-CSRFToken", $("[name=csrfmiddlewaretoken]").val()); 21 | } 22 | } 23 | }); -------------------------------------------------------------------------------- /mapsapp/static/mapsapp/js/aoe2map.js: -------------------------------------------------------------------------------- 1 | function partition(array, chunksize) { 2 | let i, j, nr, temparray; 3 | let retval = []; 4 | for (nr = 0, i = 0, j = array.length; i < j; i += chunksize) { 5 | temparray = array.slice(i, i + chunksize); 6 | retval.push({number: nr, data: temparray}); 7 | nr++; 8 | } 9 | return retval; 10 | } 11 | 12 | function toggleMaxHeight(self) { 13 | $(self).toggleClass('full'); 14 | } 15 | 16 | function addAllMaps(data, selector = '.maps') { 17 | $(selector).empty(); 18 | if (data.maps.length === 0) { 19 | $('
No result :-(
').appendTo(selector); 20 | } 21 | let partitions = partition(data.maps, 10); 22 | for (let part of partitions) { 23 | setTimeout(function () { 24 | for (let map of part.data) { 25 | if (map.images.length === 0) { 26 | if (map.versiontags.includes("DE")) { 27 | const number = Math.floor(Math.random() * 2); 28 | map.images.push({ 29 | "url": `/static/mapsapp/images/empty-de-${number}.png`, 30 | "preview_url": null 31 | }); 32 | } else { 33 | map.images.push({"url": "/static/mapsapp/images/empty.png", "preview_url": null}); 34 | } 35 | } 36 | let alert = ''; 37 | if (map.newer_version !== null) { 38 | alert = '' 42 | } 43 | let url = ''; 44 | if (map.url) { 45 | url = 'Website'; 46 | } 47 | 48 | let imageUrl = map.images[0].url; 49 | if (map.images[0].preview_url !== null) { 50 | imageUrl = map.images[0].preview_url; 51 | } 52 | $('
\ 53 |
\ 54 | \ 55 | \ 56 | \ 57 |
\ 58 | ' + alert + '\ 59 |
' + map.name + ' ' + map.version + '
\ 60 |
by ' + map.authors + '
\ 61 |

' + map.description + '

\ 62 |

\ 63 |

\ 64 | Download map\ 65 | \ 69 | \ 81 |
\ 82 | ' + url + '\ 83 |

\ 84 |
Tags: \ 85 | ' + getTags(map.tags) + '\ 86 |
\ 87 |
Versions: \ 88 | ' + getVersiontags(map.versiontags) + '\ 89 |
\ 90 |
\ 91 |
\ 92 |
').appendTo(selector); 93 | } 94 | }, part.number * 100); 95 | } 96 | setTimeout(()=>{initViewCode(); initCustomVersions();}, partitions.length * 100); 97 | } 98 | 99 | function getTags(tags) { 100 | let retval = ""; 101 | for (let tag of tags) { 102 | retval += '' + tag.name + ' '; 103 | } 104 | return retval; 105 | } 106 | 107 | function getVersiontags(tags) { 108 | let retval = ""; 109 | for (let tag of tags) { 110 | retval += '' + tag + ' '; 111 | } 112 | return retval; 113 | } 114 | 115 | $(function () { 116 | if (API_URL !== '') { 117 | $.getJSON(API_URL, function (data) { 118 | addAllMaps(data); 119 | }); 120 | } 121 | if (LATEST_MAPS_URL !== '') { 122 | $.getJSON(LATEST_MAPS_URL, function (data) { 123 | addAllMaps(data, '.latestmaps'); 124 | }); 125 | } 126 | if (LATEST_UPDATED_MAPS_URL !== '') { 127 | $.getJSON(LATEST_UPDATED_MAPS_URL, function (data) { 128 | addAllMaps(data, '.latestupdatedmaps'); 129 | }); 130 | } 131 | }); -------------------------------------------------------------------------------- /mapsapp/static/mapsapp/js/collection.js: -------------------------------------------------------------------------------- 1 | function normalizeName(name) { 2 | name = name.replace(/%40/g, "@"); 3 | return name.replace(/[^@A-Za-z0-9\-_ ()\[\].+]/g, "_"); 4 | } 5 | 6 | let collectionDownloadFunction = (folderPrefix, includeMetadataFile) => function () { 7 | let zip = new JSZip(); 8 | let promises = []; 9 | let mapUrls = $('.map-download'); 10 | let collectionName = $('#collection-name').text() || $('#map-name').text(); 11 | let authorNames = $('#author-names').text(); 12 | let description = $('#description').text(); 13 | let i = 0; 14 | let names = new Set(); 15 | for (let mapUrl of mapUrls) { 16 | i++; 17 | let filename = normalizeName(decodeURI(mapUrl.href.substring(mapUrl.href.lastIndexOf('/') + 1))); 18 | if (names.has(filename)) { 19 | filename = normalizeName(i.toString(10) + "-" + filename); 20 | } 21 | names.add(filename); 22 | 23 | promises.push( 24 | (function () { 25 | let url = mapUrl.href; 26 | let name = filename; 27 | return fetch(url) 28 | .then(response => response.arrayBuffer()) 29 | .then(buffer => { 30 | zip.file(folderPrefix + name, buffer); 31 | }) 32 | .catch(err => console.error(err)); 33 | })() 34 | ); 35 | } 36 | if (includeMetadataFile) { 37 | zip.file('info.json', JSON.stringify({Author: authorNames, Description: description, Title: collectionName})); 38 | } 39 | 40 | $.when.apply($, promises).done(function () { 41 | zip.generateAsync({type: "blob"}) 42 | .then(function (content) { 43 | saveAs(content, normalizeName(collectionName + ".zip")); 44 | }); 45 | }).fail(function () { 46 | alert('Download failed due to technical reasons. Sorry!'); 47 | }); 48 | }; 49 | 50 | $('#download').click(collectionDownloadFunction('', false)); 51 | $('#deModButton').click(collectionDownloadFunction('resources/_common/random-map-scripts/', true)); 52 | $('#wkVooblyModButton').click(collectionDownloadFunction('Voobly Mods/AOC/Data Mods/WololoKingdoms/Script.Rm/', false)); 53 | 54 | $('.roll-random-item').click(function () { 55 | $('#modal-scouting').slideDown(500); 56 | $('#modal-result').slideUp(500); 57 | $('#random_choice_modal .modal-footer').hide(); 58 | 59 | $('#random_choice_modal').modal(); 60 | 61 | setTimeout(() => { 62 | const maps = $('.maps .card'); 63 | const index = Math.floor(Math.random() * maps.length); 64 | $('#modal-result .image').empty(); 65 | $('#modal-result .description').empty(); 66 | $($(maps[index]).find('a').get(0)).clone().appendTo('#modal-result .image'); 67 | $(maps[index]).find('.card-body').clone().appendTo('#modal-result .description'); 68 | 69 | $('#modal-scouting').slideUp(500); 70 | $('#modal-result').slideDown(500); 71 | $('#random_choice_modal .modal-footer').slideDown(500); 72 | }, 3000); 73 | }); -------------------------------------------------------------------------------- /mapsapp/static/mapsapp/js/custom-versions.js: -------------------------------------------------------------------------------- 1 | $(function () {initCustomVersions();}); 2 | function initCustomVersions() { 3 | $('.x256TechButton').unbind().click(function (event) { 4 | let url = $(event.target).closest('.btn-group').find('.map-download').attr('href'); 5 | let mapName = $(event.target).closest('.map-info').find('.card-title a').text(); 6 | let filename = getFilename(url); 7 | $.ajax({ 8 | url: url, 9 | beforeSend: function (xhr) { 10 | xhr.overrideMimeType("text/plain; charset=x-user-defined"); 11 | } 12 | }).done(function (data) { 13 | if (data.startsWith('PK\x03\x04')) { 14 | const items = filename.split('@', 2); 15 | filename = items.length === 2 ? 'ZR@256x_' + items[1] : 'ZR@256x_' + items[0]; 16 | downloadPatchedZipFile(data, filename, mapName, patchWith256xTech); 17 | } else { 18 | downloadPatchedRmsFile(data, '256x_' + filename, mapName, patchWith256xTech); 19 | } 20 | }).fail(function () { 21 | alert("Oops! Could not download rms script."); 22 | }); 23 | }); 24 | 25 | $('.explodingVillagersButton').unbind().click(function (event) { 26 | let url = $(event.target).closest('.btn-group').find('.map-download').attr('href'); 27 | let mapName = $(event.target).closest('.map-info').find('.card-title a').text(); 28 | let filename = getFilename(url); 29 | $.ajax({ 30 | url: url, 31 | beforeSend: function (xhr) { 32 | xhr.overrideMimeType("text/plain; charset=x-user-defined"); 33 | } 34 | }).done(function (data) { 35 | if (data.startsWith('PK\x03\x04')) { 36 | const items = filename.split('@', 2); 37 | filename = items.length === 2 ? 'ZR@EV_' + items[1] : 'ZR@EV_' + items[0]; 38 | downloadPatchedZipFile(data, filename, mapName, patchWithExplodingVillagers); 39 | } else { 40 | downloadPatchedRmsFile(data, 'EV_' + filename, mapName, patchWithExplodingVillagers); 41 | } 42 | }).fail(function () { 43 | alert("Oops! Could not download rms script."); 44 | }); 45 | }); 46 | 47 | $('.suddenDeathButton').unbind().click(function (event) { 48 | let url = $(event.target).closest('.btn-group').find('.map-download').attr('href'); 49 | let mapName = $(event.target).closest('.map-info').find('.card-title a').text(); 50 | let filename = getFilename(url); 51 | $.ajax({ 52 | url: url, 53 | beforeSend: function (xhr) { 54 | xhr.overrideMimeType("text/plain; charset=x-user-defined"); 55 | } 56 | }).done(function (data) { 57 | if (data.startsWith('PK\x03\x04')) { 58 | const items = filename.split('@', 2); 59 | filename = items.length === 2 ? 'ZR@SD_' + items[1] : 'ZR@SD_' + items[0]; 60 | downloadPatchedZipFile(data, filename, mapName, patchWithSuddenDeath); 61 | } else { 62 | downloadPatchedRmsFile(data, 'SD_' + filename, mapName, patchWithSuddenDeath); 63 | } 64 | }).fail(function () { 65 | alert("Oops! Could not download rms script."); 66 | }); 67 | }); 68 | } 69 | 70 | function getFilename(url) { 71 | const items = url.split('/'); 72 | return decodeURI(items[items.length - 1]).replace('%40', '@'); 73 | } 74 | 75 | function patchWith256xTech(content, mapName) { 76 | content = content.replace(//g, `/* 256x tech patch */ 77 | #includeXS x256tech.xs 78 | /* 256x tech patch end */ 79 | 80 | `); 81 | content = '/* 256x tech ' + mapName + ' */\n' + 82 | '/* auto-generated on aoe2map.net */\n\n' + content; 83 | return content; 84 | } 85 | 86 | function patchWithSuddenDeath(content, mapName) { 87 | if (content.includes('guard_state')) { 88 | alert('This map already contains a guard_state command.\nSorry, we can\'t patch it automatically.'); 89 | return null; 90 | } 91 | content = content.replace(//g, `/* Sudden Death patch part 1 of 2 start */ 92 | #const TOWN_CENTER 109 93 | #const RI_TOWN_CENTER 187 94 | /* Sudden Death patch part 1 of 2 end */ 95 | 96 | 97 | /* Sudden Death patch part 2 of 2 start */ 98 | guard_state TOWN_CENTER AMOUNT_GOLD 0 1 99 | effect_amount ENABLE_TECH RI_TOWN_CENTER ATTR_DISABLE 187 100 | /* Sudden Death patch part 2 of 2 end */\n`); 101 | content = '/* Sudden Death ' + mapName + ' */\n' + 102 | '/* auto-generated on aoe2map.net */\n\n' + content; 103 | return content; 104 | } 105 | 106 | function patchWithExplodingVillagers(content, mapName) { 107 | content = content.replace(//g, ` 108 | /* Exploding villagers patch start */ 109 | effect_amount SET_ATTRIBUTE VILLAGER_CLASS ATTR_DEAD_ID 706 110 | effect_amount SET_ATTRIBUTE SABOTEUR ATTR_HITPOINTS 0 111 | effect_amount SET_ATTRIBUTE SABOTEUR ATTR_ATTACK 50 112 | effect_amount SET_ATTRIBUTE SABOTEUR ATTR_ATTACK 346 113 | effect_amount SET_ATTRIBUTE SABOTEUR ATTR_ATTACK 512 114 | effect_amount SET_ATTRIBUTE SABOTEUR ATTR_MAX_RANGE 2 115 | effect_amount SET_ATTRIBUTE SABOTEUR ATTR_BLAST_LEVEL 1 116 | /* Exploding villagers patch end */\n`); 117 | content = '/* Exploding Villagers ' + mapName + ' */\n' + 118 | '/* auto-generated on aoe2map.net */\n\n' + content; 119 | return content; 120 | } 121 | 122 | function getMapName(){ 123 | return $('.card-title a').text(); 124 | } 125 | 126 | function downloadPatchedRmsFile(content, filename, mapName, patch) { 127 | content = patch(content, mapName); 128 | if (content === null) { 129 | return; 130 | } 131 | const blob = new Blob([content], {type: "text/plain;charset=utf-8"}); 132 | saveAs(blob, filename); 133 | } 134 | 135 | function downloadPatchedZipFile(data, zipFilename, mapName, patch) { 136 | JSZip.loadAsync(data).then(function (d) { 137 | for (let filename in d.files) { 138 | if (d.files.hasOwnProperty(filename)) { 139 | if (filename.endsWith('.rms')) { 140 | let currentRmsFileName = filename; 141 | d.file(filename).async('text').then(function (content) { 142 | content = patch(content, mapName); 143 | if (content === null) { 144 | return; 145 | } 146 | d.file(filename, content); 147 | d.generateAsync({type: "blob"}).then(function (blob) { 148 | saveAs(blob, zipFilename); 149 | }); 150 | }); 151 | return; 152 | } 153 | } 154 | } 155 | alert("No .rms file found inside the archive!"); 156 | }); 157 | } -------------------------------------------------------------------------------- /mapsapp/static/mapsapp/js/fileinputs-drop.js: -------------------------------------------------------------------------------- 1 | let target = document.documentElement; 2 | let body = document.body; 3 | let fileInput = document.querySelector('#id_file'); 4 | let imageInput = document.querySelector('#id_images'); 5 | 6 | window.addEventListener('dragover', (e) => { 7 | e.preventDefault(); 8 | body.classList.add('dragging'); 9 | }); 10 | window.addEventListener('dragleave', (e) => { 11 | if (e.pageX !== 0 && e.pageY !== 0) { 12 | return false; 13 | } 14 | body.classList.remove('dragging'); 15 | }); 16 | 17 | $('#drop-info').click(function(){ 18 | body.classList.remove('dragging'); 19 | }); 20 | 21 | target.addEventListener('drop', (e) => { 22 | e.preventDefault(); 23 | body.classList.remove('dragging'); 24 | 25 | if (e.dataTransfer.files.length > 0) { 26 | let firstFile = e.dataTransfer.files[0]; 27 | if (ACCEPT_DROP.includes('image') && hasImageExtension(firstFile.name)) { 28 | addImageFiles(e.dataTransfer.files); 29 | } 30 | if (ACCEPT_DROP.includes('rms') && hasRmsExtension(firstFile.name)) { 31 | addRmsFile(e.dataTransfer.files); 32 | } 33 | } else { 34 | console.log("no files found for drop event"); 35 | } 36 | }); 37 | 38 | function addImageFiles(files) { 39 | for (let i = 0; i < files.length; i++) { 40 | if (!hasImageExtension(files[i].name)) { 41 | showAlert("warning", "Please drop only one single rms file or only images file(s) at a time!"); 42 | return; 43 | } 44 | } 45 | let warning = " "; 46 | if (imageInput.files.length > 0) { 47 | warning += "" + imageInput.files.length + " Previously added images have been removed!"; 48 | } 49 | imageInput.files = files; 50 | showAlert("info", files.length + " dropped images have been put into this form." + warning); 51 | 52 | } 53 | 54 | function addRmsFile(files) { 55 | if (files.length > 1) { 56 | showDropCompositionWarning(); 57 | } else { 58 | fileInput.files = files; 59 | showAlert("info", "The rms file in this form has been updated."); 60 | } 61 | 62 | } 63 | 64 | function hasImageExtension(name) { 65 | let nameLower = name.toLowerCase(); 66 | for (let ext of [".png", ".jpg", ".jpeg", ".bmp"]) { 67 | if (nameLower.endsWith(ext)) { 68 | return true; 69 | } 70 | } 71 | return false; 72 | } 73 | 74 | function hasRmsExtension(name) { 75 | return name.toLowerCase().endsWith(".rms"); 76 | } 77 | 78 | function showDropCompositionWarning() { 79 | showAlert("warning", "Please drop only one single rms file or only images file(s) at a time!"); 80 | } 81 | 82 | function showAlert(type, message) { 83 | $('#alert-area').prepend(''); 89 | } -------------------------------------------------------------------------------- /mapsapp/static/mapsapp/js/highlight.js-rms.js: -------------------------------------------------------------------------------- 1 | /* 2 | Language: RMS 3 | Requires: - 4 | Author: HSZemi 5 | Contributors: - 6 | Description: Age of Empires 2 Random Map Scripting language 7 | */ 8 | 9 | function rmslanguage(hljs) { 10 | return { 11 | keywords: { 12 | keyword: 'if elseif else endif random_placement start_random end_random percent_chance', 13 | symbol: 'ai_info_map_type base_terrain cliff_curliness create_elevation create_land create_object create_player_lands create_terrain direct_placement effect_amount effect_percent grouped_by_team guard_state max_length_of_cliff max_number_of_cliffs min_distance_cliffs min_length_of_cliff min_number_of_cliffs min_terrain_distance nomad_resources random_placement reate_connect_all_players_land terrain_state weather_type', 14 | params: 'assign_to assign_to_player base_elevation base_size base_terrain border_fuzziness bottom_border clumping_factor default_terrain_replacement group_placement_radius group_variance height_limits land_id land_percent land_position left_border max_distance_to_other_zones max_distance_to_players min_distance_group_placement min_distance_to_players number_of_clumps number_of_groups number_of_objects number_of_tiles other_zone_avoidance_distance place_on_specific_land_id replace_terrain resource_delta right_border set_avoid_player_start_areas set_flat_terrain_only set_gaia_object_only set_loose_grouping set_place_for_every_player set_scale_by_groups set_scale_by_size set_scaling_to_map_size set_scaling_to_player_number set_tight_grouping set_zone_by_team set_zone_randomly spacing spacing_to_other_terrain_types temp_min_distance_group_placement terrain_cost terrain_size terrain_to_place_on terrain_type top_border type zone' 15 | }, 16 | contains: [ 17 | hljs.C_BLOCK_COMMENT_MODE, 18 | hljs.C_NUMBER_MODE, 19 | { 20 | className: 'title', 21 | begin: '<', 22 | end: '>', 23 | }, 24 | { 25 | className: 'name', 26 | begin: '[A-Z][A-Z_0-9]*', 27 | relevance: 0 28 | }, 29 | { 30 | className: 'meta', 31 | begin: /#(define|const|include_drs)/, 32 | } 33 | ] 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /mapsapp/static/mapsapp/js/highlightjs-line-numbers.min.js: -------------------------------------------------------------------------------- 1 | !function(n,e){"use strict";function t(){var n=e.createElement("style");n.type="text/css",n.innerHTML=h(".{0}{border-collapse:collapse}.{0} td{padding:0}.{1}:before{content:attr({2})}",[f,m,j]),e.getElementsByTagName("head")[0].appendChild(n)}function r(t){"complete"===e.readyState?l(t):n.addEventListener("DOMContentLoaded",function(){l(t)})}function l(t){try{var r=e.querySelectorAll("code.hljs");for(var l in r)r.hasOwnProperty(l)&&i(r[l],t)}catch(o){n.console.error("LineNumbers error: ",o)}}function i(n,e){if("object"==typeof n){e=e||{singleLine:!1};var t=e.singleLine?0:1;u(function(){s(n),n.innerHTML=o(n.innerHTML,t)})}}function o(n,e){var t=c(n);if(""===t[t.length-1].trim()&&t.pop(),t.length>e){for(var r="",l=0,i=t.length;l
{6}
',[v,g,m,j,p,l+1,t[l].length>0?t[l]:" "]);return h('{1}
',[f,r])}return n}function s(n){var e=n.childNodes;for(var t in e)if(e.hasOwnProperty(t)){var r=e[t];d(r.textContent)>0&&(r.childNodes.length>0?s(r):a(r.parentNode))}}function a(n){var e=n.className;if(/hljs-/.test(e)){for(var t=c(n.innerHTML),r=0,l="";r{1}\n',[e,t[r]]);n.innerHTML=l.trim()}}function c(n){return 0===n.length?[]:n.split(L)}function d(n){return(n.trim().match(L)||[]).length}function u(e){n.setTimeout(e,0)}function h(n,e){return n.replace(/\{(\d+)\}/g,function(n,t){return e[t]?e[t]:n})}var f="hljs-ln",g="hljs-ln-line",p="hljs-ln-code",v="hljs-ln-numbers",m="hljs-ln-n",j="data-line-number",L=/\r\n|\r|\n/g;n.hljs?(n.hljs.initLineNumbersOnLoad=r,n.hljs.lineNumbersBlock=i,t()):n.console.error("highlight.js not detected!")}(window,document); -------------------------------------------------------------------------------- /mapsapp/static/mapsapp/js/list-filter.js: -------------------------------------------------------------------------------- 1 | function applyFilter() { 2 | let input, filter, lists, list, li, a, i; 3 | input = document.getElementById('filterInput'); 4 | filter = input.value.toLowerCase(); 5 | lists = document.querySelectorAll(".maplist,.mapslist"); 6 | for (list of lists) { 7 | li = list.getElementsByTagName('li'); 8 | 9 | for (i = 0; i < li.length; i++) { 10 | a = li[i].getElementsByTagName("a")[0]; 11 | if (a.innerHTML.toLowerCase().indexOf(filter) > -1) { 12 | li[i].style.display = ""; 13 | } else { 14 | li[i].style.display = "none"; 15 | } 16 | } 17 | } 18 | } 19 | 20 | document.getElementById('filterInput').addEventListener('keyup', function () { 21 | applyFilter(); 22 | }); 23 | 24 | applyFilter(); 25 | -------------------------------------------------------------------------------- /mapsapp/static/mapsapp/js/mappack.js: -------------------------------------------------------------------------------- 1 | window.addEventListener("dragover", function (e) { 2 | e = e || event; 3 | e.preventDefault(); 4 | }, false); 5 | window.addEventListener("drop", function (e) { 6 | e = e || event; 7 | e.preventDefault(); 8 | }, false); 9 | 10 | function dropHandler(ev) { 11 | ev.preventDefault(); 12 | for (var i = 0; i < ev.dataTransfer.files.length; i++) { 13 | let file = ev.dataTransfer.files[i]; 14 | handleRmsFile(file); 15 | } 16 | removeDragData(ev) 17 | } 18 | 19 | function handleFiles(filelist) { 20 | for (let file of filelist) { 21 | handleRmsFile(file); 22 | } 23 | } 24 | 25 | function recalculatePercentages() { 26 | let percentages = $('.percentage'); 27 | let chances = getChances(percentages.length); 28 | for (let i = 0; i < percentages.length; i++) { 29 | $(percentages[i]).val(chances[i]); 30 | } 31 | } 32 | 33 | function handleRmsFile(file) { 34 | if (file.name.endsWith(".rms")) { 35 | let reader = new FileReader(); 36 | reader.onload = function (e) { 37 | let contents = e.target.result; 38 | let warnings = []; 39 | if (contents.startsWith('PK\x03\x04')) { 40 | $('
  • \ 41 |
    ' + file.name + '
    \ 42 |

    Unfortunately, you cannot add ZR maps to a map pack.

    \ 43 |
  • ').appendTo('#filelist'); 44 | } else { 45 | restructureMap(contents, "", [], warnings); 46 | console.log(warnings); 47 | let clazz = 'success'; 48 | let text = 'Looks good!'; 49 | if (warnings.length > 0) { 50 | clazz = 'warning'; 51 | text = "This map might not be suitable for usage in a map pack and break the map pack you are about to create. Problems: \ 52 |
    • " + warnings.join("
    • \n
    • ") + "
    "; 53 | } 54 | $('
  • \ 55 |
    \ 56 |
    \ 57 |
    ' + file.name + '
    \ 58 |

    ' + text + '

    \ 59 |
    \ 60 |
    \ 61 | \ 62 |
    \ 63 | \ 64 |
    \ 65 | %\ 66 |
    \ 67 |
    \ 68 |
    \ 69 |
    \ 70 |
  • ').appendTo('#filelist').data('map', contents); 71 | } 72 | recalculatePercentages(); 73 | }; 74 | reader.readAsText(file); 75 | } else { 76 | $('#filelist').append('
  • ' + file.name + " is not a .rms file
  • "); 77 | } 78 | } 79 | 80 | function getdata() { 81 | let allButtons = $('.map'); 82 | let startData = 'start_random\n'; 83 | let percentage_sum = 0; 84 | for (let i = 0; i < allButtons.length; i++) { 85 | let prefix = "M" + i + "_"; 86 | let mapname = $(allButtons.get(i)).find('.mapname').text(); 87 | let percentage = parseInt($(allButtons.get(i)).find('.percentage').val()); 88 | if (!isNaN(percentage)) { 89 | percentage_sum += percentage; 90 | } else { 91 | alert("Error: Percentage for map '" + mapname + "' is not a number"); 92 | return; 93 | } 94 | startData += "percent_chance " + percentage + " #define " + marker(prefix, mapname) + "\n" 95 | } 96 | startData += 'end_random\n'; 97 | 98 | if (percentage_sum !== 100) { 99 | alert("Error: Sum of all percentages should be 100, but is " + percentage_sum); 100 | return; 101 | } 102 | 103 | 104 | let constLines = []; 105 | 106 | let mapsData = ""; 107 | 108 | allButtons.each(function (i, it) { 109 | let ifVariant = "elseif"; 110 | if (i === 0) { 111 | ifVariant = "if"; 112 | } 113 | let prefix = "M" + i + "_"; 114 | mapsData += "\n\n" + ifVariant + " " + marker(prefix, $(it).find('.mapname').text()) + "\n\n" + restructureMap($(it).data('map'), prefix, constLines, []); 115 | }); 116 | mapsData += "\n\nendif"; 117 | 118 | let wholeData = startData + "\n" + constLines.join("\n") + "\n" + mapsData; 119 | 120 | download("mappack.rms", wholeData); 121 | } 122 | 123 | function getChances(amount) { 124 | let a = []; 125 | let sum = 0; 126 | for (let i = 0; i < amount; i++) { 127 | let percentage = Math.floor(100 / amount); 128 | a.push(percentage); 129 | sum += percentage; 130 | } 131 | for (let i = 0; i < 100 - sum; i++) { 132 | a[i]++; 133 | } 134 | return a; 135 | } 136 | 137 | function marker(prefix, name) { 138 | let m = prefix + normalizeConstant(name); 139 | return m; 140 | } 141 | 142 | function normalizeConstant(name) { 143 | name = name.substr(0, name.length - 4); 144 | name = name.toUpperCase(); 145 | name = name.replace(/\s+/g, "_"); 146 | name = name.replace(/[^A-Z0-9_]/g, "X"); 147 | return name; 148 | } 149 | 150 | function normalizeMap(mapcontent, prefix) { 151 | mapcontent = mapcontent.replace(/#include_drs\s+random_map.def/g, "#include_drsrandom_mapdef"); 152 | let commentMatches = mapcontent.match(/\n?\/\*.*?\*\//g); 153 | if (commentMatches !== null) { 154 | for (let i = 0; i < commentMatches.length; i++) { 155 | mapcontent = mapcontent.replace(commentMatches[i], `§comment§${i}§`); 156 | } 157 | } 158 | mapcontent = mapcontent.replace(/\r?\n/g, " "); 159 | mapcontent = mapcontent.replace(/\s+/g, " "); 160 | mapcontent = mapcontent.replace(/\s([a-z_]+)/g, "\n$1"); 161 | mapcontent = mapcontent.replace(/#/g, "\n#"); 162 | mapcontent = mapcontent.replace(/\{/g, "\n{"); 163 | mapcontent = mapcontent.replace(/\}/g, "\n}\n"); 164 | if (commentMatches !== null) { 165 | for (let i = 0; i < commentMatches.length; i++) { 166 | mapcontent = mapcontent.replace(`§comment§${i}§`, commentMatches[i]); 167 | } 168 | } 169 | mapcontent = mapcontent.replace(/\s+\*\//g, " */"); 170 | mapcontent = mapcontent.replace(/ \n/g, "\n"); 171 | mapcontent = mapcontent.replace(/#include_drsrandom_mapdef/g, "#include_drs random_map.def"); 172 | mapcontent = mapcontent.replace(/\s+\n/g, "\n"); 173 | mapcontent = mapcontent.replace(/ 1) { 205 | let expected = values[0]; 206 | for (let value of values) { 207 | if (value !== expected) { 208 | warnings.push("constant " + key + " is declared twice: '" + value + "' vs. '" + expected + "'"); 209 | } 210 | } 211 | } 212 | } 213 | 214 | 215 | let mapLines = []; 216 | let indent = 0; 217 | let lineNr = 0; 218 | let lineInError = 0; 219 | let latestIndent = 0; 220 | for (let line of lines) { 221 | lineNr++; 222 | line = addPrefixesToConsts(line, prefix, declaredConsts); 223 | if (line.startsWith("#const")) { 224 | constLines.push(line); 225 | } else { 226 | let indentBefore = getIndentBefore(line); 227 | let indentAfter = getIndentAfter(line); 228 | if (lineInError < 1 && indent + indentBefore < 0) { 229 | lineInError = lineNr; 230 | } 231 | mapLines.push(getIndent(indent + indentBefore) + line); 232 | indent += indentAfter; 233 | latestIndent = indent; 234 | } 235 | } 236 | if (lineInError < 1 && latestIndent !== 0) { 237 | lineInError = lineNr; 238 | } 239 | 240 | if (lineInError > 0) { 241 | warnings.push("The indentation revealed issues with your brackets or if/else statements."); 242 | } 243 | 244 | return mapLines.join("\n"); 245 | } 246 | 247 | function addPrefixesToConsts(line, prefix, declaredConsts) { 248 | for (let key of declaredConsts.keys()) { 249 | if (line.includes(key)) { 250 | let re = new RegExp("(\\s)" + key + "(\\s|{|$)", "g"); 251 | line = line.replace(re, "$1" + prefix + key + "$2"); 252 | } 253 | } 254 | return line; 255 | } 256 | 257 | function addWarning(message) { 258 | console.log(message); 259 | } 260 | 261 | function getIndentBefore(line) { 262 | if (line.startsWith("elseif")) { 263 | return -1; 264 | } 265 | if (line.startsWith("else")) { 266 | return -1; 267 | } 268 | if (line.startsWith("endif")) { 269 | return -1; 270 | } 271 | if (line.startsWith("end_random")) { 272 | return -1; 273 | } 274 | if (line.startsWith("}")) { 275 | return -1; 276 | } 277 | return 0; 278 | } 279 | 280 | function getIndentAfter(line) { 281 | if (line.startsWith("if")) { 282 | return 1; 283 | } 284 | if (line.startsWith("endif")) { 285 | return -1; 286 | } 287 | if (line.startsWith("{")) { 288 | return 1; 289 | } 290 | if (line.startsWith("}")) { 291 | return -1; 292 | } 293 | if (line.startsWith("start_random")) { 294 | return 1; 295 | } 296 | if (line.startsWith("end_random")) { 297 | return -1; 298 | } 299 | return 0; 300 | } 301 | 302 | function getIndent(i) { 303 | //if (i < 0) { return "XXXXXX"; } 304 | return " ".repeat(Math.max(i * 4, 0)); 305 | } 306 | 307 | function dragOverHandler(event) { 308 | $('#target').addClass('active'); 309 | } 310 | 311 | function dragLeaveHandler(event) { 312 | $('#target').removeClass('active'); 313 | } 314 | 315 | function removeDragData(ev) { 316 | if (ev.dataTransfer.items) { 317 | // Use DataTransferItemList interface to remove the drag data 318 | ev.dataTransfer.items.clear(); 319 | } else { 320 | // Use DataTransfer interface to remove the drag data 321 | ev.dataTransfer.clearData(); 322 | } 323 | dragLeaveHandler(ev); 324 | } 325 | 326 | function download(filename, text) { 327 | var element = document.createElement('a'); 328 | element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text)); 329 | element.setAttribute('download', filename); 330 | 331 | element.style.display = 'none'; 332 | document.body.appendChild(element); 333 | 334 | element.click(); 335 | 336 | document.body.removeChild(element); 337 | } 338 | 339 | function clearList() { 340 | $('#filelist').empty(); 341 | } -------------------------------------------------------------------------------- /mapsapp/static/mapsapp/js/maps.js: -------------------------------------------------------------------------------- 1 | var mapnames = new Bloodhound({ 2 | datumTokenizer: function (n) { 3 | return n.name.split(/,/) 4 | }, 5 | queryTokenizer: function (n) { 6 | return n.split(/,/) 7 | }, 8 | prefetch: { 9 | url: MAPS_PREFETCH_URL, 10 | filter: function (list) { 11 | return $.map(list.maps, function (map) { 12 | return {name: map.name, uuid: map.uuid}; 13 | }); 14 | } 15 | }, 16 | remote: { 17 | url: MAPS_URL, 18 | wildcard: 'QUERY', 19 | filter: function (list) { 20 | return $.map(list.maps, function (map) { 21 | return {name: map.name, uuid: map.uuid}; 22 | }); 23 | } 24 | } 25 | }); 26 | mapnames.initialize(); 27 | $('#id_rms').tagsinput({ 28 | itemValue: function (item) { 29 | return item.uuid; 30 | }, 31 | itemText: function (item) { 32 | return item.name; 33 | }, 34 | typeaheadjs: { 35 | name: 'mapnames', 36 | displayKey: 'name', 37 | source: mapnames.ttAdapter() 38 | } 39 | }); 40 | 41 | $('#id_rms').val(''); 42 | for (let item of RMS_INITIAL_DATA) { 43 | $('#id_rms').tagsinput('add', {'uuid': item.uuid, 'name': item.name + ' by ' + item.authors}); 44 | } 45 | -------------------------------------------------------------------------------- /mapsapp/static/mapsapp/js/markdown.js: -------------------------------------------------------------------------------- 1 | var md = window.markdownit({breaks: true}) 2 | .disable(['image']); 3 | $('.markdown-text').each( 4 | function (nr, it) { 5 | let it_select = $(it); 6 | it_select.html( 7 | md.render(it_select.text()) 8 | .replace(/
    2 | 3 | {% load static %} 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | {% block additionalcss %} 14 | {% endblock %} 15 | 16 | {% block pagetitle %}{% endblock %}aoe2map 17 | {% block style %}{% endblock %} 18 | {% block additionalheader %}{% endblock %} 19 | 20 | 21 | 22 | {% block modal %}{% endblock %} 23 |
    24 |

    26 |

    27 | 28 | 74 | {% if request.user.is_authenticated %} 75 | 96 | {% endif %} 97 |
    98 | {% block subtitle %}{% endblock %} 99 |
    100 | {% block content %} 101 |
    102 | {% include 'mapsapp/snippets/loading_animation.html' %} 103 |
    104 | {% endblock %} 105 |
    106 | {% block js %} 107 | {% endblock %} 108 | 109 | 110 | -------------------------------------------------------------------------------- /mapsapp/templates/mapsapp/collection.html: -------------------------------------------------------------------------------- 1 | {% extends "mapsapp/base.html" %} 2 | {% load static %} 3 | 4 | {% block modal %} 5 | 31 | {% endblock %} 32 | 33 | {% block pagetitle %}{{ collection.name }} – {% endblock %} 34 | {% block subtitle %} 35 |

    {{ collection.name }} 36 | by {{ collection.authors }} ({{ collection.rms.count }} maps) 37 |

    38 |
    39 |
    40 |

    {{ collection.description }}

    41 |
    42 | {% if collection.mod_id %} 43 | Go to Mod 46 | {% endif %} 47 |
    48 | 51 | 55 | 59 |
    60 | 61 | 62 | {% if request.user and collection.owner == request.user %} 63 | Edit collection 65 | {% endif %} 66 |
    67 |

    This collection is managed by: {{ collection.owner.username }}

    69 |
    70 | {% endblock %} 71 | 72 | {% block js %} 73 | {% include 'mapsapp/snippets/maps_js_block.html' %} 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | {% endblock %} 82 | -------------------------------------------------------------------------------- /mapsapp/templates/mapsapp/collections.html: -------------------------------------------------------------------------------- 1 | {% extends "mapsapp/base.html" %} 2 | {% load static %} 3 | 4 | {% block pagetitle %}Map Collections – {% endblock %} 5 | {% block subtitle %} 6 |

    7 | Map Collections 8 |

    9 | {% endblock %} 10 | 11 | {% block content %} 12 |
    13 | {% include 'mapsapp/snippets/filter.html' %} 14 | 15 | 22 |
    23 | {% endblock %} 24 | 25 | {% block js %} 26 | 27 | {% endblock %} -------------------------------------------------------------------------------- /mapsapp/templates/mapsapp/editcollection.html: -------------------------------------------------------------------------------- 1 | {% extends "mapsapp/base.html" %} 2 | {% load widget_tweaks %} 3 | {% load static %} 4 | 5 | {% block additionalcss %} 6 | 7 | 12 | {% endblock %} 13 | 14 | {% block pagetitle %}Edit collection – {% endblock %} 15 | 16 | {% block subtitle %} 17 |

    {{ action }} Collection

    18 | {% endblock %} 19 | 20 | {% block content %} 21 |
    22 | 23 |
    24 | {% csrf_token %} 25 | {% include 'mapsapp/snippets/bootstrap-form.html' %} 26 | 27 | 28 |
    29 |
    30 |
    31 | {% endblock %} 32 | 33 | {% block js %} 34 | 35 | 36 | 37 | 42 | 43 | 44 | {% endblock %} -------------------------------------------------------------------------------- /mapsapp/templates/mapsapp/editmap.html: -------------------------------------------------------------------------------- 1 | {% extends "mapsapp/base.html" %} 2 | {% load widget_tweaks %} 3 | {% load static %} 4 | 5 | {% block additionalcss %} 6 | 7 | {% endblock %} 8 | 9 | {% block pagetitle %}Edit Map – {% endblock %} 10 | {% block subtitle %} 11 |

    Edit Map

    12 | {% endblock %} 13 | 14 | {% block content %} 15 | {% if not rms.newer_version %} 16 |

    Hej! If you want to upload a new version of your map, 17 | click here!

    18 | {% endif %} 19 |
    20 | Drop image(s) here! 21 |
    22 |
    23 | 24 |
    25 | 26 |
    27 | {% csrf_token %} 28 | 29 | {% include 'mapsapp/snippets/bootstrap-form.html' %} 30 | 31 | 32 |
    33 |
    34 | {% endblock %} 35 | {% block js %} 36 | 37 | 38 | 39 | 40 | 44 | 45 | 46 | {% endblock %} -------------------------------------------------------------------------------- /mapsapp/templates/mapsapp/email_verification_invalid.html: -------------------------------------------------------------------------------- 1 | {% extends "mapsapp/base.html" %} 2 | 3 | {% block content %} 4 | 5 |
    6 |

    Oops…

    7 |

    …something went wrong. The link you clicked is no longer valid.

    8 |
    9 | 10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /mapsapp/templates/mapsapp/email_verification_sent.html: -------------------------------------------------------------------------------- 1 | {% extends "mapsapp/base.html" %} 2 | 3 | {% block content %} 4 | 5 |
    6 |

    Check your mailbox!

    7 |

    An email has been sent to {{ email }}. Click on the link in that email in order to confirm your 8 | email address!

    9 |
    10 | 11 | {% endblock %} -------------------------------------------------------------------------------- /mapsapp/templates/mapsapp/email_verification_valid.html: -------------------------------------------------------------------------------- 1 | {% extends "mapsapp/base.html" %} 2 | 3 | {% block content %} 4 | 5 |
    6 |

    Success!

    7 |

    Your email address has been confirmed.

    8 |
    9 | 10 | {% endblock %} -------------------------------------------------------------------------------- /mapsapp/templates/mapsapp/index.html: -------------------------------------------------------------------------------- 1 | {% extends "mapsapp/base.html" %} 2 | {% load static %} 3 | 4 | {% block pagetitle %}Index – {% endblock %} 5 | {% block subtitle %} 6 |
    Welcome! 7 | aoe2map.net is a place to share your random map scripts (rms) for Age of Empires II. 8 | {% if not request.user.is_authenticated %} 9 | Login to upload a new rms file 10 | (or register first). 11 | {% endif %}
    12 | {% endblock %} 13 | 14 | 15 | {% block content %} 16 |
    Latest additions The latest and greatest maps from the map making community
    17 |
    18 | {% include 'mapsapp/snippets/loading_animation.html' %} 19 |
    20 |
    Latest updates It only keeps getting better
    21 |
    22 | {% include 'mapsapp/snippets/loading_animation.html' %} 23 |
    24 |
    Randomly selected maps They are called 'random maps' after all, right?
    25 |
    26 | {% include 'mapsapp/snippets/loading_animation.html' %} 27 |
    28 |
    Looking for more maps? Check out the full list.
    29 | {% endblock %} 30 | 31 | {% block js %} 32 | {% include 'mapsapp/snippets/maps_js_block.html' %} 33 | {% endblock %} 34 | -------------------------------------------------------------------------------- /mapsapp/templates/mapsapp/login.html: -------------------------------------------------------------------------------- 1 | {% extends "mapsapp/base.html" %} 2 | {% block pagetitle %}Login – {% endblock %} 3 | {% block subtitle %} 4 |

    Login

    5 | {% endblock %} 6 | {% block content %} 7 |
    8 |
    9 | {% csrf_token %} 10 | {% if messages %} 11 | {% for message in messages %} 12 | 15 | {% endfor %} 16 | {% endif %} 17 |
    18 | 19 | 21 |
    22 |
    23 | 24 | 25 |
    26 |
    27 | 28 |
    29 | 30 | 33 | 36 |
    37 |
    38 | {% endblock %} 39 | {% block js %} 40 | {% endblock %} -------------------------------------------------------------------------------- /mapsapp/templates/mapsapp/map_archive.html: -------------------------------------------------------------------------------- 1 | {% extends "mapsapp/base.html" %} 2 | {% load static %} 3 | 4 | {% block pagetitle %}Remove/archive {{ rms.name }} – {% endblock %} 5 | 6 | {% block subtitle %}

    Confirm remove/archive

    {% endblock %} 7 | 8 | {% block content %} 9 |
    10 |
    {% csrf_token %} 11 |

    Are you sure you want to remove/archive the map "{{ rms.name }}"?

    12 |

    This means:

    13 |
      14 |
    • The map will no longer be randomly presented on the main page.
    • 15 |
    • The map will no longer show up in searches or lists of maps.
    • 16 |
    • The map will no longer show up in your list of your own maps.
    • 17 |
    • Nobody will be able to add the map to collections.
    • 18 |
    • You will no longer be able to edit the map.
    • 19 |
    • Users will no longer be able to express their affection towards the map.
    • 20 |
    • You will not be able to remove the "archived" status from your map again.
    • 21 |
    22 |

    However:

    23 |
      24 |
    • The map will remain accessible directly via its url, but a giant "archived" warning will greet each 25 | visitor. 26 |
    • 27 |
    • The map will remain part of any collection it is currently a part of, until it is removed from the 28 | collection. 29 |
    • 30 |
    • The map will still be displayed in the changelog of other versions of this map.
    • 31 |
    32 | 33 |

    34 | We might actually delete archived maps that look to us like they were uploaded 35 | errorneously. 36 | Marking maps as archived is a relatively new feature, and its functionality is due to change. 37 |

    38 | 39 |

    Now that you know what you are doing, do you really want to remove/archive the map 40 | "{{ rms.name }}"?

    41 | 42 | 43 | Cancel 44 |
    45 |
    46 | {% endblock %} 47 | -------------------------------------------------------------------------------- /mapsapp/templates/mapsapp/mappack.html: -------------------------------------------------------------------------------- 1 | {% extends "mapsapp/base.html" %} 2 | {% load static %} 3 | {% block style %} 4 | 27 | {% endblock %} 28 | 29 | {% block pagetitle %}Mappack creator – {% endblock %} 30 | 31 | {% block subtitle %} 32 |

    Create a mappack

    33 | {% endblock %} 34 | 35 | {% block content %} 36 |
    37 |

    A mappack is a random map script that contains multiple other random map scripts and generates a random one 38 | of them each time it is played. The contained map scripts can get chosen with equal or different 39 | probabilities, depending on how the mappack is designed.

    40 |

    When you generate a mappack using this generator, the selection part will be at the very top of the file. 41 | Once you added all maps you want, you can easily adapt the probability for each map before generating 42 | the mappack.

    43 |

    You can only use "plain" .rms files here, 44 | ZR@ maps will not work.

    45 |
    46 |
    47 | 63 | 64 |
    65 | 66 |
    67 |
    68 | *All data is processed in the browser. Nothing is transmitted over the web. 69 |
    It's just that the browser will "download" the result. 70 |
    71 |
    72 |
    73 | 74 |
    75 |
    76 |
    77 | 78 | We have bugs, many bugs! 79 | 80 |
    81 | {% endblock %} 82 | {% block js %} 83 | 84 | 85 | {% endblock %} -------------------------------------------------------------------------------- /mapsapp/templates/mapsapp/maps.html: -------------------------------------------------------------------------------- 1 | {% extends "mapsapp/base.html" %} 2 | {% load static %} 3 | 4 | {% block pagetitle %}Maps – {% endblock %} 5 | {% block subtitle %} 6 |

    Maps

    7 | {% endblock %} 8 | 9 | {% block content %} 10 |
    11 | {% include 'mapsapp/snippets/filter.html' %} 12 | 13 | 20 |
    21 | {% endblock %} 22 | 23 | {% block js %} 24 | 25 | {% endblock %} -------------------------------------------------------------------------------- /mapsapp/templates/mapsapp/mycollections.html: -------------------------------------------------------------------------------- 1 | {% extends "mapsapp/base.html" %} 2 | {% load static %} 3 | 4 | {% block pagetitle %}My collections – {% endblock %} 5 | 6 | {% block subtitle %} 7 |

    My Collections

    8 | {% endblock %} 9 | 10 | 11 | {% block content %} 12 |
    13 | {% if collections %} 14 | {% include 'mapsapp/snippets/filter.html' %} 15 | 16 | 23 | {% else %} 24 |
    25 |
    😢
    26 |

    Looks like you have not created a collection yet. Start now!

    27 |
    28 | {% endif %} 29 |
    30 | {% endblock %} 31 | 32 | {% block js %} 33 | 34 | {% endblock %} -------------------------------------------------------------------------------- /mapsapp/templates/mapsapp/mymaps.html: -------------------------------------------------------------------------------- 1 | {% extends "mapsapp/base.html" %} 2 | {% load static %} 3 | 4 | {% block pagetitle %}My maps – {% endblock %} 5 | 6 | {% block subtitle %} 7 |

    My maps

    8 | {% endblock %} 9 | 10 | {% block content %} 11 |
    12 | {% if rmss %} 13 | {% include 'mapsapp/snippets/filter.html' %} 14 | 15 | 40 | {% else %} 41 |
    42 |
    😢
    43 |

    Looks like you have not uploaded any maps yet. Start now!

    44 |
    45 | {% endif %} 46 |
    47 | {% endblock %} 48 | 49 | {% block js %} 50 | 51 | 52 | 53 | 54 | {% endblock %} -------------------------------------------------------------------------------- /mapsapp/templates/mapsapp/newmap.html: -------------------------------------------------------------------------------- 1 | {% extends "mapsapp/base.html" %} 2 | {% load widget_tweaks %} 3 | {% load static %} 4 | 5 | {% block additionalcss %} 6 | 7 | {% endblock %} 8 | 9 | {% block pagetitle %}New Map – {% endblock %} 10 | {% block subtitle %} 11 |

    New Map

    12 | {% if old_rms %} 13 |

    You are currently uploading a new version of 14 | {{ old_rms.name }}

    15 | {% endif %} 16 | {% endblock %} 17 | 18 | {% block content %} 19 |
    20 | Drop a .rms file or image(s)! 21 |
    22 |
    23 | 24 |

    You want to upload a new map? Just fill out the fields below with meaningful data, add your .rms 25 | file 26 | and a screenshot of the map (or another meaningful image) and hit Upload!

    27 | 28 |
    29 | 30 |
    31 | {% csrf_token %} 32 | 33 | {% include 'mapsapp/snippets/bootstrap-form.html' %} 34 | 35 | 36 |
    37 |
    38 | {% endblock %} 39 | {% block js %} 40 | 41 | 42 | 43 | 44 | 48 | 49 | 50 | {% endblock %} -------------------------------------------------------------------------------- /mapsapp/templates/mapsapp/register.html: -------------------------------------------------------------------------------- 1 | {% extends "mapsapp/base.html" %} 2 | {% load widget_tweaks %} 3 | 4 | {% block pagetitle %}Register – {% endblock %} 5 | {% block subtitle %} 6 |

    Register

    7 | {% endblock %} 8 | 9 | {% block content %} 10 |
    11 |
    12 | {% csrf_token %} 13 | 14 | {% include 'mapsapp/snippets/bootstrap-form.html' %} 15 | 16 | 17 |
    18 |
    19 | {% endblock %} -------------------------------------------------------------------------------- /mapsapp/templates/mapsapp/search.html: -------------------------------------------------------------------------------- 1 | {% extends "mapsapp/base.html" %} 2 | {% load static %} 3 | 4 | {% block pagetitle %}Search – {% endblock %} 5 | 6 | {% block subtitle %} 7 |

    Search results for ›{{ term }}‹

    8 | {% endblock %} 9 | 10 | {% block js %} 11 | {% include 'mapsapp/snippets/maps_js_block.html' %} 12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /mapsapp/templates/mapsapp/settings.html: -------------------------------------------------------------------------------- 1 | {% extends "mapsapp/base.html" %} 2 | {% load widget_tweaks %} 3 | 4 | {% block pagetitle %}Settings – {% endblock %} 5 | {% block subtitle %} 6 |

    Settings

    7 | {% endblock %} 8 | 9 | {% block content %} 10 |
    11 | 12 |

    You can change your password and modify your email address here. If add or modify your email address, you 13 | will receive an email with a link that you have to click in order to confirm your email address. You can 14 | also remove your email address and save an empty field in order to remove your email address from our 15 | system.

    16 | 17 |

    You cannot reset a forgotten password unless you add and confirm an email address.

    18 | 19 |
    20 | {% csrf_token %} 21 | 22 | {% include 'mapsapp/snippets/bootstrap-form.html' %} 23 | 24 | 25 |
    26 |
    27 | {% endblock %} -------------------------------------------------------------------------------- /mapsapp/templates/mapsapp/snippets/archived_alert.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mapsapp/templates/mapsapp/snippets/bootstrap-form.html: -------------------------------------------------------------------------------- 1 | {% load widget_tweaks %} 2 | {% load not_empty_choices %} 3 | {% if messages %} 4 | {% for message in messages %} 5 | 8 | {% endfor %} 9 | {% endif %} 10 | {% for hidden_field in form.hidden_fields %} 11 | {{ hidden_field }} 12 | {% endfor %} 13 | 14 | {% if form.non_field_errors %} 15 | 20 | {% endif %} 21 | 22 | {% for field in form.visible_fields %} 23 | {% if field.field|not_empty_choices %} 24 |
    25 | 32 | 33 | {% if form.is_bound %} 34 | {% if field.errors %} 35 | {% render_field field class="form-control is-invalid" %} 36 | {% for error in field.errors %} 37 |
    38 | {{ error }} 39 |
    40 | {% endfor %} 41 | {% else %} 42 | {% render_field field class="form-control is-valid" %} 43 | {% endif %} 44 | {% else %} 45 | {% render_field field class="form-control" %} 46 | {% endif %} 47 |
    48 | {% endif %} 49 | {% endfor %} -------------------------------------------------------------------------------- /mapsapp/templates/mapsapp/snippets/changelog_item.html: -------------------------------------------------------------------------------- 1 |
  • 2 |
    3 | 11 |
    12 | {{ rms.created | date:"Y-m-d" }} 13 |
    14 |
    15 | {% if rms.changelog %} 16 |
    {{ rms.changelog }}
    17 | {% endif %} 18 |
  • -------------------------------------------------------------------------------- /mapsapp/templates/mapsapp/snippets/filter.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    Filter
    4 |
    5 | 6 |
    -------------------------------------------------------------------------------- /mapsapp/templates/mapsapp/snippets/latest_version_alert.html: -------------------------------------------------------------------------------- 1 | {% if rms.newer_version %} 2 | {% include "mapsapp/snippets/latest_version_alert.html" with rms=rms.newer_version %} 3 | {% else %} 4 | 8 | {% endif %} -------------------------------------------------------------------------------- /mapsapp/templates/mapsapp/snippets/loading_animation.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 |
    3 | Scouting for maps 4 |
    5 | -------------------------------------------------------------------------------- /mapsapp/templates/mapsapp/snippets/maps_js_block.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 12 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /mapsapp/templates/mapsapp/snippets/newer_version.html: -------------------------------------------------------------------------------- 1 | {% if rms.newer_version %} 2 | {% include "mapsapp/snippets/newer_version.html" with rms=rms.newer_version %} 3 | {% endif %} 4 | 5 | {% include "mapsapp/snippets/changelog_item.html" %} -------------------------------------------------------------------------------- /mapsapp/templates/mapsapp/snippets/older_versions.html: -------------------------------------------------------------------------------- 1 | {% include "mapsapp/snippets/changelog_item.html" %} 2 | 3 | {% if rms.predecessors.exists %} 4 | {% for predecessor in rms.predecessors.all %} 5 | {% include "mapsapp/snippets/older_versions.html" with rms=predecessor current=False %} 6 | {% endfor %} 7 | {% endif %} 8 | 9 | -------------------------------------------------------------------------------- /mapsapp/templates/mapsapp/tags.html: -------------------------------------------------------------------------------- 1 | {% extends "mapsapp/base.html" %} 2 | 3 | {% block pagetitle %}Tags – {% endblock %} 4 | 5 | {% block subtitle %} 6 |

    Maps tagged with ›{{ tag }}‹

    7 | {% endblock %} 8 | 9 | {% block content %} 10 |
    11 |
    12 | {% for single_tag in alltags %} 13 | {{ single_tag }} 14 | {% endfor %} 15 |
    16 |
    17 |
    18 | {% include 'mapsapp/snippets/loading_animation.html' %} 19 |
    20 | {% endblock %} 21 | 22 | {% block js %} 23 | {% include 'mapsapp/snippets/maps_js_block.html' %} 24 | {% endblock %} 25 | -------------------------------------------------------------------------------- /mapsapp/templates/mapsapp/version.html: -------------------------------------------------------------------------------- 1 | {% extends "mapsapp/base.html" %} 2 | 3 | {% block pagetitle %}Versions – {% endblock %} 4 | 5 | {% block subtitle %} 6 |

    Maps compatible with version ›{{ version_name }}‹

    7 | {% endblock %} 8 | 9 | {% block content %} 10 |
    11 |
    12 | {% for version in allversions %} 13 | {% if version.name == version_name %} 14 | {{ version.name }} 15 | {% else %} 16 | {{ version.name }} 18 | {% endif %} 19 | {% endfor %} 20 |
    21 |
    22 |
    23 | {% include 'mapsapp/snippets/loading_animation.html' %} 24 |
    25 | {% endblock %} 26 | 27 | {% block js %} 28 | {% include 'mapsapp/snippets/maps_js_block.html' %} 29 | {% endblock %} 30 | -------------------------------------------------------------------------------- /mapsapp/templates/registration/password_reset_complete.html: -------------------------------------------------------------------------------- 1 | {% extends "mapsapp/base.html" %} 2 | 3 | {% block content %} 4 |
    5 |

    6 | Your password has been set. You may go ahead and login now. 7 |

    8 |
    9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /mapsapp/templates/registration/password_reset_confirm.html: -------------------------------------------------------------------------------- 1 | {% extends "mapsapp/base.html" %} 2 | {% load widget_tweaks %} 3 | 4 | {% block content %} 5 |
    6 | {% if validlink %} 7 |

    Change password

    8 |
    9 | {% csrf_token %} 10 | 11 | {% include 'mapsapp/snippets/bootstrap-form.html' %} 12 | 13 | 14 |
    15 | {% else %} 16 |

    17 | The password reset link was invalid, possibly because it has already been used. 18 | Please request a new password reset. 19 |

    20 | {% endif %} 21 |
    22 | {% endblock %} -------------------------------------------------------------------------------- /mapsapp/templates/registration/password_reset_done.html: -------------------------------------------------------------------------------- 1 | {% extends "mapsapp/base.html" %} 2 | 3 | {% block content %} 4 |
    5 |

    6 | We have emailed you instructions for setting your password, if an account exists with the email address you 7 | entered. You should receive them shortly. 8 |

    9 |

    10 | If you do not receive an email, please make sure you have entered the address you registered with, 11 | and check your spam folder. 12 |

    13 |
    14 | {% endblock %} -------------------------------------------------------------------------------- /mapsapp/templates/registration/password_reset_email.html: -------------------------------------------------------------------------------- 1 | {% autoescape off %} 2 | To initiate the password reset process for your {{ user.get_username }} aoe2map Account, 3 | click the link below: 4 | 5 | {{ django_top_url }}{% url 'password_reset_confirm' uidb64=uid token=token %} 6 | 7 | If clicking the link above doesn't work, please copy and paste the URL in a new browser 8 | window instead. 9 | {% endautoescape %} -------------------------------------------------------------------------------- /mapsapp/templates/registration/password_reset_form.html: -------------------------------------------------------------------------------- 1 | {% extends "mapsapp/base.html" %} 2 | {% load widget_tweaks %} 3 | 4 | {% block content %} 5 |
    6 |

    Reset password

    7 |
    8 | {% csrf_token %} 9 | 10 | {% include 'mapsapp/snippets/bootstrap-form.html' %} 11 | 12 | 13 |
    14 |
    15 | {% endblock %} -------------------------------------------------------------------------------- /mapsapp/templates/registration/password_reset_subject.txt: -------------------------------------------------------------------------------- 1 | aoe2map password reset -------------------------------------------------------------------------------- /mapsapp/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiegeEngineers/aoe2map/d7d72597ec0c9a4bbf6eb83f3161335730d8daca/mapsapp/templatetags/__init__.py -------------------------------------------------------------------------------- /mapsapp/templatetags/not_empty_choices.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | 3 | register = template.Library() 4 | 5 | 6 | def not_empty_choices(value): 7 | try: 8 | return value.choices != [] 9 | except AttributeError: 10 | return True 11 | 12 | 13 | register.filter('not_empty_choices', not_empty_choices) 14 | -------------------------------------------------------------------------------- /mapsapp/templatetags/startswith.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | 3 | register = template.Library() 4 | 5 | 6 | def startswith(text, value): 7 | try: 8 | return text.startswith(value) 9 | except AttributeError: 10 | return False 11 | 12 | 13 | register.filter('startswith', startswith) 14 | -------------------------------------------------------------------------------- /mapsapp/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiegeEngineers/aoe2map/d7d72597ec0c9a4bbf6eb83f3161335730d8daca/mapsapp/tests/__init__.py -------------------------------------------------------------------------------- /mapsapp/tests/aoe2maptest.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | import json 3 | import os 4 | 5 | from django.contrib.auth.models import User 6 | from django.core.files.uploadedfile import SimpleUploadedFile 7 | from django.test import TestCase 8 | from django.urls import reverse 9 | 10 | from mapsapp.models import Tag, VersionTag, Rms 11 | 12 | 13 | class AbstractAoe2mapTest(TestCase): 14 | 15 | def setUp(self): 16 | self.failures = [] 17 | 18 | def tearDown(self): 19 | self.assertEqual([], self.failures) 20 | 21 | @classmethod 22 | def setUpTestData(cls): 23 | cls.aTag = Tag.objects.create(name='tag-name') 24 | cls.aVersion = VersionTag.objects.create(name='version-name') 25 | cls.aUser = User.objects.create_user(username='username', password='password') 26 | cls.counter = itertools.count() 27 | 28 | def create_sample_map(self, changelog='', newer_version=None, name=None, authors=None): 29 | rms = Rms() 30 | rms.owner = self.aUser 31 | rms.name = f"map-name-{next(self.counter)}" if name is None else name 32 | rms.changelog = changelog 33 | rms.authors = 'rms-authors' if authors is None else authors 34 | rms.description = 'rms-description' 35 | rms.file = SimpleUploadedFile('file-name', b'file-contents') 36 | rms.newer_version = newer_version 37 | rms.save() 38 | rms.tags.add(self.aTag) 39 | rms.versiontags.add(self.aVersion) 40 | rms.save() 41 | return rms 42 | 43 | def assert_map_has_changelog(self, rms): 44 | response = self.client.get(reverse('map', kwargs={'rms_id': rms.id, 'slug': rms.slug})) 45 | self.assertIn(b'Changelog', response.content) 46 | 47 | def assert_map_does_not_have_changelog(self, rms): 48 | response = self.client.get(reverse('map', kwargs={'rms_id': rms.id, 'slug': rms.slug})) 49 | self.assertNotIn(b'Changelog', response.content) 50 | 51 | def compareJsonWithValidationFile(self, output, suffix="", masking=None): 52 | output = json.dumps(output, indent=4) 53 | self.compareWithValidationFile(output, suffix, masking) 54 | 55 | def compareWithValidationFile(self, output, suffix="", masking=None): 56 | if masking is None: 57 | masking = [] 58 | 59 | for func in masking: 60 | output = func(output) 61 | validation = f"=== new file ===\n{output}" 62 | filename = f"{self._testMethodName}_{suffix}.html" 63 | script_file_path = os.path.dirname(os.path.realpath(__file__)) 64 | with open(os.path.join(script_file_path, "../tests/snapshots", "output", filename), "w") as f: 65 | print(output, file=f) 66 | 67 | if not os.path.isfile(os.path.join(script_file_path, "../tests/snapshots", "validation", filename)): 68 | with open(os.path.join(script_file_path, "../tests/snapshots", "validation", filename), "w") as f: 69 | print(validation, file=f) 70 | else: 71 | with open(os.path.join(script_file_path, "../tests/snapshots", "validation", filename), "r") as f: 72 | validation = f.read() 73 | 74 | if output.strip() != validation.strip(): 75 | self.failures.append("{} does not match {}!".format( 76 | os.path.join("output", filename), 77 | os.path.join("validation", filename) 78 | )) 79 | 80 | @staticmethod 81 | def mask_uuid(rms): 82 | return lambda x: x.replace(str(rms.uuid), f'[{rms.name}_UUID]') 83 | 84 | @staticmethod 85 | def mask_id(rms): 86 | return lambda x: x.replace(str(rms.id), f'[{rms.name}_ID]') 87 | -------------------------------------------------------------------------------- /mapsapp/tests/snapshots/validation/test_latest_rms_.html: -------------------------------------------------------------------------------- 1 | { 2 | "maps": [ 3 | { 4 | "uuid": "[map-name-1_UUID]", 5 | "name": "map-name-1", 6 | "version": "", 7 | "authors": "rms-authors", 8 | "description": "rms-description", 9 | "pageurl": "/map/[map-name-1_ID]/map-name-1", 10 | "newer_version": null, 11 | "latest_version": null, 12 | "url": "", 13 | "file": "[map-name-1_UUID]/file-name", 14 | "original_filename": "", 15 | "fileurl": "/media/[map-name-1_UUID]/file-name", 16 | "tags": [ 17 | { 18 | "name": "tag-name", 19 | "id": 1 20 | } 21 | ], 22 | "versiontags": [ 23 | "version-name" 24 | ], 25 | "collections": [], 26 | "images": [], 27 | "votes": 0 28 | }, 29 | { 30 | "uuid": "[map-name-0_UUID]", 31 | "name": "map-name-0", 32 | "version": "", 33 | "authors": "rms-authors", 34 | "description": "rms-description", 35 | "pageurl": "/map/[map-name-0_ID]/map-name-0", 36 | "newer_version": null, 37 | "latest_version": null, 38 | "url": "", 39 | "file": "[map-name-0_UUID]/file-name", 40 | "original_filename": "", 41 | "fileurl": "/media/[map-name-0_UUID]/file-name", 42 | "tags": [ 43 | { 44 | "name": "tag-name", 45 | "id": 1 46 | } 47 | ], 48 | "versiontags": [ 49 | "version-name" 50 | ], 51 | "collections": [], 52 | "images": [], 53 | "votes": 0 54 | } 55 | ] 56 | } 57 | -------------------------------------------------------------------------------- /mapsapp/tests/snapshots/validation/test_latest_updated_rms_.html: -------------------------------------------------------------------------------- 1 | { 2 | "maps": [ 3 | { 4 | "uuid": "[map-name-2_UUID]", 5 | "name": "map-name-2", 6 | "version": "", 7 | "authors": "rms-authors", 8 | "description": "rms-description", 9 | "pageurl": "/map/[map-name-2_ID]/map-name-2", 10 | "newer_version": null, 11 | "latest_version": null, 12 | "url": "", 13 | "file": "[map-name-2_UUID]/file-name", 14 | "original_filename": "", 15 | "fileurl": "/media/[map-name-2_UUID]/file-name", 16 | "tags": [ 17 | { 18 | "name": "tag-name", 19 | "id": 1 20 | } 21 | ], 22 | "versiontags": [ 23 | "version-name" 24 | ], 25 | "collections": [], 26 | "images": [], 27 | "votes": 0 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /mapsapp/tests/test_api.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import itertools 3 | 4 | import pytz 5 | from django.test import Client 6 | from django.urls import reverse 7 | 8 | from mapsapp.tests.aoe2maptest import AbstractAoe2mapTest 9 | 10 | 11 | class ApiTest(AbstractAoe2mapTest): 12 | 13 | def setUp(self): 14 | self.failures = [] 15 | self.counter = itertools.count() 16 | 17 | def test_latest_rms(self): 18 | masking = self.prepare_sample_rms_entities() 19 | 20 | c = Client() 21 | response = c.get(reverse('api:latest_rms', kwargs={'amount': 3})) 22 | self.compareJsonWithValidationFile(response.json(), masking=masking) 23 | 24 | def test_latest_updated_rms(self): 25 | masking = self.prepare_sample_rms_entities() 26 | 27 | c = Client() 28 | response = c.get(reverse('api:latest_updated_rms', kwargs={'amount': 3})) 29 | self.compareJsonWithValidationFile(response.json(), masking=masking) 30 | 31 | def test_archived_rms(self): 32 | rms = self.create_sample_map() 33 | self.assertFalse(rms.archived) 34 | 35 | c = Client() 36 | 37 | response = c.get(reverse('api:latest_rms', kwargs={'amount': 1})) 38 | self.assertEquals(1, len(response.json()['maps'])) 39 | 40 | rms.archived = True 41 | rms.save() 42 | 43 | response = c.get(reverse('api:latest_rms', kwargs={'amount': 1})) 44 | self.assertEquals(0, len(response.json()['maps'])) 45 | 46 | response = c.get(reverse('api:rms', kwargs={'rms_id': rms.uuid})) 47 | self.assertEquals(1, len(response.json()['maps'])) 48 | 49 | def test_rms_by_name(self): 50 | self.create_sample_map(name='Bamboo_Arabia', authors='T-West') 51 | self.create_sample_map(name='Bamboo Arabia', authors='T-West') 52 | self.create_sample_map(name='Bamboo', authors='T West') 53 | self.create_sample_map(name='Arabia', authors='Arabia') 54 | self.create_sample_map(name='Dark Forest', authors='Bamboo Arabia') 55 | 56 | c = Client() 57 | self.assert_rms_by_name_returns_number_of_maps(c, 'Bamboo Arabia', 3) 58 | self.assert_rms_by_name_returns_number_of_maps(c, 'Bamboo', 4) 59 | self.assert_rms_by_name_returns_number_of_maps(c, 'Arabia', 4) 60 | self.assert_rms_by_name_returns_number_of_maps(c, 'Arabia T-West', 2) 61 | self.assert_rms_by_name_returns_number_of_maps(c, 'T West Arabia', 2) 62 | 63 | def assert_rms_by_name_returns_number_of_maps(self, c, query, count): 64 | response = c.get(reverse('api:rms_by_name', kwargs={'name': query})) 65 | self.assertEquals(count, len(response.json()['maps'])) 66 | 67 | def prepare_sample_rms_entities(self): 68 | masking = [] 69 | first = self.create_sample_map() 70 | first.created = datetime.datetime(2018, 12, 1, 1, 1, 1, tzinfo=pytz.utc) 71 | masking.append(self.mask_uuid(first)) 72 | masking.append(self.mask_id(first)) 73 | second = self.create_sample_map() 74 | second.created = datetime.datetime(2018, 12, 2, 1, 1, 1, tzinfo=pytz.utc) 75 | masking.append(self.mask_uuid(second)) 76 | masking.append(self.mask_id(second)) 77 | third = self.create_sample_map() 78 | third.created = datetime.datetime(2018, 12, 3, 1, 1, 1, tzinfo=pytz.utc) 79 | masking.append(self.mask_uuid(third)) 80 | masking.append(self.mask_id(third)) 81 | third_predecessor = self.create_sample_map(newer_version=third) 82 | third_predecessor.created = datetime.datetime(2018, 12, 2, 2, 1, 1, tzinfo=pytz.utc) 83 | masking.append(self.mask_uuid(third_predecessor)) 84 | masking.append(self.mask_id(third_predecessor)) 85 | return masking 86 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /mapsapp/tests/test_authentification.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.hashers import make_password 2 | from django.contrib.auth.models import User 3 | from django.contrib.staticfiles.testing import StaticLiveServerTestCase 4 | from django.urls import reverse 5 | from selenium.webdriver.common.by import By 6 | from selenium.webdriver.firefox.options import Options 7 | from selenium.webdriver.firefox.webdriver import WebDriver 8 | 9 | 10 | class AuthenticationTest(StaticLiveServerTestCase): 11 | 12 | @classmethod 13 | def setUpClass(cls): 14 | super().setUpClass() 15 | options = Options() 16 | options.headless = True 17 | cls.browser = WebDriver(options=options) 18 | cls.browser.implicitly_wait(10) 19 | 20 | @classmethod 21 | def tearDownClass(cls): 22 | cls.browser.quit() 23 | super().tearDownClass() 24 | 25 | def test_index_works(self): 26 | self.browser.get(self.live_server_url) 27 | 28 | self.assertIn('aoe2map', self.browser.title) 29 | 30 | def test_login(self): 31 | self.create_test_user('hscmi', 'password') 32 | self.browser.get(self.live_server_url + reverse('login')) 33 | 34 | username = self.browser.find_element(By.ID, 'username') 35 | password = self.browser.find_element(By.ID, 'password') 36 | loginbutton = self.browser.find_element(By.ID, 'login') 37 | 38 | username.send_keys("hscmi") 39 | password.send_keys("password") 40 | 41 | loginbutton.click() 42 | 43 | self.assertIn('My Maps', self.browser.page_source) 44 | 45 | @staticmethod 46 | def create_test_user(username, password): 47 | testuser = User() 48 | testuser.username = username 49 | testuser.password = make_password(password) 50 | testuser.save() 51 | -------------------------------------------------------------------------------- /mapsapp/tests/test_changelog.py: -------------------------------------------------------------------------------- 1 | from django.test import Client 2 | from django.urls import reverse 3 | from django.utils.encoding import smart_str 4 | 5 | from mapsapp.tests.aoe2maptest import AbstractAoe2mapTest 6 | 7 | 8 | class ChangelogTest(AbstractAoe2mapTest): 9 | 10 | def test_single_map_has_no_changelog(self): 11 | rms = self.create_sample_map() 12 | self.assertIn('map-name', rms.name) 13 | 14 | self.assert_map_does_not_have_changelog(rms) 15 | 16 | def test_single_map_with_changelog_text_has_changelog(self): 17 | rms = self.create_sample_map(changelog='changelog') 18 | self.assertIn('map-name', rms.name) 19 | self.assertEqual('changelog', rms.changelog) 20 | 21 | self.assert_map_has_changelog(rms) 22 | 23 | def test_three_maps_all_have_changelog(self): 24 | rms1 = self.create_sample_map() 25 | rms2 = self.create_sample_map(newer_version=rms1) 26 | rms3 = self.create_sample_map(newer_version=rms2) 27 | self.assertIn('map-name', rms1.name) 28 | self.assertIn('map-name', rms2.name) 29 | self.assertIn('map-name', rms3.name) 30 | 31 | self.assert_map_has_changelog(rms1) 32 | self.assert_map_has_changelog(rms2) 33 | self.assert_map_has_changelog(rms3) 34 | 35 | def test_three_maps_with_changelog_text_all_have_changelog(self): 36 | rms1 = self.create_sample_map(changelog='changelog') 37 | rms2 = self.create_sample_map(newer_version=rms1, changelog='changelog') 38 | rms3 = self.create_sample_map(newer_version=rms2, changelog='changelog') 39 | self.assertIn('map-name', rms1.name) 40 | self.assertIn('map-name', rms2.name) 41 | self.assertIn('map-name', rms3.name) 42 | 43 | self.assert_map_has_changelog(rms1) 44 | self.assert_map_has_changelog(rms2) 45 | self.assert_map_has_changelog(rms3) 46 | 47 | def test_new_version_link_from_third_map_leads_to_first_map(self): 48 | rms1 = self.create_sample_map() 49 | rms2 = self.create_sample_map(newer_version=rms1) 50 | rms3 = self.create_sample_map(newer_version=rms2) 51 | self.assertIn('map-name', rms1.name) 52 | self.assertIn('map-name', rms2.name) 53 | self.assertIn('map-name', rms3.name) 54 | 55 | c = Client() 56 | response = c.get(reverse('map', kwargs={'rms_id': rms3.id, 'slug': rms3.slug})) 57 | self.assertEquals(rms3.newer_version, rms2) 58 | self.assertIn('A newer version of this map is available!', smart_str(response.content)) 59 | self.assertIn(f'Check it out!', 60 | smart_str(response.content)) 61 | -------------------------------------------------------------------------------- /mapsapp/tests/test_homepagetest.py: -------------------------------------------------------------------------------- 1 | from django.urls import resolve, reverse 2 | 3 | from mapsapp.tests.aoe2maptest import AbstractAoe2mapTest 4 | from mapsapp.views import index 5 | 6 | 7 | class HomePageTest(AbstractAoe2mapTest): 8 | 9 | def test_root_url_resolves_to_index_view(self): 10 | found = resolve('/') 11 | self.assertEqual(found.func, index) 12 | 13 | def test_map_url(self): 14 | rms = self.create_sample_map(name='Sample Map', authors='Test Author') 15 | response = self.client.get(reverse('map_uuid', kwargs={'rms_id': rms.uuid})) 16 | assert response.status_code == 302 17 | assert response.url == f'/map/{rms.id}/sample-map' 18 | -------------------------------------------------------------------------------- /mapsapp/tests/test_models.py: -------------------------------------------------------------------------------- 1 | from django.urls import reverse 2 | 3 | from mapsapp.models import Collection 4 | from mapsapp.tests.aoe2maptest import AbstractAoe2mapTest 5 | 6 | 7 | class HomePageTest(AbstractAoe2mapTest): 8 | 9 | def test_rms_id_calculation(self): 10 | rms = self.create_sample_map(name='Sample Map', authors='Test Author') 11 | assert str(rms.uuid).startswith(rms.id) 12 | 13 | def test_collection_id_calculation(self): 14 | collection = Collection() 15 | collection.name = 'My Collection' 16 | collection.authors = 'Some Authors' 17 | collection.owner = self.aUser 18 | collection.save() 19 | assert str(collection.uuid).startswith(collection.id) 20 | -------------------------------------------------------------------------------- /mapsapp/tests/test_smoketest.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | from time import sleep 4 | 5 | from django.conf import settings 6 | from django.contrib.staticfiles.testing import StaticLiveServerTestCase 7 | from django.test import override_settings 8 | from django.urls import reverse, re_path 9 | from django.views.static import serve 10 | from selenium.webdriver.common.by import By 11 | from selenium.webdriver.firefox.options import Options 12 | from selenium.webdriver.firefox.webdriver import WebDriver 13 | 14 | from aoe2map import imagestorage 15 | from aoe2map.urls import urlpatterns as base_patterns 16 | from mapsapp.models import VersionTag 17 | 18 | urlpatterns = base_patterns + [re_path(r'^{}(?P.*)$'.format(re.escape(settings.IMAGE_URL.lstrip('/'))), 19 | serve, kwargs={'document_root': imagestorage.IMAGE_ROOT})] 20 | 21 | 22 | @override_settings(ROOT_URLCONF=__name__) 23 | class SmokeTest(StaticLiveServerTestCase): 24 | 25 | @classmethod 26 | def setUpClass(cls): 27 | super().setUpClass() 28 | options = Options() 29 | options.headless = True 30 | cls.browser = WebDriver(options=options) 31 | cls.browser.implicitly_wait(10) 32 | 33 | versiontag = VersionTag() 34 | versiontag.name = "Version Tag" 35 | versiontag.save() 36 | 37 | @classmethod 38 | def tearDownClass(cls): 39 | cls.browser.quit() 40 | super().tearDownClass() 41 | 42 | def test_smoke_test(self): 43 | script_file_path = os.path.dirname(os.path.realpath(__file__)) 44 | # 000_index_works 45 | 46 | self.open_index_page() 47 | 48 | # 001_open_login_page 49 | 50 | self.click_page_link('nav-link-login', 'Login') 51 | 52 | # 002_open_registration_page 53 | 54 | self.click_page_link('a-register', 'Register') 55 | 56 | # 003_register_without_email 57 | 58 | self.fill_fields({ 59 | 'id_username': "hscmi", 60 | 'id_password1': "password", 61 | 'id_password2': "password", 62 | 'id_daut': "DauT" 63 | }) 64 | 65 | self.click_to_login('register') 66 | 67 | # 004_logout 68 | 69 | self.click_to_logout('user-nav-logout') 70 | 71 | # 005_open_login_page 72 | 73 | self.click_page_link('nav-link-login', 'Login') 74 | 75 | # 006_login 76 | 77 | self.fill_fields({ 78 | 'username': "hscmi", 79 | 'password': "password", 80 | }) 81 | self.click_to_login('login') 82 | 83 | # 007_open_new_map_page 84 | 85 | self.click_page_link('user-nav-new-map', 'New Map') 86 | 87 | # 008_create_new_map 88 | 89 | self.fill_fields({ 90 | 'id_file': os.path.join(script_file_path, 'testdata', 'relic_nothing.rms'), 91 | 'id_name': 'Map Name', 92 | 'id_version': 'Map Version', 93 | 'id_authors': 'Map Authors', 94 | 'id_description': 'Map Description', 95 | 'id_information': 'Map Information', 96 | 'id_changelog': 'Changelog Information', 97 | 'id_url': 'map_url', 98 | 'id_mod_id': '1337', 99 | 'id_tags': 'map,tags', 100 | 'id_images': os.path.join(script_file_path, 'testdata', 'relic_nothing.png') 101 | }) 102 | self.click('id_versiontags_0') 103 | self.click_page_link('upload', 'Your Map has been created!') 104 | new_map_uuid = self.get_new_map_uuid() 105 | 106 | # 009_open_mymaps_page_and_find_map 107 | 108 | self.click_page_link('user-nav-my-maps', 'My Maps') 109 | self.assertIn('Map Name', self.browser.page_source) 110 | 111 | # 010_open_maps_page_and_find_map 112 | 113 | self.click_page_link('nav-link-maps', ' Maps') 114 | self.assertIn('Map Name', self.browser.page_source) 115 | 116 | # 0101_open_map_page_and_find_changelog 117 | 118 | self.browser.get(self.live_server_url + reverse('map_uuid', kwargs={'rms_id': new_map_uuid})) 119 | self.assertIn('Changelog Information', self.browser.page_source) 120 | self.assertIn('https://mods.aoe2.se/1337', self.browser.page_source) 121 | self.assertIn('0', self.browser.page_source) 122 | 123 | # 0102_press_the_heart 124 | self.click('vote-button') 125 | self.assertIn('1', self.browser.page_source) 126 | 127 | # 0103_press_the_heart_again 128 | self.click('vote-button') 129 | self.assertIn('0', self.browser.page_source) 130 | 131 | # 011_open_create_collection_page 132 | 133 | self.click_page_link('user-nav-new-collection', 'Create Collection') 134 | 135 | # 012_create_new_collection 136 | 137 | self.fill_fields({ 138 | 'id_name': 'Collection Name', 139 | 'id_authors': 'Collection Authors', 140 | 'id_mod_id': '42', 141 | 'id_description': 'Collection Description', 142 | 'id_rms': new_map_uuid 143 | }) 144 | self.click_page_link('save', 'Collection created successfully') 145 | 146 | # 013_open_collections_page_and_find_collection 147 | 148 | self.click_page_link('nav-link-map-collections', 149 | ' Map Collections') 150 | self.assertIn('Collection Name', self.browser.page_source) 151 | 152 | # 014_open_collection_page_and_find_map 153 | 154 | self.click_page_link_text('Collection Name', 'Collection Description') 155 | self.assertIn('https://mods.aoe2.se/42', self.browser.page_source) 156 | sleep(1) 157 | self.assertIn('Map Name', self.browser.page_source) 158 | 159 | # 015_open_map 160 | 161 | self.click_page_link_text('Map Name', 'Upload new version') 162 | 163 | # 016_click_upload_new_version 164 | 165 | self.click_page_link_text('Upload new version', 'You are currently uploading a new version of') 166 | 167 | # 017_fill_fields 168 | 169 | self.fill_fields({ 170 | 'id_file': os.path.join(script_file_path, 'testdata', 'relic_nothing.rms'), 171 | 'id_changelog': 'Changelog Information: New Version' 172 | }) 173 | self.click('id_versiontags_0') 174 | self.click('id_images_to_copy_0') 175 | self.click_page_link('upload', 'Your Map has been created!') 176 | 177 | # 018_open_mymaps_page_and_find_map 178 | 179 | self.click_page_link('a-goto-created-map', 'Map Name') 180 | self.assertIn('Changelog Information: New Version', self.browser.page_source) 181 | self.assertIn('relic_nothing.png', self.browser.page_source) 182 | self.assertIn('https://mods.aoe2.se/1337', self.browser.page_source) 183 | 184 | # 099_logout 185 | 186 | logout = self.browser.find_element(By.ID, 'user-nav-logout') 187 | 188 | logout.click() 189 | 190 | self.assertLoggedOut() 191 | 192 | def get_new_map_uuid(self): 193 | href = self.browser.find_element(By.ID, 'a-goto-created-map').get_attribute('href') 194 | return href.split('/')[-1] 195 | 196 | def open_index_page(self): 197 | self.browser.get(self.live_server_url) 198 | self.assertIn('aoe2map', self.browser.title) 199 | 200 | def click_to_logout(self, element_id): 201 | self.click(element_id) 202 | self.assertLoggedOut() 203 | 204 | def click_to_login(self, element_id): 205 | self.click(element_id) 206 | self.assertLoggedIn() 207 | 208 | def click(self, element_id): 209 | button = self.browser.find_element(By.ID, element_id) 210 | button.click() 211 | 212 | def fill_fields(self, content): 213 | for key, value in content.items(): 214 | self.fill_field(key, value) 215 | 216 | def fill_field(self, element_id, content): 217 | field = self.browser.find_element(By.ID, element_id) 218 | field.send_keys(content) 219 | 220 | def click_page_link(self, element_id, content): 221 | link = self.browser.find_element(By.ID, element_id) 222 | self.scroll_to(link) 223 | link.click() 224 | self.assertIn(content, self.browser.page_source) 225 | 226 | def click_page_link_text(self, link_text, content): 227 | link = self.browser.find_element(By.PARTIAL_LINK_TEXT, link_text) 228 | self.scroll_to(link) 229 | link.click() 230 | self.assertIn(content, self.browser.page_source) 231 | 232 | def scroll_to(self, link): 233 | self.browser.execute_script("arguments[0].scrollIntoView();", link) 234 | 235 | def assertLoggedIn(self): 236 | self.browser.find_element(By.ID, 'user-nav-username') 237 | 238 | def assertLoggedOut(self): 239 | self.assertNotIn('user-nav-username', self.browser.page_source) 240 | -------------------------------------------------------------------------------- /mapsapp/tests/test_votes.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | 3 | from mapsapp.models import Vote 4 | from mapsapp.tests.aoe2maptest import AbstractAoe2mapTest 5 | from mapsapp.helpers import count_voters 6 | 7 | 8 | class VotesTest(AbstractAoe2mapTest): 9 | 10 | def test_count_voters_two_users_two_rms(self): 11 | first_user = User.objects.create_user(username='firstuser', password='password') 12 | second_user = User.objects.create_user(username='seconduser', password='password') 13 | 14 | rms1 = self.create_sample_map() 15 | rms2 = self.create_sample_map(newer_version=rms1) 16 | 17 | Vote(rms=rms1, user=first_user).save() 18 | Vote(rms=rms2, user=second_user).save() 19 | 20 | self.assertEquals(2, count_voters(rms1)) 21 | self.assertEquals(2, count_voters(rms2)) 22 | 23 | def test_count_voters_one_user_two_rms(self): 24 | user = User.objects.create_user(username='user', password='password') 25 | 26 | rms1 = self.create_sample_map() 27 | rms2 = self.create_sample_map(newer_version=rms1) 28 | 29 | Vote(rms=rms1, user=user).save() 30 | Vote(rms=rms2, user=user).save() 31 | 32 | self.assertEquals(1, count_voters(rms1)) 33 | self.assertEquals(1, count_voters(rms2)) 34 | -------------------------------------------------------------------------------- /mapsapp/tests/testdata/relic_nothing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiegeEngineers/aoe2map/d7d72597ec0c9a4bbf6eb83f3161335730d8daca/mapsapp/tests/testdata/relic_nothing.png -------------------------------------------------------------------------------- /mapsapp/tests/testdata/relic_nothing.rms: -------------------------------------------------------------------------------- 1 | #define LANDMAP 2 | #define NO_CLIFFS 3 | 4 | random_placement 5 | 6 | base_terrain GRASS 7 | create_player_lands 8 | { 9 | terrain_type GRASS 10 | border_fuzziness 1 11 | land_percent 1 12 | base_size 1 13 | } 14 | 15 | 16 | create_object TOWN_CENTER 17 | { 18 | set_place_for_every_player 19 | group_placement_radius 1 20 | min_distance_to_players 0 21 | max_distance_to_players 0 22 | } 23 | 24 | create_object MARKET 25 | { 26 | set_place_for_every_player 27 | group_placement_radius 1 28 | min_distance_to_players 0 29 | max_distance_to_players 0 30 | } 31 | 32 | create_object MONASTERY 33 | { 34 | set_place_for_every_player 35 | group_placement_radius 1 36 | min_distance_to_players 0 37 | max_distance_to_players 0 38 | } 39 | 40 | create_object MONK 41 | { 42 | set_place_for_every_player 43 | min_distance_to_players 3 44 | max_distance_to_players 4 45 | } 46 | 47 | if REGICIDE 48 | create_object KING 49 | { 50 | set_place_for_every_player 51 | min_distance_to_players 6 52 | max_distance_to_players 7 53 | } 54 | 55 | endif 56 | create_object RELIC 57 | { 58 | number_of_objects 5000 59 | min_distance_to_players 5 60 | temp_min_distance_group_placement 5 61 | set_scaling_to_map_size 62 | } 63 | 64 | create_object OAKTREE 65 | { 66 | set_place_for_every_player 67 | group_placement_radius 1 68 | number_of_groups 2 69 | number_of_objects 3 70 | set_gaia_object_only 71 | min_distance_to_players 5 72 | max_distance_to_players 10 73 | } 74 | 75 | create_object OAKTREE 76 | { 77 | set_place_for_every_player 78 | group_placement_radius 1 79 | number_of_groups 6 80 | number_of_objects 5 81 | set_tight_grouping 82 | set_gaia_object_only 83 | set_scaling_to_map_size 84 | min_distance_to_players 20 85 | max_distance_to_players 150 86 | } 87 | -------------------------------------------------------------------------------- /mapsapp/tokens.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.tokens import PasswordResetTokenGenerator 2 | import six 3 | 4 | 5 | class AccountActivationTokenGenerator(PasswordResetTokenGenerator): 6 | def _make_hash_value(self, user, timestamp): 7 | return ( 8 | six.text_type(user.pk) + six.text_type(timestamp) + 9 | six.text_type(user.profile.email_confirmed) 10 | ) 11 | 12 | 13 | email_verification_token = AccountActivationTokenGenerator() 14 | -------------------------------------------------------------------------------- /mapsapp/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import views as auth_views 2 | from django.urls import path, re_path, register_converter 3 | 4 | from mapsapp.views import PasswordResetViewWithCustomDomain 5 | from . import views 6 | 7 | 8 | class UUIDSubstringConverter: 9 | regex = '[-0-9a-f]{1,35}' 10 | 11 | def to_python(self, value): 12 | return value 13 | 14 | def to_url(self, value): 15 | return value 16 | 17 | 18 | register_converter(UUIDSubstringConverter, 'subuuid') 19 | 20 | urlpatterns = [ 21 | path('', views.index, name='index'), 22 | path('info', views.info, name='info'), 23 | path('maps', views.maps, name='maps'), 24 | path('map//', views.rms, name='map'), 25 | path('map/', views.rms_redirect, name='map_uuid'), 26 | path('map//archive', views.rms_archive, name='map_archive'), 27 | path('map/s/', views.map_search, name='map_search'), 28 | path('map/s/', views.map_search, name='map_search_post'), 29 | path('collections', views.collections, name='collections'), 30 | path('collection//', views.collection, name='collection'), 31 | path('collection/', views.collection_redirect, name='collection_uuid'), 32 | path('mappack', views.mappack, name='mappack'), 33 | path('login', views.loginpage, name='login'), 34 | path('register', views.registerpage, name='register'), 35 | path('settings', views.settings, name='settings'), 36 | path('logout', views.logoutpage, name='logout'), 37 | path('mymaps', views.mymaps, name='mymaps'), 38 | path('mycollection', views.mycollections, name='mycollections'), 39 | path('newmap/created/', views.newmap, name='newmap_created'), 40 | path('newmap/', views.newmap, name='newmap_newer_version'), 41 | path('newmap', views.newmap, name='newmap'), 42 | path('edit/', views.editmap, name='editmap'), 43 | path('editcollection/', views.editcollection, name='editcollection'), 44 | path('newcollection', views.editcollection, name='newcollection'), 45 | path('newcollection/', views.editcollection, name='newcollection'), 46 | path('version/', views.version, name='version'), 47 | path('tags/', views.tags, name='tags'), 48 | path('email_verification_sent', views.email_verification_sent, name='email_verification_sent'), 49 | path('verify//', views.verify_email, name='verify_email'), 50 | path('password_reset', PasswordResetViewWithCustomDomain.as_view(), name='password_reset'), 51 | path('password_reset/done', auth_views.PasswordResetDoneView.as_view(), name='password_reset_done'), 52 | path('password_reset//', auth_views.PasswordResetConfirmView.as_view(), 53 | name='password_reset_confirm'), 54 | path('password_reset/complete', auth_views.PasswordResetCompleteView.as_view(), name='password_reset_complete'), 55 | ] 56 | 57 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | adal==1.2.7 2 | appdirs==1.4.4 3 | asgiref==3.7.2 4 | asn1crypto==1.5.1 5 | azure-common==1.1.28 6 | azure-core==1.28.0 7 | azure-storage-blob==12.17.0 8 | azure-storage-common==2.1.0 9 | CacheControl==0.13.1 10 | certifi==2023.7.22 11 | cffi==1.15.1 12 | chardet==5.2.0 13 | charset-normalizer==3.2.0 14 | click==8.1.6 15 | colorama==0.4.6 16 | contextlib2==21.6.0 17 | cryptography==41.0.3 18 | distlib==0.3.7 19 | distro==1.8.0 20 | Django==4.2.4 21 | django-storages==1.13.2 22 | django-widget-tweaks==1.4.12 23 | html5lib==1.1 24 | idna==3.4 25 | ipaddr==2.2.0 26 | isodate==0.6.1 27 | lockfile==0.12.2 28 | msgpack==1.0.5 29 | msrest==0.7.1 30 | msrestazure==0.6.4 31 | oauthlib==3.2.2 32 | packaging==23.1 33 | pep517==0.13.0 34 | Pillow==10.0.0 35 | progress==1.6 36 | pycparser==2.21 37 | PyJWT==2.8.0 38 | pyparsing==3.1.1 39 | python-dateutil==2.8.2 40 | pytoml==0.1.21 41 | pytz==2023.3 42 | requests==2.31.0 43 | requests-oauthlib==1.3.1 44 | retrying==1.3.4 45 | six==1.16.0 46 | sqlparse==0.4.4 47 | toml==0.10.2 48 | tomli==2.0.1 49 | typing_extensions==4.7.1 50 | urllib3==1.26.16 51 | uuid==1.30 52 | webencodings==0.5.1 53 | --------------------------------------------------------------------------------