From 5fd006aca940c773ea031835d435f229dd1c9271 Mon Sep 17 00:00:00 2001 From: adevopg Date: Wed, 15 Jul 2026 14:39:19 +0000 Subject: [PATCH] Jubilar Django: borrar el portal antiguo, la web ya la sirve Next MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit El cutover se hizo hoy: Caddy manda www.nightspire.gg a :3001 (Next), así que Django se quedó sin dominio y sin uso. Se comprobó antes de borrar que nada dependía de él: ninguna referencia a :8001 en Caddy, el timer de reconciliación llama a Next, DJANGO_API_BASE no lo leía ni una línea del código (se quita también de la unidad de systemd), web-next/public es autónomo (736 ficheros reales, 0 symlinks) y no hay ni una referencia a /static/. Con Django parado la web siguió dando 200 en todas las rutas. Se va: home/, novawow/, forum/, wotlk_db/, frontend/ (las islas React que Next sustituyó), static/, staticfiles/, manage.py, requirements.txt, db.sqlite3 y el Docker de Django. Se quedan docs/ y sql/: no son código Django sino documentación y esquemas, y sql/forum_schema.sql describe la BD acore_web que Next usa hoy. La BASE DE DATOS no se toca. django_wow y sus 41 tablas home_* son ahora de Next, que las consulta con SQL directo. El nombre se queda por historia. README reescrito: describía cómo montar un Django que ya no existe (venv, manage.py migrate, gunicorn, Docker). Co-Authored-By: Claude Opus 4.8 (1M context) --- Dockerfile | 32 - README.md | 145 +- db.sqlite3 | 0 docker-compose.yml | 44 - docker/entrypoint.sh | 22 - docker/mysql-init.sql | 14 - forum/__init__.py | 0 forum/apps.py | 7 - forum/db.py | 414 - forum/forms.py | 23 - forum/permissions.py | 71 - forum/sanitize.py | 52 - forum/templates/forum/_style.html | 45 - forum/templates/forum/create_topic.html | 35 - forum/templates/forum/edit_post.html | 33 - forum/templates/forum/forum.html | 63 - forum/templates/forum/index.html | 55 - forum/templates/forum/profile.html | 30 - forum/templates/forum/search.html | 60 - forum/templates/forum/topic.html | 122 - forum/templatetags/__init__.py | 0 forum/templatetags/forum_extras.py | 18 - forum/urls.py | 38 - forum/views.py | 453 - frontend/README.md | 32 - frontend/package-lock.json | 1729 -- frontend/package.json | 22 - frontend/src/components/AccountDashboard.tsx | 176 - frontend/src/components/ChangeEmailForm.tsx | 102 - .../src/components/ChangePasswordForm.tsx | 108 - .../src/components/CharacterPurchaseForm.tsx | 110 - frontend/src/components/GoldForm.tsx | 90 - frontend/src/components/GuildRenameForm.tsx | 114 - frontend/src/components/HistoryTable.tsx | 42 - frontend/src/components/LoginForm.tsx | 158 - frontend/src/components/NewsList.tsx | 78 - frontend/src/components/RecoverForm.tsx | 110 - frontend/src/components/RegisterForm.tsx | 215 - frontend/src/components/ResetPassword.tsx | 131 - .../src/components/RestoreCharacterForm.tsx | 80 - frontend/src/components/RestoreItemsForm.tsx | 124 - frontend/src/components/SecurityTokenForm.tsx | 62 - frontend/src/components/SelectAccount.tsx | 67 - frontend/src/components/ServerStatus.tsx | 74 - frontend/src/components/TransferForm.tsx | 96 - frontend/src/components/VotePanel.tsx | 101 - frontend/src/entries/change_email.tsx | 12 - frontend/src/entries/change_password.tsx | 16 - frontend/src/entries/character_purchase.tsx | 24 - frontend/src/entries/gold_character.tsx | 16 - frontend/src/entries/history_table.tsx | 17 - frontend/src/entries/home.tsx | 16 - frontend/src/entries/login.tsx | 17 - frontend/src/entries/my_account.tsx | 12 - frontend/src/entries/recover.tsx | 16 - frontend/src/entries/register.tsx | 16 - frontend/src/entries/rename_guild.tsx | 14 - frontend/src/entries/reset_password.tsx | 18 - frontend/src/entries/restore_character.tsx | 14 - frontend/src/entries/restore_items.tsx | 14 - frontend/src/entries/security_token.tsx | 16 - frontend/src/entries/select_account.tsx | 18 - frontend/src/entries/transfer_character.tsx | 19 - frontend/src/entries/vote_points.tsx | 14 - frontend/src/stripe.ts | 47 - frontend/src/turnstile.ts | 77 - frontend/src/types.ts | 15 - frontend/tsconfig.json | 21 - frontend/vite.config.ts | 38 - home/__init__.py | 0 home/ac_soap.py | 51 - home/admin.py | 204 - home/api_urls.py | 10 - home/apps.py | 6 - home/bnet.py | 216 - home/context_processors.py | 40 - home/library_correo.py | 35 - home/middleware.py | 28 - home/migrations/0001_initial.py | 114 - ...emove_recruitreward_claimed_by_and_more.py | 37 - .../0003_alter_accountactivation_salt.py | 18 - ...4_alter_accountactivation_salt_and_more.py | 23 - home/migrations/0005_securitytoken.py | 24 - ...changeraceprice_customizeprice_and_more.py | 186 - home/migrations/0007_maintenancemode.py | 20 - ..._storecategory_parent_category_and_more.py | 49 - home/migrations/0009_stripelog.py | 30 - home/migrations/0010_loginattempt.py | 23 - ...er_guildrenamesettings_options_and_more.py | 32 - ...2_alter_accountactivation_salt_and_more.py | 28 - home/migrations/0013_stripelog_fulfilled.py | 18 - home/migrations/0014_passwordreset.py | 24 - home/migrations/__init__.py | 0 home/models/__init__.py | 13 - home/models/accounts.py | 62 - home/models/history.py | 13 - home/models/pricing.py | 51 - home/models/recruit.py | 34 - home/models/site.py | 95 - home/models/store.py | 44 - home/models/voting.py | 35 - home/pagos_stripe.py | 63 - home/pagos_sumup.py | 72 - home/templates/404.html | 9 - home/templates/account/ban_history.html | 9 - home/templates/account/battlepay.html | 60 - home/templates/account/change_email.html | 9 - home/templates/account/change_faction.html | 9 - .../account/change_faction_cancel.html | 9 - .../account/change_faction_success.html | 9 - home/templates/account/change_password.html | 9 - home/templates/account/change_race.html | 9 - .../templates/account/change_race_cancel.html | 9 - .../account/change_race_success.html | 9 - .../account/confirm_new_email_success.html | 9 - .../account/confirm_old_email_success.html | 9 - .../account/customize_character.html | 9 - home/templates/account/customize_success.html | 9 - home/templates/account/d_points.html | 9 - home/templates/account/expired_link.html | 9 - home/templates/account/gold_cancel.html | 9 - home/templates/account/gold_character.html | 9 - home/templates/account/gold_success.html | 9 - .../account/guild_rename_cancel.html | 9 - .../account/guild_rename_success.html | 9 - home/templates/account/level_up.html | 9 - home/templates/account/level_up_cancel.html | 9 - home/templates/account/level_up_success.html | 9 - home/templates/account/pago_sumup.html | 9 - home/templates/account/points_history.html | 9 - home/templates/account/promo_code.html | 9 - home/templates/account/quest_character.html | 9 - home/templates/account/rename_cancel.html | 9 - home/templates/account/rename_character.html | 9 - home/templates/account/rename_guild.html | 9 - home/templates/account/rename_success.html | 9 - home/templates/account/restore_character.html | 9 - home/templates/account/restore_items.html | 9 - home/templates/account/revive_character.html | 9 - home/templates/account/security_history.html | 9 - home/templates/account/security_token.html | 9 - home/templates/account/send_gift.html | 9 - home/templates/account/trade_points.html | 9 - home/templates/account/trans_history.html | 9 - home/templates/account/transfer_cancel.html | 9 - .../templates/account/transfer_character.html | 9 - home/templates/account/transfer_d_points.html | 9 - home/templates/account/transfer_success.html | 9 - home/templates/account/unstuck_character.html | 9 - home/templates/account/vote_points.html | 9 - home/templates/admin/server_selection.html | 44 - home/templates/auth/activation_invalid.html | 9 - home/templates/auth/activation_success.html | 9 - home/templates/auth/login.html | 9 - home/templates/auth/recover_account.html | 9 - home/templates/auth/register.html | 9 - home/templates/auth/reset_password.html | 29 - home/templates/auth/select_account.html | 45 - home/templates/base/base.html | 10 - .../templates/community/content_creators.html | 9 - home/templates/community/help.html | 9 - home/templates/contact/contact_us.html | 9 - home/templates/download/download_client.html | 9 - home/templates/emails/account_names.html | 34 - home/templates/emails/activation.html | 145 - home/templates/emails/confirm_new_email.html | 28 - home/templates/emails/confirm_old_email.html | 28 - .../emails/old_email_notification.html | 27 - home/templates/emails/password_reset.html | 29 - home/templates/emails/security_token.html | 163 - home/templates/home/home.html | 1 - home/templates/legal/cookies.html | 9 - home/templates/legal/legal_notice.html | 9 - home/templates/legal/privacy_policy.html | 9 - home/templates/legal/refund_policy.html | 9 - .../templates/legal/terms_and_conditions.html | 9 - home/templates/maintenance/maintenance.html | 9 - home/templates/my-account/my-account.html | 9 - .../partials/activation_invalid.html | 14 - .../partials/activation_success.html | 17 - home/templates/partials/ban_history.html | 36 - home/templates/partials/change_email.html | 36 - home/templates/partials/change_faction.html | 74 - .../partials/change_faction_cancel.html | 0 .../partials/change_faction_success.html | 0 home/templates/partials/change_password.html | 30 - home/templates/partials/change_race.html | 38 - .../partials/change_race_cancel.html | 0 .../partials/change_race_success.html | 1 - .../partials/confirm_new_email_success.html | 15 - .../partials/confirm_old_email_success.html | 15 - home/templates/partials/contact_us.html | 23 - home/templates/partials/cookies.html | 12 - .../partials/customize_character.html | 43 - .../templates/partials/customize_success.html | 44 - home/templates/partials/d_points.html | 934 - home/templates/partials/descargas.html | 119 - home/templates/partials/expired_link.html | 27 - home/templates/partials/final.html | 2 - home/templates/partials/footer.html | 11 - home/templates/partials/gold_cancel.html | 0 home/templates/partials/gold_character.html | 24 - home/templates/partials/gold_success.html | 0 .../partials/guild_rename_cancel.html | 0 .../partials/guild_rename_success.html | 0 home/templates/partials/head.html | 125 - home/templates/partials/header.html | 66 - home/templates/partials/help.html | 82 - home/templates/partials/legal_notice.html | 24 - home/templates/partials/level_up.html | 39 - home/templates/partials/level_up_cancel.html | 0 home/templates/partials/level_up_success.html | 0 home/templates/partials/login.html | 46 - home/templates/partials/maintenance.html | 16 - home/templates/partials/my-account.html | 410 - home/templates/partials/not_found.html | 12 - home/templates/partials/noticias.html | 53 - home/templates/partials/novawow-realm.html | 31 - home/templates/partials/novawow_players.html | 159 - home/templates/partials/pago_sumup.html | 38 - home/templates/partials/points_history.html | 27 - home/templates/partials/privacy_policy.html | 68 - home/templates/partials/promo_code.html | 29 - home/templates/partials/quest_character.html | 90 - home/templates/partials/recover_account.html | 41 - home/templates/partials/recruit_a_friend.html | 190 - home/templates/partials/refund_policy.html | 96 - home/templates/partials/register.html | 71 - home/templates/partials/rename_cancel.html | 35 - home/templates/partials/rename_character.html | 34 - home/templates/partials/rename_guild.html | 46 - home/templates/partials/rename_success.html | 0 .../templates/partials/restore_character.html | 51 - home/templates/partials/restore_items.html | 32 - home/templates/partials/revive_character.html | 34 - home/templates/partials/security_history.html | 28 - home/templates/partials/security_token.html | 47 - home/templates/partials/send_gift.html | 81 - home/templates/partials/social.html | 28 - home/templates/partials/start_time.html | 1 - .../partials/store/store_novawow.html | 153 - .../partials/store/store_novawow_cancel.html | 0 .../partials/store/store_novawow_success.html | 0 .../partials/terms_and_conditions.html | 107 - home/templates/partials/trade_points.html | 156 - home/templates/partials/trans_history.html | 40 - home/templates/partials/transfer_cancel.html | 0 .../partials/transfer_character.html | 23 - .../templates/partials/transfer_d_points.html | 33 - home/templates/partials/transfer_success.html | 0 .../templates/partials/unstuck_character.html | 39 - home/templates/partials/video.html | 13 - home/templates/partials/videos.html | 50 - home/templates/partials/vote_points.html | 65 - home/templates/realms/novawow_players.html | 9 - home/templates/realms/novawow_realm.html | 9 - home/templates/recruit/recruit_a_friend.html | 9 - .../server_status/server_status.html | 21 - home/templates/store/store_novawow.html | 9 - .../templates/store/store_novawow_cancel.html | 9 - .../store/store_novawow_success.html | 9 - home/tests.py | 3 - home/urls.py | 87 - home/views/__init__.py | 19 - home/views/_base.py | 101 - home/views/account.py | 268 - home/views/api.py | 109 - home/views/auth.py | 719 - home/views/characters.py | 1005 - home/views/guild.py | 127 - home/views/history.py | 97 - home/views/pages.py | 92 - home/views/payments.py | 132 - home/views/players.py | 228 - home/views/points.py | 113 - home/views/recruit.py | 161 - home/views/store.py | 173 - home/zone_definitions.py | 9166 ------ manage.py | 22 - novawow/__init__.py | 0 novawow/asgi.py | 16 - novawow/settings.py | 313 - novawow/urls.py | 24 - novawow/views.py | 4 - novawow/wsgi.py | 16 - requirements.txt | 20 - static/admin/css/autocomplete.css | 279 - static/admin/css/base.css | 1179 - static/admin/css/changelists.css | 343 - static/admin/css/dark_mode.css | 130 - static/admin/css/dashboard.css | 29 - static/admin/css/forms.css | 512 - static/admin/css/login.css | 61 - static/admin/css/nav_sidebar.css | 150 - static/admin/css/responsive.css | 967 - static/admin/css/responsive_rtl.css | 111 - static/admin/css/rtl.css | 291 - static/admin/css/unusable_password_field.css | 19 - .../css/vendor/select2/LICENSE-SELECT2.md | 21 - static/admin/css/vendor/select2/select2.css | 481 - .../admin/css/vendor/select2/select2.min.css | 1 - static/admin/css/widgets.css | 593 - static/admin/img/LICENSE | 20 - static/admin/img/README.txt | 7 - static/admin/img/calendar-icons.svg | 63 - static/admin/img/gis/move_vertex_off.svg | 1 - static/admin/img/gis/move_vertex_on.svg | 1 - static/admin/img/icon-addlink.svg | 3 - static/admin/img/icon-alert.svg | 3 - static/admin/img/icon-calendar.svg | 9 - static/admin/img/icon-changelink.svg | 3 - static/admin/img/icon-clock.svg | 9 - static/admin/img/icon-deletelink.svg | 3 - static/admin/img/icon-hidelink.svg | 3 - static/admin/img/icon-no.svg | 3 - static/admin/img/icon-unknown-alt.svg | 3 - static/admin/img/icon-unknown.svg | 3 - static/admin/img/icon-viewlink.svg | 3 - static/admin/img/icon-yes.svg | 3 - static/admin/img/inline-delete.svg | 3 - static/admin/img/search.svg | 3 - static/admin/img/selector-icons.svg | 34 - static/admin/img/sorting-icons.svg | 19 - static/admin/img/tooltag-add.svg | 3 - static/admin/img/tooltag-arrowright.svg | 3 - static/admin/js/SelectBox.js | 116 - static/admin/js/SelectFilter2.js | 286 - static/admin/js/actions.js | 204 - static/admin/js/admin/DateTimeShortcuts.js | 408 - static/admin/js/admin/RelatedObjectLookups.js | 240 - static/admin/js/autocomplete.js | 33 - static/admin/js/calendar.js | 239 - static/admin/js/cancel.js | 29 - static/admin/js/change_form.js | 16 - static/admin/js/collapse.js | 43 - static/admin/js/core.js | 184 - static/admin/js/filters.js | 30 - static/admin/js/inlines.js | 359 - static/admin/js/jquery.init.js | 8 - static/admin/js/nav_sidebar.js | 79 - static/admin/js/popup_response.js | 15 - static/admin/js/prepopulate.js | 43 - static/admin/js/prepopulate_init.js | 15 - static/admin/js/theme.js | 51 - static/admin/js/unusable_password_field.js | 29 - static/admin/js/urlify.js | 169 - static/admin/js/vendor/jquery/LICENSE.txt | 20 - static/admin/js/vendor/jquery/jquery.js | 10716 ------- static/admin/js/vendor/jquery/jquery.min.js | 2 - static/admin/js/vendor/select2/LICENSE.md | 21 - static/admin/js/vendor/select2/i18n/af.js | 3 - static/admin/js/vendor/select2/i18n/ar.js | 3 - static/admin/js/vendor/select2/i18n/az.js | 3 - static/admin/js/vendor/select2/i18n/bg.js | 3 - static/admin/js/vendor/select2/i18n/bn.js | 3 - static/admin/js/vendor/select2/i18n/bs.js | 3 - static/admin/js/vendor/select2/i18n/ca.js | 3 - static/admin/js/vendor/select2/i18n/cs.js | 3 - static/admin/js/vendor/select2/i18n/da.js | 3 - static/admin/js/vendor/select2/i18n/de.js | 3 - static/admin/js/vendor/select2/i18n/dsb.js | 3 - static/admin/js/vendor/select2/i18n/el.js | 3 - static/admin/js/vendor/select2/i18n/en.js | 3 - static/admin/js/vendor/select2/i18n/es.js | 3 - static/admin/js/vendor/select2/i18n/et.js | 3 - static/admin/js/vendor/select2/i18n/eu.js | 3 - static/admin/js/vendor/select2/i18n/fa.js | 3 - static/admin/js/vendor/select2/i18n/fi.js | 3 - static/admin/js/vendor/select2/i18n/fr.js | 3 - static/admin/js/vendor/select2/i18n/gl.js | 3 - static/admin/js/vendor/select2/i18n/he.js | 3 - static/admin/js/vendor/select2/i18n/hi.js | 3 - static/admin/js/vendor/select2/i18n/hr.js | 3 - static/admin/js/vendor/select2/i18n/hsb.js | 3 - static/admin/js/vendor/select2/i18n/hu.js | 3 - static/admin/js/vendor/select2/i18n/hy.js | 3 - static/admin/js/vendor/select2/i18n/id.js | 3 - static/admin/js/vendor/select2/i18n/is.js | 3 - static/admin/js/vendor/select2/i18n/it.js | 3 - static/admin/js/vendor/select2/i18n/ja.js | 3 - static/admin/js/vendor/select2/i18n/ka.js | 3 - static/admin/js/vendor/select2/i18n/km.js | 3 - static/admin/js/vendor/select2/i18n/ko.js | 3 - static/admin/js/vendor/select2/i18n/lt.js | 3 - static/admin/js/vendor/select2/i18n/lv.js | 3 - static/admin/js/vendor/select2/i18n/mk.js | 3 - static/admin/js/vendor/select2/i18n/ms.js | 3 - static/admin/js/vendor/select2/i18n/nb.js | 3 - static/admin/js/vendor/select2/i18n/ne.js | 3 - static/admin/js/vendor/select2/i18n/nl.js | 3 - static/admin/js/vendor/select2/i18n/pl.js | 3 - static/admin/js/vendor/select2/i18n/ps.js | 3 - static/admin/js/vendor/select2/i18n/pt-BR.js | 3 - static/admin/js/vendor/select2/i18n/pt.js | 3 - static/admin/js/vendor/select2/i18n/ro.js | 3 - static/admin/js/vendor/select2/i18n/ru.js | 3 - static/admin/js/vendor/select2/i18n/sk.js | 3 - static/admin/js/vendor/select2/i18n/sl.js | 3 - static/admin/js/vendor/select2/i18n/sq.js | 3 - .../admin/js/vendor/select2/i18n/sr-Cyrl.js | 3 - static/admin/js/vendor/select2/i18n/sr.js | 3 - static/admin/js/vendor/select2/i18n/sv.js | 3 - static/admin/js/vendor/select2/i18n/th.js | 3 - static/admin/js/vendor/select2/i18n/tk.js | 3 - static/admin/js/vendor/select2/i18n/tr.js | 3 - static/admin/js/vendor/select2/i18n/uk.js | 3 - static/admin/js/vendor/select2/i18n/vi.js | 3 - static/admin/js/vendor/select2/i18n/zh-CN.js | 3 - static/admin/js/vendor/select2/i18n/zh-TW.js | 3 - .../admin/js/vendor/select2/select2.full.js | 6820 ----- .../js/vendor/select2/select2.full.min.js | 2 - static/admin/js/vendor/xregexp/LICENSE.txt | 21 - static/admin/js/vendor/xregexp/xregexp.js | 6126 ---- static/admin/js/vendor/xregexp/xregexp.min.js | 17 - static/android-chrome-192x192.png | Bin 18496 -> 0 bytes static/apple-touch-icon.png | Bin 12046 -> 0 bytes static/browserconfig.xml | 12 - static/css/basic.css | 206 - static/css/global.css | 847 - static/css/home.css | 199 - static/css/locale_eses.css | 70 - static/css/novawow.css | 4693 --- static/django_ckeditor_5/app.js | 187 - static/django_ckeditor_5/dist/bundle.js | 3 - .../dist/bundle.js.LICENSE.txt | 25 - static/django_ckeditor_5/dist/bundle.js.map | 1 - static/django_ckeditor_5/dist/styles.css | 114 - static/django_ckeditor_5/dist/styles.css.map | 1 - .../django_ckeditor_5/dist/translations/af.js | 1 - .../django_ckeditor_5/dist/translations/ar.js | 1 - .../dist/translations/ast.js | 1 - .../django_ckeditor_5/dist/translations/az.js | 1 - .../django_ckeditor_5/dist/translations/bg.js | 1 - .../django_ckeditor_5/dist/translations/bn.js | 1 - .../django_ckeditor_5/dist/translations/bs.js | 1 - .../django_ckeditor_5/dist/translations/ca.js | 1 - .../django_ckeditor_5/dist/translations/cs.js | 1 - .../django_ckeditor_5/dist/translations/da.js | 1 - .../dist/translations/de-ch.js | 1 - .../django_ckeditor_5/dist/translations/de.js | 1 - .../django_ckeditor_5/dist/translations/el.js | 1 - .../dist/translations/en-au.js | 1 - .../dist/translations/en-gb.js | 1 - .../django_ckeditor_5/dist/translations/eo.js | 1 - .../dist/translations/es-co.js | 1 - .../django_ckeditor_5/dist/translations/es.js | 1 - .../django_ckeditor_5/dist/translations/et.js | 1 - .../django_ckeditor_5/dist/translations/eu.js | 1 - .../django_ckeditor_5/dist/translations/fa.js | 1 - .../django_ckeditor_5/dist/translations/fi.js | 1 - .../django_ckeditor_5/dist/translations/fr.js | 1 - .../django_ckeditor_5/dist/translations/gl.js | 1 - .../django_ckeditor_5/dist/translations/gu.js | 1 - .../django_ckeditor_5/dist/translations/he.js | 1 - .../django_ckeditor_5/dist/translations/hi.js | 1 - .../django_ckeditor_5/dist/translations/hr.js | 1 - .../django_ckeditor_5/dist/translations/hu.js | 1 - .../django_ckeditor_5/dist/translations/hy.js | 1 - .../django_ckeditor_5/dist/translations/id.js | 1 - .../django_ckeditor_5/dist/translations/it.js | 1 - .../django_ckeditor_5/dist/translations/ja.js | 1 - .../django_ckeditor_5/dist/translations/jv.js | 1 - .../django_ckeditor_5/dist/translations/kk.js | 1 - .../django_ckeditor_5/dist/translations/km.js | 1 - .../django_ckeditor_5/dist/translations/kn.js | 1 - .../django_ckeditor_5/dist/translations/ko.js | 1 - .../django_ckeditor_5/dist/translations/ku.js | 1 - .../django_ckeditor_5/dist/translations/lt.js | 1 - .../django_ckeditor_5/dist/translations/lv.js | 1 - .../django_ckeditor_5/dist/translations/ms.js | 1 - .../django_ckeditor_5/dist/translations/nb.js | 1 - .../django_ckeditor_5/dist/translations/ne.js | 1 - .../django_ckeditor_5/dist/translations/nl.js | 1 - .../django_ckeditor_5/dist/translations/no.js | 1 - .../django_ckeditor_5/dist/translations/oc.js | 1 - .../django_ckeditor_5/dist/translations/pl.js | 1 - .../dist/translations/pt-br.js | 1 - .../django_ckeditor_5/dist/translations/pt.js | 1 - .../django_ckeditor_5/dist/translations/ro.js | 1 - .../django_ckeditor_5/dist/translations/ru.js | 1 - .../django_ckeditor_5/dist/translations/si.js | 1 - .../django_ckeditor_5/dist/translations/sk.js | 1 - .../django_ckeditor_5/dist/translations/sl.js | 1 - .../django_ckeditor_5/dist/translations/sq.js | 1 - .../dist/translations/sr-latn.js | 1 - .../django_ckeditor_5/dist/translations/sr.js | 1 - .../django_ckeditor_5/dist/translations/sv.js | 1 - .../django_ckeditor_5/dist/translations/th.js | 1 - .../django_ckeditor_5/dist/translations/ti.js | 1 - .../django_ckeditor_5/dist/translations/tk.js | 1 - .../django_ckeditor_5/dist/translations/tr.js | 1 - .../django_ckeditor_5/dist/translations/tt.js | 1 - .../django_ckeditor_5/dist/translations/ug.js | 1 - .../django_ckeditor_5/dist/translations/uk.js | 1 - .../django_ckeditor_5/dist/translations/ur.js | 1 - .../django_ckeditor_5/dist/translations/uz.js | 1 - .../django_ckeditor_5/dist/translations/vi.js | 1 - .../dist/translations/zh-cn.js | 1 - .../django_ckeditor_5/dist/translations/zh.js | 1 - static/django_ckeditor_5/src/ckeditor.js | 114 - .../django_ckeditor_5/src/override-django.css | 47 - .../css/jquery.autocomplete.css | 38 - static/django_extensions/img/indicator.gif | Bin 1553 -> 0 bytes .../django_extensions/js/jquery.ajaxQueue.js | 116 - .../js/jquery.autocomplete.js | 1152 - .../django_extensions/js/jquery.bgiframe.js | 39 - static/download/searchplugins/novawow.xml | 8 - static/favicon-16x16.png | Bin 662 -> 0 bytes static/favicon-194x194.png | Bin 19744 -> 0 bytes static/favicon-32x32.png | Bin 1359 -> 0 bytes static/favicon.ico | Bin 15086 -> 0 bytes static/images/Menu/check.gif | Bin 54 -> 0 bytes static/images/eses/modelviewer-picshures.gif | Bin 3484 -> 0 bytes static/images/icons/facebook.webp | Bin 190 -> 0 bytes static/images/icons/forum.webp | Bin 532 -> 0 bytes static/images/icons/twitter.webp | Bin 196 -> 0 bytes static/images/icons/web.webp | Bin 530 -> 0 bytes static/images/icons/wow-death-knight.webp | Bin 3570 -> 0 bytes static/images/icons/wow-druid.webp | Bin 3880 -> 0 bytes static/images/icons/wow-hunter.webp | Bin 2724 -> 0 bytes static/images/icons/wow-mage.png | Bin 6205 -> 0 bytes static/images/icons/wow-paladin.webp | Bin 3076 -> 0 bytes static/images/icons/wow-priest.webp | Bin 2664 -> 0 bytes static/images/icons/wow-rogue.webp | Bin 3308 -> 0 bytes static/images/icons/wow-shaman.webp | Bin 4082 -> 0 bytes static/images/icons/wow-warlock.png | Bin 6522 -> 0 bytes static/images/icons/wow-warrior.webp | Bin 3080 -> 0 bytes static/images/logos/favicon.ico | Bin 15086 -> 0 bytes static/images/logos/home-background.jpg | Bin 527794 -> 0 bytes static/images/logos/home.webp | Bin 33208 -> 0 bytes .../wow/icons/tiny/ability_druid_swipe.gif | Bin 414 -> 0 bytes .../tiny/ability_hunter_combatexperience.gif | Bin 564 -> 0 bytes .../icons/tiny/ability_hunter_pet_bear.gif | Bin 494 -> 0 bytes .../wow/icons/tiny/class_deathknight.gif | Bin 504 -> 0 bytes static/images/wow/icons/tiny/class_druid.gif | Bin 388 -> 0 bytes static/images/wow/icons/tiny/class_hunter.gif | Bin 378 -> 0 bytes static/images/wow/icons/tiny/class_mage.gif | Bin 532 -> 0 bytes .../images/wow/icons/tiny/class_paladin.gif | Bin 498 -> 0 bytes static/images/wow/icons/tiny/class_priest.gif | Bin 408 -> 0 bytes static/images/wow/icons/tiny/class_rogue.gif | Bin 486 -> 0 bytes static/images/wow/icons/tiny/class_shaman.gif | Bin 434 -> 0 bytes .../images/wow/icons/tiny/class_warlock.gif | Bin 486 -> 0 bytes .../images/wow/icons/tiny/class_warrior.gif | Bin 462 -> 0 bytes .../tiny/inv_inscription_tradeskill01.gif | Bin 574 -> 0 bytes .../wow/icons/tiny/inv_misc_armorkit_17.gif | Bin 264 -> 0 bytes .../wow/icons/tiny/inv_misc_food_15.gif | Bin 456 -> 0 bytes .../images/wow/icons/tiny/inv_misc_gem_01.gif | Bin 430 -> 0 bytes .../wow/icons/tiny/inv_misc_pelt_wolf_01.gif | Bin 406 -> 0 bytes .../wow/icons/tiny/race_bloodelf_female.gif | Bin 470 -> 0 bytes .../wow/icons/tiny/race_draenei_female.gif | Bin 444 -> 0 bytes .../wow/icons/tiny/race_dwarf_female.gif | Bin 468 -> 0 bytes .../wow/icons/tiny/race_gnome_female.gif | Bin 508 -> 0 bytes .../wow/icons/tiny/race_human_female.gif | Bin 506 -> 0 bytes .../wow/icons/tiny/race_nightelf_female.gif | Bin 506 -> 0 bytes .../images/wow/icons/tiny/race_orc_female.gif | Bin 436 -> 0 bytes .../wow/icons/tiny/race_scourge_female.gif | Bin 382 -> 0 bytes .../wow/icons/tiny/race_tauren_female.gif | Bin 520 -> 0 bytes .../wow/icons/tiny/race_troll_female.gif | Bin 518 -> 0 bytes .../icons/tiny/spell_holy_sealofsacrifice.gif | Bin 524 -> 0 bytes .../tiny/spell_nature_naturetouchgrow.gif | Bin 366 -> 0 bytes .../wow/icons/tiny/spell_nature_swiftness.gif | Bin 526 -> 0 bytes .../images/wow/icons/tiny/trade_alchemy.gif | Bin 432 -> 0 bytes .../wow/icons/tiny/trade_blacksmithing.gif | Bin 450 -> 0 bytes .../wow/icons/tiny/trade_engineering.gif | Bin 408 -> 0 bytes .../images/wow/icons/tiny/trade_engraving.gif | Bin 542 -> 0 bytes .../images/wow/icons/tiny/trade_fishing.gif | Bin 356 -> 0 bytes static/images/wow/icons/tiny/trade_mining.gif | Bin 420 -> 0 bytes .../images/wow/icons/tiny/trade_tailoring.gif | Bin 456 -> 0 bytes static/js/Markup.js | 4680 --- static/js/TalentCalc.js | 2919 -- static/js/basic.js | 2426 -- static/js/global.js | 24119 ---------------- static/js/home.js | 22 - static/js/locale.js | 68 - static/js/locale_eses.js | 4832 ---- static/js/swfobject.js | 777 - static/js/talent.js | 150 - static/mstile-144x144.png | Bin 2014 -> 0 bytes .../nw-themes/nw-ryu/nw-css/novawow-style.css | 2384 -- .../nw-ryu/nw-font/FuturaEF-Book.woff | Bin 25108 -> 0 bytes .../nw-images/nw-avatars/default-avatar.webp | Bin 1912 -> 0 bytes .../nw-classes/death-knight-chest.webp | Bin 84068 -> 0 bytes .../nw-classes/death-knight-medium.jpg | Bin 1149 -> 0 bytes .../nw-images/nw-classes/druid-chest.webp | Bin 75569 -> 0 bytes .../nw-images/nw-classes/druid-medium.jpg | Bin 1043 -> 0 bytes .../nw-images/nw-classes/hunter-chest.webp | Bin 92837 -> 0 bytes .../nw-images/nw-classes/hunter-medium.jpg | Bin 1073 -> 0 bytes .../nw-images/nw-classes/mage-chest.webp | Bin 86959 -> 0 bytes .../nw-images/nw-classes/mage-medium.jpg | Bin 1163 -> 0 bytes .../nw-images/nw-classes/paladin-chest.webp | Bin 86411 -> 0 bytes .../nw-images/nw-classes/paladin-medium.jpg | Bin 1235 -> 0 bytes .../nw-images/nw-classes/priest-chest.webp | Bin 84460 -> 0 bytes .../nw-images/nw-classes/priest-medium.jpg | Bin 1118 -> 0 bytes .../nw-images/nw-classes/rogue-chest.webp | Bin 77234 -> 0 bytes .../nw-images/nw-classes/rogue-medium.jpg | Bin 1163 -> 0 bytes .../nw-images/nw-classes/shaman-chest.webp | Bin 93400 -> 0 bytes .../nw-images/nw-classes/shaman-medium.jpg | Bin 1279 -> 0 bytes .../nw-images/nw-classes/warlock-chest.webp | Bin 77994 -> 0 bytes .../nw-images/nw-classes/warlock-medium.jpg | Bin 1166 -> 0 bytes .../nw-images/nw-classes/warrior-chest.webp | Bin 118851 -> 0 bytes .../nw-images/nw-classes/warrior-medium.jpg | Bin 1043 -> 0 bytes .../nw-classes/wow-death-knight.webp | Bin 3570 -> 0 bytes .../nw-ryu/nw-images/nw-classes/wow-druid.png | Bin 6491 -> 0 bytes .../nw-images/nw-classes/wow-hunter.webp | Bin 2724 -> 0 bytes .../nw-ryu/nw-images/nw-classes/wow-mage.webp | Bin 3998 -> 0 bytes .../nw-images/nw-classes/wow-paladin.webp | Bin 3076 -> 0 bytes .../nw-images/nw-classes/wow-priest.webp | Bin 2664 -> 0 bytes .../nw-images/nw-classes/wow-rogue.webp | Bin 3308 -> 0 bytes .../nw-images/nw-classes/wow-shaman.webp | Bin 4082 -> 0 bytes .../nw-images/nw-classes/wow-warlock.webp | Bin 3522 -> 0 bytes .../nw-images/nw-classes/wow-warrior.webp | Bin 3080 -> 0 bytes .../nw-ryu/nw-images/nw-country-flags/ar.webp | Bin 338 -> 0 bytes .../nw-ryu/nw-images/nw-country-flags/bo.webp | Bin 314 -> 0 bytes .../nw-ryu/nw-images/nw-country-flags/cl.webp | Bin 310 -> 0 bytes .../nw-ryu/nw-images/nw-country-flags/co.webp | Bin 342 -> 0 bytes .../nw-ryu/nw-images/nw-country-flags/cr.webp | Bin 332 -> 0 bytes .../nw-ryu/nw-images/nw-country-flags/ec.webp | Bin 354 -> 0 bytes .../nw-ryu/nw-images/nw-country-flags/gt.webp | Bin 294 -> 0 bytes .../nw-ryu/nw-images/nw-country-flags/mx.png | Bin 441 -> 0 bytes .../nw-ryu/nw-images/nw-country-flags/pa.png | Bin 402 -> 0 bytes .../nw-ryu/nw-images/nw-country-flags/pe.webp | Bin 222 -> 0 bytes .../nw-ryu/nw-images/nw-country-flags/py.webp | Bin 326 -> 0 bytes .../nw-ryu/nw-images/nw-country-flags/uy.webp | Bin 364 -> 0 bytes .../nw-general/account-settings.webp | Bin 17602 -> 0 bytes .../nw-general/novawow-maintenance.webp | Bin 562812 -> 0 bytes .../nw-images/nw-general/nw-divider-mini.webp | Bin 760 -> 0 bytes .../nw-images/nw-general/nw-divider.png | Bin 2186 -> 0 bytes .../nw-images/nw-icons/account-icons.webp | Bin 176042 -> 0 bytes .../nw-icons/achievement-level-80.jpg | Bin 1275 -> 0 bytes .../nw-images/nw-icons/alliance-icon.jpg | Bin 1356 -> 0 bytes .../nw-icons/alliance-no-border.webp | Bin 7196 -> 0 bytes .../nw-icons/death-knight-medium.jpg | Bin 1149 -> 0 bytes .../nw-images/nw-icons/druid-medium.jpg | Bin 1043 -> 0 bytes .../nw-ryu/nw-images/nw-icons/horde-icon.jpg | Bin 1247 -> 0 bytes .../nw-images/nw-icons/horde-no-border.webp | Bin 3632 -> 0 bytes .../nw-images/nw-icons/hunter-medium.jpg | Bin 1073 -> 0 bytes .../nw-ryu/nw-images/nw-icons/mage-medium.jpg | Bin 1163 -> 0 bytes .../nw-images/nw-icons/money-copper.gif | Bin 234 -> 0 bytes .../nw-ryu/nw-images/nw-icons/money-gold.gif | Bin 244 -> 0 bytes .../nw-images/nw-icons/money-silver.webp | Bin 196 -> 0 bytes .../nw-images/nw-icons/paladin-medium.jpg | Bin 1235 -> 0 bytes .../nw-images/nw-icons/priest-medium.jpg | Bin 1118 -> 0 bytes .../nw-ryu/nw-images/nw-icons/refresh.webp | Bin 786 -> 0 bytes .../nw-images/nw-icons/rogue-medium.jpg | Bin 1163 -> 0 bytes .../nw-images/nw-icons/shaman-medium.jpg | Bin 1279 -> 0 bytes .../nw-images/nw-icons/warlock-medium.jpg | Bin 1166 -> 0 bytes .../nw-images/nw-icons/warrior-medium.jpg | Bin 1043 -> 0 bytes .../nw-images/nw-logos/bancolombia-logo.webp | Bin 4120 -> 0 bytes .../nw-images/nw-logos/discord-logo.webp | Bin 548 -> 0 bytes .../nw-images/nw-logos/dlocalgo-logo.webp | Bin 574 -> 0 bytes .../nw-ryu/nw-images/nw-logos/facebook.png | Bin 395179 -> 0 bytes .../nw-ryu/nw-images/nw-logos/nequi-logo.webp | Bin 3126 -> 0 bytes .../novawow-main-logo-transparent.webp | Bin 138 -> 0 bytes .../nw-ryu/nw-images/nw-logos/nw-logo.webp | Bin 1858 -> 0 bytes .../nw-images/nw-logos/nw-long-logo.webp | Bin 6602 -> 0 bytes .../nw-images/nw-logos/oficina-peru.png | Bin 264251 -> 0 bytes .../nw-images/nw-logos/paypal-logo.webp | Bin 10408 -> 0 bytes .../nw-images/nw-logos/paypal-p-logo.webp | Bin 606 -> 0 bytes .../nw-ryu/nw-images/nw-logos/plin-peru.webp | Bin 122828 -> 0 bytes .../nw-images/nw-logos/skrill-logo-small.webp | Bin 230 -> 0 bytes .../nw-images/nw-logos/skrill-logo.webp | Bin 1358 -> 0 bytes .../nw-ryu/nw-images/nw-logos/twitch.png | Bin 463189 -> 0 bytes .../nw-images/nw-logos/whatsapp-logo.webp | Bin 682 -> 0 bytes .../nw-ryu/nw-images/nw-logos/yape-peru.png | Bin 170459 -> 0 bytes .../nw-ryu/nw-images/nw-logos/youtube.webp | Bin 208226 -> 0 bytes .../nw-images/nw-mails/facebook-logo-gray.png | Bin 605 -> 0 bytes .../nw-mails/instagram-logo-gray.png | Bin 640 -> 0 bytes .../nw-images/nw-mails/nw-mail-body.jpg | Bin 84498 -> 0 bytes .../nw-images/nw-mails/nw-mail-footer.jpg | Bin 28394 -> 0 bytes .../nw-images/nw-mails/nw-mail-logo.webp | Bin 177412 -> 0 bytes .../nw-images/nw-mails/twitter-logo-gray.png | Bin 602 -> 0 bytes .../nw-images/nw-mails/youtube-logo-gray.png | Bin 402 -> 0 bytes .../nw-races/big-blood-elf-female.webp | Bin 6216 -> 0 bytes .../nw-races/big-blood-elf-male.webp | Bin 6338 -> 0 bytes .../nw-races/big-draenei-female.webp | Bin 5548 -> 0 bytes .../nw-images/nw-races/big-draenei-male.webp | Bin 6062 -> 0 bytes .../nw-images/nw-races/big-dwarf-female.webp | Bin 5358 -> 0 bytes .../nw-images/nw-races/big-dwarf-male.webp | Bin 5378 -> 0 bytes .../nw-images/nw-races/big-gnome-female.webp | Bin 5784 -> 0 bytes .../nw-images/nw-races/big-gnome-male.webp | Bin 5692 -> 0 bytes .../nw-images/nw-races/big-human-female.webp | Bin 5644 -> 0 bytes .../nw-images/nw-races/big-human-male.webp | Bin 5894 -> 0 bytes .../nw-races/big-night-elf-female.webp | Bin 6276 -> 0 bytes .../nw-races/big-night-elf-male.webp | Bin 6166 -> 0 bytes .../nw-images/nw-races/big-orc-female.webp | Bin 5900 -> 0 bytes .../nw-images/nw-races/big-orc-male.webp | Bin 6324 -> 0 bytes .../nw-images/nw-races/big-tauren-female.webp | Bin 5396 -> 0 bytes .../nw-images/nw-races/big-tauren-male.webp | Bin 5958 -> 0 bytes .../nw-images/nw-races/big-troll-female.webp | Bin 5932 -> 0 bytes .../nw-images/nw-races/big-troll-male.webp | Bin 6940 -> 0 bytes .../nw-images/nw-races/big-undead-female.webp | Bin 4890 -> 0 bytes .../nw-images/nw-races/big-undead-male.webp | Bin 5536 -> 0 bytes .../nw-images/nw-ranks/10_Proficient.svg | 18 - .../nw-images/nw-ranks/11_Experienced.svg | 13 - .../nw-ryu/nw-images/nw-ranks/12_Mentor.svg | 13 - .../nw-ryu/nw-images/nw-ranks/13_Veteran.svg | 13 - .../nw-images/nw-ranks/14_GrandMaster.svg | 13 - .../nw-ryu/nw-images/nw-ranks/1_Newbie.svg | 13 - .../nw-ryu/nw-images/nw-ranks/2_Rookie.svg | 13 - .../nw-images/nw-ranks/3_Apprentice.svg | 13 - .../nw-ryu/nw-images/nw-ranks/4_Explorer.svg | 13 - .../nw-images/nw-ranks/5_Contributor.svg | 13 - .../nw-images/nw-ranks/6_Enthusiast.svg | 13 - .../nw-images/nw-ranks/7_Collaborator.svg | 13 - .../nw-ryu/nw-images/nw-ranks/8_Regular.svg | 13 - .../nw-images/nw-ranks/9_RisingStar.svg | 18 - .../nw-images/nw-vote-sites/arenatop100.webp | Bin 9086 -> 0 bytes .../nw-images/nw-vote-sites/gtop100.jpg | Bin 2425 -> 0 bytes .../nw-images/nw-vote-sites/top100arena.jpg | Bin 2283 -> 0 bytes .../nw-ryu/nw-images/nw-vote-sites/topg.webp | Bin 3840 -> 0 bytes .../nw-ryu/nw-js-handlers/create_check.js | 1883 -- .../nw-js-handlers/my_account_response.js | 1 - .../nw-js-handlers/promo_code_response.js | 59 - .../nw-ryu/nw-js-handlers/recruit-response.js | 62 - .../nw-ryu/nw-js-handlers/rename_response.js | 75 - .../nw-js-handlers/store_novawow_response.js | 91 - .../nw-ryu/nw-js-handlers/store_response.js | 28 - .../nw-js-handlers/trade_points_response.js | 4 - static/nw-themes/nw-ryu/nw-js/nw-scripts.js | 35 - static/nw-themes/nw-ryu/nw-js/power.js | 871 - static/nw-themes/nw-ryu/nw-videos/nw-logo.mp4 | Bin 12092315 -> 0 bytes static/safari-pinned-tab.svg | 49 - static/site.webmanifest | 20 - static/widgets/power.js | 874 - wotlk_db/__init__.py | 0 wotlk_db/__pycache__/__init__.cpython-313.pyc | Bin 157 -> 0 bytes wotlk_db/__pycache__/admin.cpython-313.pyc | Bin 201 -> 0 bytes wotlk_db/__pycache__/apps.cpython-313.pyc | Bin 524 -> 0 bytes wotlk_db/__pycache__/models.cpython-313.pyc | Bin 198 -> 0 bytes wotlk_db/__pycache__/urls.cpython-313.pyc | Bin 332 -> 0 bytes wotlk_db/__pycache__/views.cpython-313.pyc | Bin 361 -> 0 bytes wotlk_db/admin.py | 3 - wotlk_db/apps.py | 6 - wotlk_db/migrations/__init__.py | 0 .../__pycache__/__init__.cpython-313.pyc | Bin 168 -> 0 bytes wotlk_db/models.py | 3 - wotlk_db/templates/home/inicio.html | 103 - wotlk_db/tests.py | 3 - wotlk_db/urls.py | 6 - wotlk_db/views.py | 5 - 740 files changed, 56 insertions(+), 112924 deletions(-) delete mode 100644 Dockerfile delete mode 100644 db.sqlite3 delete mode 100644 docker-compose.yml delete mode 100755 docker/entrypoint.sh delete mode 100644 docker/mysql-init.sql delete mode 100644 forum/__init__.py delete mode 100644 forum/apps.py delete mode 100644 forum/db.py delete mode 100644 forum/forms.py delete mode 100644 forum/permissions.py delete mode 100644 forum/sanitize.py delete mode 100644 forum/templates/forum/_style.html delete mode 100644 forum/templates/forum/create_topic.html delete mode 100644 forum/templates/forum/edit_post.html delete mode 100644 forum/templates/forum/forum.html delete mode 100644 forum/templates/forum/index.html delete mode 100644 forum/templates/forum/profile.html delete mode 100644 forum/templates/forum/search.html delete mode 100644 forum/templates/forum/topic.html delete mode 100644 forum/templatetags/__init__.py delete mode 100644 forum/templatetags/forum_extras.py delete mode 100644 forum/urls.py delete mode 100644 forum/views.py delete mode 100644 frontend/README.md delete mode 100644 frontend/package-lock.json delete mode 100644 frontend/package.json delete mode 100644 frontend/src/components/AccountDashboard.tsx delete mode 100644 frontend/src/components/ChangeEmailForm.tsx delete mode 100644 frontend/src/components/ChangePasswordForm.tsx delete mode 100644 frontend/src/components/CharacterPurchaseForm.tsx delete mode 100644 frontend/src/components/GoldForm.tsx delete mode 100644 frontend/src/components/GuildRenameForm.tsx delete mode 100644 frontend/src/components/HistoryTable.tsx delete mode 100644 frontend/src/components/LoginForm.tsx delete mode 100644 frontend/src/components/NewsList.tsx delete mode 100644 frontend/src/components/RecoverForm.tsx delete mode 100644 frontend/src/components/RegisterForm.tsx delete mode 100644 frontend/src/components/ResetPassword.tsx delete mode 100644 frontend/src/components/RestoreCharacterForm.tsx delete mode 100644 frontend/src/components/RestoreItemsForm.tsx delete mode 100644 frontend/src/components/SecurityTokenForm.tsx delete mode 100644 frontend/src/components/SelectAccount.tsx delete mode 100644 frontend/src/components/ServerStatus.tsx delete mode 100644 frontend/src/components/TransferForm.tsx delete mode 100644 frontend/src/components/VotePanel.tsx delete mode 100644 frontend/src/entries/change_email.tsx delete mode 100644 frontend/src/entries/change_password.tsx delete mode 100644 frontend/src/entries/character_purchase.tsx delete mode 100644 frontend/src/entries/gold_character.tsx delete mode 100644 frontend/src/entries/history_table.tsx delete mode 100644 frontend/src/entries/home.tsx delete mode 100644 frontend/src/entries/login.tsx delete mode 100644 frontend/src/entries/my_account.tsx delete mode 100644 frontend/src/entries/recover.tsx delete mode 100644 frontend/src/entries/register.tsx delete mode 100644 frontend/src/entries/rename_guild.tsx delete mode 100644 frontend/src/entries/reset_password.tsx delete mode 100644 frontend/src/entries/restore_character.tsx delete mode 100644 frontend/src/entries/restore_items.tsx delete mode 100644 frontend/src/entries/security_token.tsx delete mode 100644 frontend/src/entries/select_account.tsx delete mode 100644 frontend/src/entries/transfer_character.tsx delete mode 100644 frontend/src/entries/vote_points.tsx delete mode 100644 frontend/src/stripe.ts delete mode 100644 frontend/src/turnstile.ts delete mode 100644 frontend/src/types.ts delete mode 100644 frontend/tsconfig.json delete mode 100644 frontend/vite.config.ts delete mode 100644 home/__init__.py delete mode 100644 home/ac_soap.py delete mode 100644 home/admin.py delete mode 100644 home/api_urls.py delete mode 100644 home/apps.py delete mode 100644 home/bnet.py delete mode 100644 home/context_processors.py delete mode 100644 home/library_correo.py delete mode 100644 home/middleware.py delete mode 100644 home/migrations/0001_initial.py delete mode 100644 home/migrations/0002_accountactivation_remove_recruitreward_claimed_by_and_more.py delete mode 100644 home/migrations/0003_alter_accountactivation_salt.py delete mode 100644 home/migrations/0004_alter_accountactivation_salt_and_more.py delete mode 100644 home/migrations/0005_securitytoken.py delete mode 100644 home/migrations/0006_changefactionprice_changeraceprice_customizeprice_and_more.py delete mode 100644 home/migrations/0007_maintenancemode.py delete mode 100644 home/migrations/0008_category_remove_storecategory_parent_category_and_more.py delete mode 100644 home/migrations/0009_stripelog.py delete mode 100644 home/migrations/0010_loginattempt.py delete mode 100644 home/migrations/0011_pedido_alter_guildrenamesettings_options_and_more.py delete mode 100644 home/migrations/0012_alter_accountactivation_salt_and_more.py delete mode 100644 home/migrations/0013_stripelog_fulfilled.py delete mode 100644 home/migrations/0014_passwordreset.py delete mode 100644 home/migrations/__init__.py delete mode 100644 home/models/__init__.py delete mode 100644 home/models/accounts.py delete mode 100644 home/models/history.py delete mode 100644 home/models/pricing.py delete mode 100644 home/models/recruit.py delete mode 100644 home/models/site.py delete mode 100644 home/models/store.py delete mode 100644 home/models/voting.py delete mode 100644 home/pagos_stripe.py delete mode 100644 home/pagos_sumup.py delete mode 100644 home/templates/404.html delete mode 100644 home/templates/account/ban_history.html delete mode 100644 home/templates/account/battlepay.html delete mode 100644 home/templates/account/change_email.html delete mode 100644 home/templates/account/change_faction.html delete mode 100644 home/templates/account/change_faction_cancel.html delete mode 100644 home/templates/account/change_faction_success.html delete mode 100644 home/templates/account/change_password.html delete mode 100644 home/templates/account/change_race.html delete mode 100644 home/templates/account/change_race_cancel.html delete mode 100644 home/templates/account/change_race_success.html delete mode 100644 home/templates/account/confirm_new_email_success.html delete mode 100644 home/templates/account/confirm_old_email_success.html delete mode 100644 home/templates/account/customize_character.html delete mode 100644 home/templates/account/customize_success.html delete mode 100644 home/templates/account/d_points.html delete mode 100644 home/templates/account/expired_link.html delete mode 100644 home/templates/account/gold_cancel.html delete mode 100644 home/templates/account/gold_character.html delete mode 100644 home/templates/account/gold_success.html delete mode 100644 home/templates/account/guild_rename_cancel.html delete mode 100644 home/templates/account/guild_rename_success.html delete mode 100644 home/templates/account/level_up.html delete mode 100644 home/templates/account/level_up_cancel.html delete mode 100644 home/templates/account/level_up_success.html delete mode 100644 home/templates/account/pago_sumup.html delete mode 100644 home/templates/account/points_history.html delete mode 100644 home/templates/account/promo_code.html delete mode 100644 home/templates/account/quest_character.html delete mode 100644 home/templates/account/rename_cancel.html delete mode 100644 home/templates/account/rename_character.html delete mode 100644 home/templates/account/rename_guild.html delete mode 100644 home/templates/account/rename_success.html delete mode 100644 home/templates/account/restore_character.html delete mode 100644 home/templates/account/restore_items.html delete mode 100644 home/templates/account/revive_character.html delete mode 100644 home/templates/account/security_history.html delete mode 100644 home/templates/account/security_token.html delete mode 100644 home/templates/account/send_gift.html delete mode 100644 home/templates/account/trade_points.html delete mode 100644 home/templates/account/trans_history.html delete mode 100644 home/templates/account/transfer_cancel.html delete mode 100644 home/templates/account/transfer_character.html delete mode 100644 home/templates/account/transfer_d_points.html delete mode 100644 home/templates/account/transfer_success.html delete mode 100644 home/templates/account/unstuck_character.html delete mode 100644 home/templates/account/vote_points.html delete mode 100644 home/templates/admin/server_selection.html delete mode 100644 home/templates/auth/activation_invalid.html delete mode 100644 home/templates/auth/activation_success.html delete mode 100644 home/templates/auth/login.html delete mode 100644 home/templates/auth/recover_account.html delete mode 100644 home/templates/auth/register.html delete mode 100644 home/templates/auth/reset_password.html delete mode 100644 home/templates/auth/select_account.html delete mode 100644 home/templates/base/base.html delete mode 100644 home/templates/community/content_creators.html delete mode 100644 home/templates/community/help.html delete mode 100644 home/templates/contact/contact_us.html delete mode 100644 home/templates/download/download_client.html delete mode 100644 home/templates/emails/account_names.html delete mode 100644 home/templates/emails/activation.html delete mode 100644 home/templates/emails/confirm_new_email.html delete mode 100644 home/templates/emails/confirm_old_email.html delete mode 100644 home/templates/emails/old_email_notification.html delete mode 100644 home/templates/emails/password_reset.html delete mode 100644 home/templates/emails/security_token.html delete mode 100644 home/templates/home/home.html delete mode 100644 home/templates/legal/cookies.html delete mode 100644 home/templates/legal/legal_notice.html delete mode 100644 home/templates/legal/privacy_policy.html delete mode 100644 home/templates/legal/refund_policy.html delete mode 100644 home/templates/legal/terms_and_conditions.html delete mode 100644 home/templates/maintenance/maintenance.html delete mode 100644 home/templates/my-account/my-account.html delete mode 100644 home/templates/partials/activation_invalid.html delete mode 100644 home/templates/partials/activation_success.html delete mode 100644 home/templates/partials/ban_history.html delete mode 100644 home/templates/partials/change_email.html delete mode 100644 home/templates/partials/change_faction.html delete mode 100644 home/templates/partials/change_faction_cancel.html delete mode 100644 home/templates/partials/change_faction_success.html delete mode 100644 home/templates/partials/change_password.html delete mode 100644 home/templates/partials/change_race.html delete mode 100644 home/templates/partials/change_race_cancel.html delete mode 100644 home/templates/partials/change_race_success.html delete mode 100644 home/templates/partials/confirm_new_email_success.html delete mode 100644 home/templates/partials/confirm_old_email_success.html delete mode 100644 home/templates/partials/contact_us.html delete mode 100644 home/templates/partials/cookies.html delete mode 100644 home/templates/partials/customize_character.html delete mode 100644 home/templates/partials/customize_success.html delete mode 100644 home/templates/partials/d_points.html delete mode 100644 home/templates/partials/descargas.html delete mode 100644 home/templates/partials/expired_link.html delete mode 100644 home/templates/partials/final.html delete mode 100644 home/templates/partials/footer.html delete mode 100644 home/templates/partials/gold_cancel.html delete mode 100644 home/templates/partials/gold_character.html delete mode 100644 home/templates/partials/gold_success.html delete mode 100644 home/templates/partials/guild_rename_cancel.html delete mode 100644 home/templates/partials/guild_rename_success.html delete mode 100644 home/templates/partials/head.html delete mode 100644 home/templates/partials/header.html delete mode 100644 home/templates/partials/help.html delete mode 100644 home/templates/partials/legal_notice.html delete mode 100644 home/templates/partials/level_up.html delete mode 100644 home/templates/partials/level_up_cancel.html delete mode 100644 home/templates/partials/level_up_success.html delete mode 100644 home/templates/partials/login.html delete mode 100644 home/templates/partials/maintenance.html delete mode 100644 home/templates/partials/my-account.html delete mode 100644 home/templates/partials/not_found.html delete mode 100644 home/templates/partials/noticias.html delete mode 100644 home/templates/partials/novawow-realm.html delete mode 100644 home/templates/partials/novawow_players.html delete mode 100644 home/templates/partials/pago_sumup.html delete mode 100644 home/templates/partials/points_history.html delete mode 100644 home/templates/partials/privacy_policy.html delete mode 100644 home/templates/partials/promo_code.html delete mode 100644 home/templates/partials/quest_character.html delete mode 100644 home/templates/partials/recover_account.html delete mode 100644 home/templates/partials/recruit_a_friend.html delete mode 100644 home/templates/partials/refund_policy.html delete mode 100644 home/templates/partials/register.html delete mode 100644 home/templates/partials/rename_cancel.html delete mode 100644 home/templates/partials/rename_character.html delete mode 100644 home/templates/partials/rename_guild.html delete mode 100644 home/templates/partials/rename_success.html delete mode 100644 home/templates/partials/restore_character.html delete mode 100644 home/templates/partials/restore_items.html delete mode 100644 home/templates/partials/revive_character.html delete mode 100644 home/templates/partials/security_history.html delete mode 100644 home/templates/partials/security_token.html delete mode 100644 home/templates/partials/send_gift.html delete mode 100644 home/templates/partials/social.html delete mode 100644 home/templates/partials/start_time.html delete mode 100644 home/templates/partials/store/store_novawow.html delete mode 100644 home/templates/partials/store/store_novawow_cancel.html delete mode 100644 home/templates/partials/store/store_novawow_success.html delete mode 100644 home/templates/partials/terms_and_conditions.html delete mode 100644 home/templates/partials/trade_points.html delete mode 100644 home/templates/partials/trans_history.html delete mode 100644 home/templates/partials/transfer_cancel.html delete mode 100644 home/templates/partials/transfer_character.html delete mode 100644 home/templates/partials/transfer_d_points.html delete mode 100644 home/templates/partials/transfer_success.html delete mode 100644 home/templates/partials/unstuck_character.html delete mode 100644 home/templates/partials/video.html delete mode 100644 home/templates/partials/videos.html delete mode 100644 home/templates/partials/vote_points.html delete mode 100644 home/templates/realms/novawow_players.html delete mode 100644 home/templates/realms/novawow_realm.html delete mode 100644 home/templates/recruit/recruit_a_friend.html delete mode 100644 home/templates/server_status/server_status.html delete mode 100644 home/templates/store/store_novawow.html delete mode 100644 home/templates/store/store_novawow_cancel.html delete mode 100644 home/templates/store/store_novawow_success.html delete mode 100644 home/tests.py delete mode 100644 home/urls.py delete mode 100644 home/views/__init__.py delete mode 100644 home/views/_base.py delete mode 100644 home/views/account.py delete mode 100644 home/views/api.py delete mode 100644 home/views/auth.py delete mode 100644 home/views/characters.py delete mode 100644 home/views/guild.py delete mode 100644 home/views/history.py delete mode 100644 home/views/pages.py delete mode 100644 home/views/payments.py delete mode 100644 home/views/players.py delete mode 100644 home/views/points.py delete mode 100644 home/views/recruit.py delete mode 100644 home/views/store.py delete mode 100644 home/zone_definitions.py delete mode 100644 manage.py delete mode 100644 novawow/__init__.py delete mode 100644 novawow/asgi.py delete mode 100644 novawow/settings.py delete mode 100644 novawow/urls.py delete mode 100644 novawow/views.py delete mode 100644 novawow/wsgi.py delete mode 100644 requirements.txt delete mode 100644 static/admin/css/autocomplete.css delete mode 100644 static/admin/css/base.css delete mode 100644 static/admin/css/changelists.css delete mode 100644 static/admin/css/dark_mode.css delete mode 100644 static/admin/css/dashboard.css delete mode 100644 static/admin/css/forms.css delete mode 100644 static/admin/css/login.css delete mode 100644 static/admin/css/nav_sidebar.css delete mode 100644 static/admin/css/responsive.css delete mode 100644 static/admin/css/responsive_rtl.css delete mode 100644 static/admin/css/rtl.css delete mode 100644 static/admin/css/unusable_password_field.css delete mode 100644 static/admin/css/vendor/select2/LICENSE-SELECT2.md delete mode 100644 static/admin/css/vendor/select2/select2.css delete mode 100644 static/admin/css/vendor/select2/select2.min.css delete mode 100644 static/admin/css/widgets.css delete mode 100644 static/admin/img/LICENSE delete mode 100644 static/admin/img/README.txt delete mode 100644 static/admin/img/calendar-icons.svg delete mode 100644 static/admin/img/gis/move_vertex_off.svg delete mode 100644 static/admin/img/gis/move_vertex_on.svg delete mode 100644 static/admin/img/icon-addlink.svg delete mode 100644 static/admin/img/icon-alert.svg delete mode 100644 static/admin/img/icon-calendar.svg delete mode 100644 static/admin/img/icon-changelink.svg delete mode 100644 static/admin/img/icon-clock.svg delete mode 100644 static/admin/img/icon-deletelink.svg delete mode 100644 static/admin/img/icon-hidelink.svg delete mode 100644 static/admin/img/icon-no.svg delete mode 100644 static/admin/img/icon-unknown-alt.svg delete mode 100644 static/admin/img/icon-unknown.svg delete mode 100644 static/admin/img/icon-viewlink.svg delete mode 100644 static/admin/img/icon-yes.svg delete mode 100644 static/admin/img/inline-delete.svg delete mode 100644 static/admin/img/search.svg delete mode 100644 static/admin/img/selector-icons.svg delete mode 100644 static/admin/img/sorting-icons.svg delete mode 100644 static/admin/img/tooltag-add.svg delete mode 100644 static/admin/img/tooltag-arrowright.svg delete mode 100644 static/admin/js/SelectBox.js delete mode 100644 static/admin/js/SelectFilter2.js delete mode 100644 static/admin/js/actions.js delete mode 100644 static/admin/js/admin/DateTimeShortcuts.js delete mode 100644 static/admin/js/admin/RelatedObjectLookups.js delete mode 100644 static/admin/js/autocomplete.js delete mode 100644 static/admin/js/calendar.js delete mode 100644 static/admin/js/cancel.js delete mode 100644 static/admin/js/change_form.js delete mode 100644 static/admin/js/collapse.js delete mode 100644 static/admin/js/core.js delete mode 100644 static/admin/js/filters.js delete mode 100644 static/admin/js/inlines.js delete mode 100644 static/admin/js/jquery.init.js delete mode 100644 static/admin/js/nav_sidebar.js delete mode 100644 static/admin/js/popup_response.js delete mode 100644 static/admin/js/prepopulate.js delete mode 100644 static/admin/js/prepopulate_init.js delete mode 100644 static/admin/js/theme.js delete mode 100644 static/admin/js/unusable_password_field.js delete mode 100644 static/admin/js/urlify.js delete mode 100644 static/admin/js/vendor/jquery/LICENSE.txt delete mode 100644 static/admin/js/vendor/jquery/jquery.js delete mode 100644 static/admin/js/vendor/jquery/jquery.min.js delete mode 100644 static/admin/js/vendor/select2/LICENSE.md delete mode 100644 static/admin/js/vendor/select2/i18n/af.js delete mode 100644 static/admin/js/vendor/select2/i18n/ar.js delete mode 100644 static/admin/js/vendor/select2/i18n/az.js delete mode 100644 static/admin/js/vendor/select2/i18n/bg.js delete mode 100644 static/admin/js/vendor/select2/i18n/bn.js delete mode 100644 static/admin/js/vendor/select2/i18n/bs.js delete mode 100644 static/admin/js/vendor/select2/i18n/ca.js delete mode 100644 static/admin/js/vendor/select2/i18n/cs.js delete mode 100644 static/admin/js/vendor/select2/i18n/da.js delete mode 100644 static/admin/js/vendor/select2/i18n/de.js delete mode 100644 static/admin/js/vendor/select2/i18n/dsb.js delete mode 100644 static/admin/js/vendor/select2/i18n/el.js delete mode 100644 static/admin/js/vendor/select2/i18n/en.js delete mode 100644 static/admin/js/vendor/select2/i18n/es.js delete mode 100644 static/admin/js/vendor/select2/i18n/et.js delete mode 100644 static/admin/js/vendor/select2/i18n/eu.js delete mode 100644 static/admin/js/vendor/select2/i18n/fa.js delete mode 100644 static/admin/js/vendor/select2/i18n/fi.js delete mode 100644 static/admin/js/vendor/select2/i18n/fr.js delete mode 100644 static/admin/js/vendor/select2/i18n/gl.js delete mode 100644 static/admin/js/vendor/select2/i18n/he.js delete mode 100644 static/admin/js/vendor/select2/i18n/hi.js delete mode 100644 static/admin/js/vendor/select2/i18n/hr.js delete mode 100644 static/admin/js/vendor/select2/i18n/hsb.js delete mode 100644 static/admin/js/vendor/select2/i18n/hu.js delete mode 100644 static/admin/js/vendor/select2/i18n/hy.js delete mode 100644 static/admin/js/vendor/select2/i18n/id.js delete mode 100644 static/admin/js/vendor/select2/i18n/is.js delete mode 100644 static/admin/js/vendor/select2/i18n/it.js delete mode 100644 static/admin/js/vendor/select2/i18n/ja.js delete mode 100644 static/admin/js/vendor/select2/i18n/ka.js delete mode 100644 static/admin/js/vendor/select2/i18n/km.js delete mode 100644 static/admin/js/vendor/select2/i18n/ko.js delete mode 100644 static/admin/js/vendor/select2/i18n/lt.js delete mode 100644 static/admin/js/vendor/select2/i18n/lv.js delete mode 100644 static/admin/js/vendor/select2/i18n/mk.js delete mode 100644 static/admin/js/vendor/select2/i18n/ms.js delete mode 100644 static/admin/js/vendor/select2/i18n/nb.js delete mode 100644 static/admin/js/vendor/select2/i18n/ne.js delete mode 100644 static/admin/js/vendor/select2/i18n/nl.js delete mode 100644 static/admin/js/vendor/select2/i18n/pl.js delete mode 100644 static/admin/js/vendor/select2/i18n/ps.js delete mode 100644 static/admin/js/vendor/select2/i18n/pt-BR.js delete mode 100644 static/admin/js/vendor/select2/i18n/pt.js delete mode 100644 static/admin/js/vendor/select2/i18n/ro.js delete mode 100644 static/admin/js/vendor/select2/i18n/ru.js delete mode 100644 static/admin/js/vendor/select2/i18n/sk.js delete mode 100644 static/admin/js/vendor/select2/i18n/sl.js delete mode 100644 static/admin/js/vendor/select2/i18n/sq.js delete mode 100644 static/admin/js/vendor/select2/i18n/sr-Cyrl.js delete mode 100644 static/admin/js/vendor/select2/i18n/sr.js delete mode 100644 static/admin/js/vendor/select2/i18n/sv.js delete mode 100644 static/admin/js/vendor/select2/i18n/th.js delete mode 100644 static/admin/js/vendor/select2/i18n/tk.js delete mode 100644 static/admin/js/vendor/select2/i18n/tr.js delete mode 100644 static/admin/js/vendor/select2/i18n/uk.js delete mode 100644 static/admin/js/vendor/select2/i18n/vi.js delete mode 100644 static/admin/js/vendor/select2/i18n/zh-CN.js delete mode 100644 static/admin/js/vendor/select2/i18n/zh-TW.js delete mode 100644 static/admin/js/vendor/select2/select2.full.js delete mode 100644 static/admin/js/vendor/select2/select2.full.min.js delete mode 100644 static/admin/js/vendor/xregexp/LICENSE.txt delete mode 100644 static/admin/js/vendor/xregexp/xregexp.js delete mode 100644 static/admin/js/vendor/xregexp/xregexp.min.js delete mode 100644 static/android-chrome-192x192.png delete mode 100644 static/apple-touch-icon.png delete mode 100644 static/browserconfig.xml delete mode 100644 static/css/basic.css delete mode 100644 static/css/global.css delete mode 100644 static/css/home.css delete mode 100644 static/css/locale_eses.css delete mode 100644 static/css/novawow.css delete mode 100644 static/django_ckeditor_5/app.js delete mode 100644 static/django_ckeditor_5/dist/bundle.js delete mode 100644 static/django_ckeditor_5/dist/bundle.js.LICENSE.txt delete mode 100644 static/django_ckeditor_5/dist/bundle.js.map delete mode 100644 static/django_ckeditor_5/dist/styles.css delete mode 100644 static/django_ckeditor_5/dist/styles.css.map delete mode 100644 static/django_ckeditor_5/dist/translations/af.js delete mode 100644 static/django_ckeditor_5/dist/translations/ar.js delete mode 100644 static/django_ckeditor_5/dist/translations/ast.js delete mode 100644 static/django_ckeditor_5/dist/translations/az.js delete mode 100644 static/django_ckeditor_5/dist/translations/bg.js delete mode 100644 static/django_ckeditor_5/dist/translations/bn.js delete mode 100644 static/django_ckeditor_5/dist/translations/bs.js delete mode 100644 static/django_ckeditor_5/dist/translations/ca.js delete mode 100644 static/django_ckeditor_5/dist/translations/cs.js delete mode 100644 static/django_ckeditor_5/dist/translations/da.js delete mode 100644 static/django_ckeditor_5/dist/translations/de-ch.js delete mode 100644 static/django_ckeditor_5/dist/translations/de.js delete mode 100644 static/django_ckeditor_5/dist/translations/el.js delete mode 100644 static/django_ckeditor_5/dist/translations/en-au.js delete mode 100644 static/django_ckeditor_5/dist/translations/en-gb.js delete mode 100644 static/django_ckeditor_5/dist/translations/eo.js delete mode 100644 static/django_ckeditor_5/dist/translations/es-co.js delete mode 100644 static/django_ckeditor_5/dist/translations/es.js delete mode 100644 static/django_ckeditor_5/dist/translations/et.js delete mode 100644 static/django_ckeditor_5/dist/translations/eu.js delete mode 100644 static/django_ckeditor_5/dist/translations/fa.js delete mode 100644 static/django_ckeditor_5/dist/translations/fi.js delete mode 100644 static/django_ckeditor_5/dist/translations/fr.js delete mode 100644 static/django_ckeditor_5/dist/translations/gl.js delete mode 100644 static/django_ckeditor_5/dist/translations/gu.js delete mode 100644 static/django_ckeditor_5/dist/translations/he.js delete mode 100644 static/django_ckeditor_5/dist/translations/hi.js delete mode 100644 static/django_ckeditor_5/dist/translations/hr.js delete mode 100644 static/django_ckeditor_5/dist/translations/hu.js delete mode 100644 static/django_ckeditor_5/dist/translations/hy.js delete mode 100644 static/django_ckeditor_5/dist/translations/id.js delete mode 100644 static/django_ckeditor_5/dist/translations/it.js delete mode 100644 static/django_ckeditor_5/dist/translations/ja.js delete mode 100644 static/django_ckeditor_5/dist/translations/jv.js delete mode 100644 static/django_ckeditor_5/dist/translations/kk.js delete mode 100644 static/django_ckeditor_5/dist/translations/km.js delete mode 100644 static/django_ckeditor_5/dist/translations/kn.js delete mode 100644 static/django_ckeditor_5/dist/translations/ko.js delete mode 100644 static/django_ckeditor_5/dist/translations/ku.js delete mode 100644 static/django_ckeditor_5/dist/translations/lt.js delete mode 100644 static/django_ckeditor_5/dist/translations/lv.js delete mode 100644 static/django_ckeditor_5/dist/translations/ms.js delete mode 100644 static/django_ckeditor_5/dist/translations/nb.js delete mode 100644 static/django_ckeditor_5/dist/translations/ne.js delete mode 100644 static/django_ckeditor_5/dist/translations/nl.js delete mode 100644 static/django_ckeditor_5/dist/translations/no.js delete mode 100644 static/django_ckeditor_5/dist/translations/oc.js delete mode 100644 static/django_ckeditor_5/dist/translations/pl.js delete mode 100644 static/django_ckeditor_5/dist/translations/pt-br.js delete mode 100644 static/django_ckeditor_5/dist/translations/pt.js delete mode 100644 static/django_ckeditor_5/dist/translations/ro.js delete mode 100644 static/django_ckeditor_5/dist/translations/ru.js delete mode 100644 static/django_ckeditor_5/dist/translations/si.js delete mode 100644 static/django_ckeditor_5/dist/translations/sk.js delete mode 100644 static/django_ckeditor_5/dist/translations/sl.js delete mode 100644 static/django_ckeditor_5/dist/translations/sq.js delete mode 100644 static/django_ckeditor_5/dist/translations/sr-latn.js delete mode 100644 static/django_ckeditor_5/dist/translations/sr.js delete mode 100644 static/django_ckeditor_5/dist/translations/sv.js delete mode 100644 static/django_ckeditor_5/dist/translations/th.js delete mode 100644 static/django_ckeditor_5/dist/translations/ti.js delete mode 100644 static/django_ckeditor_5/dist/translations/tk.js delete mode 100644 static/django_ckeditor_5/dist/translations/tr.js delete mode 100644 static/django_ckeditor_5/dist/translations/tt.js delete mode 100644 static/django_ckeditor_5/dist/translations/ug.js delete mode 100644 static/django_ckeditor_5/dist/translations/uk.js delete mode 100644 static/django_ckeditor_5/dist/translations/ur.js delete mode 100644 static/django_ckeditor_5/dist/translations/uz.js delete mode 100644 static/django_ckeditor_5/dist/translations/vi.js delete mode 100644 static/django_ckeditor_5/dist/translations/zh-cn.js delete mode 100644 static/django_ckeditor_5/dist/translations/zh.js delete mode 100644 static/django_ckeditor_5/src/ckeditor.js delete mode 100644 static/django_ckeditor_5/src/override-django.css delete mode 100644 static/django_extensions/css/jquery.autocomplete.css delete mode 100644 static/django_extensions/img/indicator.gif delete mode 100644 static/django_extensions/js/jquery.ajaxQueue.js delete mode 100644 static/django_extensions/js/jquery.autocomplete.js delete mode 100644 static/django_extensions/js/jquery.bgiframe.js delete mode 100644 static/download/searchplugins/novawow.xml delete mode 100644 static/favicon-16x16.png delete mode 100644 static/favicon-194x194.png delete mode 100644 static/favicon-32x32.png delete mode 100644 static/favicon.ico delete mode 100644 static/images/Menu/check.gif delete mode 100644 static/images/eses/modelviewer-picshures.gif delete mode 100644 static/images/icons/facebook.webp delete mode 100644 static/images/icons/forum.webp delete mode 100644 static/images/icons/twitter.webp delete mode 100644 static/images/icons/web.webp delete mode 100644 static/images/icons/wow-death-knight.webp delete mode 100644 static/images/icons/wow-druid.webp delete mode 100644 static/images/icons/wow-hunter.webp delete mode 100644 static/images/icons/wow-mage.png delete mode 100644 static/images/icons/wow-paladin.webp delete mode 100644 static/images/icons/wow-priest.webp delete mode 100644 static/images/icons/wow-rogue.webp delete mode 100644 static/images/icons/wow-shaman.webp delete mode 100644 static/images/icons/wow-warlock.png delete mode 100644 static/images/icons/wow-warrior.webp delete mode 100644 static/images/logos/favicon.ico delete mode 100644 static/images/logos/home-background.jpg delete mode 100644 static/images/logos/home.webp delete mode 100644 static/images/wow/icons/tiny/ability_druid_swipe.gif delete mode 100644 static/images/wow/icons/tiny/ability_hunter_combatexperience.gif delete mode 100644 static/images/wow/icons/tiny/ability_hunter_pet_bear.gif delete mode 100644 static/images/wow/icons/tiny/class_deathknight.gif delete mode 100644 static/images/wow/icons/tiny/class_druid.gif delete mode 100644 static/images/wow/icons/tiny/class_hunter.gif delete mode 100644 static/images/wow/icons/tiny/class_mage.gif delete mode 100644 static/images/wow/icons/tiny/class_paladin.gif delete mode 100644 static/images/wow/icons/tiny/class_priest.gif delete mode 100644 static/images/wow/icons/tiny/class_rogue.gif delete mode 100644 static/images/wow/icons/tiny/class_shaman.gif delete mode 100644 static/images/wow/icons/tiny/class_warlock.gif delete mode 100644 static/images/wow/icons/tiny/class_warrior.gif delete mode 100644 static/images/wow/icons/tiny/inv_inscription_tradeskill01.gif delete mode 100644 static/images/wow/icons/tiny/inv_misc_armorkit_17.gif delete mode 100644 static/images/wow/icons/tiny/inv_misc_food_15.gif delete mode 100644 static/images/wow/icons/tiny/inv_misc_gem_01.gif delete mode 100644 static/images/wow/icons/tiny/inv_misc_pelt_wolf_01.gif delete mode 100644 static/images/wow/icons/tiny/race_bloodelf_female.gif delete mode 100644 static/images/wow/icons/tiny/race_draenei_female.gif delete mode 100644 static/images/wow/icons/tiny/race_dwarf_female.gif delete mode 100644 static/images/wow/icons/tiny/race_gnome_female.gif delete mode 100644 static/images/wow/icons/tiny/race_human_female.gif delete mode 100644 static/images/wow/icons/tiny/race_nightelf_female.gif delete mode 100644 static/images/wow/icons/tiny/race_orc_female.gif delete mode 100644 static/images/wow/icons/tiny/race_scourge_female.gif delete mode 100644 static/images/wow/icons/tiny/race_tauren_female.gif delete mode 100644 static/images/wow/icons/tiny/race_troll_female.gif delete mode 100644 static/images/wow/icons/tiny/spell_holy_sealofsacrifice.gif delete mode 100644 static/images/wow/icons/tiny/spell_nature_naturetouchgrow.gif delete mode 100644 static/images/wow/icons/tiny/spell_nature_swiftness.gif delete mode 100644 static/images/wow/icons/tiny/trade_alchemy.gif delete mode 100644 static/images/wow/icons/tiny/trade_blacksmithing.gif delete mode 100644 static/images/wow/icons/tiny/trade_engineering.gif delete mode 100644 static/images/wow/icons/tiny/trade_engraving.gif delete mode 100644 static/images/wow/icons/tiny/trade_fishing.gif delete mode 100644 static/images/wow/icons/tiny/trade_mining.gif delete mode 100644 static/images/wow/icons/tiny/trade_tailoring.gif delete mode 100644 static/js/Markup.js delete mode 100644 static/js/TalentCalc.js delete mode 100644 static/js/basic.js delete mode 100644 static/js/global.js delete mode 100644 static/js/home.js delete mode 100644 static/js/locale.js delete mode 100644 static/js/locale_eses.js delete mode 100644 static/js/swfobject.js delete mode 100644 static/js/talent.js delete mode 100644 static/mstile-144x144.png delete mode 100644 static/nw-themes/nw-ryu/nw-css/novawow-style.css delete mode 100644 static/nw-themes/nw-ryu/nw-font/FuturaEF-Book.woff delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-avatars/default-avatar.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-classes/death-knight-chest.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-classes/death-knight-medium.jpg delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-classes/druid-chest.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-classes/druid-medium.jpg delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-classes/hunter-chest.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-classes/hunter-medium.jpg delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-classes/mage-chest.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-classes/mage-medium.jpg delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-classes/paladin-chest.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-classes/paladin-medium.jpg delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-classes/priest-chest.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-classes/priest-medium.jpg delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-classes/rogue-chest.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-classes/rogue-medium.jpg delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-classes/shaman-chest.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-classes/shaman-medium.jpg delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-classes/warlock-chest.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-classes/warlock-medium.jpg delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-classes/warrior-chest.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-classes/warrior-medium.jpg delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-classes/wow-death-knight.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-classes/wow-druid.png delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-classes/wow-hunter.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-classes/wow-mage.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-classes/wow-paladin.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-classes/wow-priest.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-classes/wow-rogue.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-classes/wow-shaman.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-classes/wow-warlock.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-classes/wow-warrior.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-country-flags/ar.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-country-flags/bo.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-country-flags/cl.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-country-flags/co.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-country-flags/cr.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-country-flags/ec.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-country-flags/gt.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-country-flags/mx.png delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-country-flags/pa.png delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-country-flags/pe.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-country-flags/py.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-country-flags/uy.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-general/account-settings.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-general/novawow-maintenance.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-general/nw-divider-mini.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-general/nw-divider.png delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-icons/account-icons.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-icons/achievement-level-80.jpg delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-icons/alliance-icon.jpg delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-icons/alliance-no-border.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-icons/death-knight-medium.jpg delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-icons/druid-medium.jpg delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-icons/horde-icon.jpg delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-icons/horde-no-border.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-icons/hunter-medium.jpg delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-icons/mage-medium.jpg delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-icons/money-copper.gif delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-icons/money-gold.gif delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-icons/money-silver.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-icons/paladin-medium.jpg delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-icons/priest-medium.jpg delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-icons/refresh.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-icons/rogue-medium.jpg delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-icons/shaman-medium.jpg delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-icons/warlock-medium.jpg delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-icons/warrior-medium.jpg delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-logos/bancolombia-logo.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-logos/discord-logo.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-logos/dlocalgo-logo.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-logos/facebook.png delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-logos/nequi-logo.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-logos/novawow-main-logo-transparent.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-logos/nw-logo.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-logos/nw-long-logo.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-logos/oficina-peru.png delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-logos/paypal-logo.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-logos/paypal-p-logo.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-logos/plin-peru.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-logos/skrill-logo-small.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-logos/skrill-logo.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-logos/twitch.png delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-logos/whatsapp-logo.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-logos/yape-peru.png delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-logos/youtube.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-mails/facebook-logo-gray.png delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-mails/instagram-logo-gray.png delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-mails/nw-mail-body.jpg delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-mails/nw-mail-footer.jpg delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-mails/nw-mail-logo.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-mails/twitter-logo-gray.png delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-mails/youtube-logo-gray.png delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-races/big-blood-elf-female.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-races/big-blood-elf-male.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-races/big-draenei-female.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-races/big-draenei-male.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-races/big-dwarf-female.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-races/big-dwarf-male.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-races/big-gnome-female.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-races/big-gnome-male.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-races/big-human-female.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-races/big-human-male.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-races/big-night-elf-female.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-races/big-night-elf-male.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-races/big-orc-female.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-races/big-orc-male.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-races/big-tauren-female.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-races/big-tauren-male.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-races/big-troll-female.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-races/big-troll-male.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-races/big-undead-female.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-races/big-undead-male.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-ranks/10_Proficient.svg delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-ranks/11_Experienced.svg delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-ranks/12_Mentor.svg delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-ranks/13_Veteran.svg delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-ranks/14_GrandMaster.svg delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-ranks/1_Newbie.svg delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-ranks/2_Rookie.svg delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-ranks/3_Apprentice.svg delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-ranks/4_Explorer.svg delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-ranks/5_Contributor.svg delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-ranks/6_Enthusiast.svg delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-ranks/7_Collaborator.svg delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-ranks/8_Regular.svg delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-ranks/9_RisingStar.svg delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-vote-sites/arenatop100.webp delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-vote-sites/gtop100.jpg delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-vote-sites/top100arena.jpg delete mode 100644 static/nw-themes/nw-ryu/nw-images/nw-vote-sites/topg.webp delete mode 100644 static/nw-themes/nw-ryu/nw-js-handlers/create_check.js delete mode 100644 static/nw-themes/nw-ryu/nw-js-handlers/my_account_response.js delete mode 100644 static/nw-themes/nw-ryu/nw-js-handlers/promo_code_response.js delete mode 100644 static/nw-themes/nw-ryu/nw-js-handlers/recruit-response.js delete mode 100644 static/nw-themes/nw-ryu/nw-js-handlers/rename_response.js delete mode 100644 static/nw-themes/nw-ryu/nw-js-handlers/store_novawow_response.js delete mode 100644 static/nw-themes/nw-ryu/nw-js-handlers/store_response.js delete mode 100644 static/nw-themes/nw-ryu/nw-js-handlers/trade_points_response.js delete mode 100644 static/nw-themes/nw-ryu/nw-js/nw-scripts.js delete mode 100644 static/nw-themes/nw-ryu/nw-js/power.js delete mode 100644 static/nw-themes/nw-ryu/nw-videos/nw-logo.mp4 delete mode 100644 static/safari-pinned-tab.svg delete mode 100644 static/site.webmanifest delete mode 100644 static/widgets/power.js delete mode 100644 wotlk_db/__init__.py delete mode 100644 wotlk_db/__pycache__/__init__.cpython-313.pyc delete mode 100644 wotlk_db/__pycache__/admin.cpython-313.pyc delete mode 100644 wotlk_db/__pycache__/apps.cpython-313.pyc delete mode 100644 wotlk_db/__pycache__/models.cpython-313.pyc delete mode 100644 wotlk_db/__pycache__/urls.cpython-313.pyc delete mode 100644 wotlk_db/__pycache__/views.cpython-313.pyc delete mode 100644 wotlk_db/admin.py delete mode 100644 wotlk_db/apps.py delete mode 100644 wotlk_db/migrations/__init__.py delete mode 100644 wotlk_db/migrations/__pycache__/__init__.cpython-313.pyc delete mode 100644 wotlk_db/models.py delete mode 100644 wotlk_db/templates/home/inicio.html delete mode 100644 wotlk_db/tests.py delete mode 100644 wotlk_db/urls.py delete mode 100644 wotlk_db/views.py diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 05bdadd..0000000 --- a/Dockerfile +++ /dev/null @@ -1,32 +0,0 @@ -# Nova WoW - imagen del portal Django -FROM python:3.14-slim - -ENV PYTHONDONTWRITEBYTECODE=1 \ - PYTHONUNBUFFERED=1 - -# Dependencias de sistema para mysqlclient (compilación + runtime) y Pillow -RUN apt-get update && apt-get install -y --no-install-recommends \ - build-essential \ - pkg-config \ - default-libmysqlclient-dev \ - libgmp-dev libmpfr-dev libmpc-dev \ - libjpeg-dev zlib1g-dev \ - netcat-openbsd \ - && rm -rf /var/lib/apt/lists/* - -WORKDIR /app - -# Instalar dependencias Python primero (mejor cache) -COPY requirements.txt . -RUN pip install --upgrade pip && pip install --no-cache-dir -r requirements.txt - -# Copiar el código -COPY . . - -# Entrypoint -RUN chmod +x /app/docker/entrypoint.sh - -EXPOSE 8000 - -ENTRYPOINT ["/app/docker/entrypoint.sh"] -CMD ["gunicorn", "novawow.wsgi:application", "--bind", "0.0.0.0:8000", "--workers", "4"] diff --git a/README.md b/README.md index 8165cd7..3537579 100644 --- a/README.md +++ b/README.md @@ -1,115 +1,82 @@ # Nova WoW -Portal web (Django) para un servidor de World of Warcraft basado en AzerothCore. +Portal web para un servidor de World of Warcraft 3.4.3 basado en AzerothCore. -- **Python:** 3.14 -- **Framework:** Django 6.0 -- **Base de datos:** MySQL (4 bases: la del portal `django_wow` + las de AzerothCore `acore_auth`, `acore_characters`, `acore_world`) +- **Framework:** Next.js 16 (App Router) + Tailwind, en `web-next/` +- **Idiomas:** ES/EN con next-intl +- **Bases de datos (MySQL):** `django_wow` (la del portal), `acore_auth`, `acore_characters`, + `acore_world` y `acore_web` (el foro) + +> La base del portal se sigue llamando `django_wow` y sus tablas `home_*` por motivos históricos: el +> portal era una app Django, jubilada el 2026-07-15. El código se borró (está en el historial de git), +> pero **las tablas son las mismas y siguen en uso**: Next las consulta con SQL directo, sin ORM. --- -## 1. Requisitos - -- Python 3.14 y `venv` -- MySQL/MariaDB accesible -- Librerías de sistema para compilar algunas dependencias: - -```bash -sudo apt install python3.14-venv python3.14-dev build-essential pkg-config \ - default-libmysqlclient-dev libgmp-dev libmpfr-dev libmpc-dev libjpeg-dev zlib1g-dev -``` - ---- - -## 2. Puesta en marcha (desarrollo) +## 1. Puesta en marcha (desarrollo) ```bash git clone https://git.nightspire.gg/Inna/NovaWoW.git -cd NovaWoW - -# Entorno virtual -python3 -m venv .venv -source .venv/bin/activate -pip install --upgrade pip -pip install -r requirements.txt - -# Variables de entorno -cp .env.example .env -# edita .env con tus credenciales reales (ver sección 3) - -# Migraciones y arranque -python manage.py migrate -python manage.py createsuperuser # opcional -python manage.py runserver +cd NovaWoW/web-next +npm install +# crear .env.local con las variables de la sección 2 +npm run dev # http://127.0.0.1:3000 ``` -La app queda en http://127.0.0.1:8000 +## 2. Configuración (`web-next/.env.local`) ---- - -## 3. Configuración (`.env`) - -Toda la configuración sensible se lee de variables de entorno mediante -[python-dotenv](https://pypi.org/project/python-dotenv/). Copia `.env.example` -a `.env` y rellena los valores. **El `.env` no se versiona.** +**No se versiona** (contiene secretos). | Variable | Descripción | |---|---| -| `DJANGO_SECRET_KEY` | Clave secreta de Django. Genera una nueva (ver abajo). | -| `DJANGO_DEBUG` | `True` en desarrollo, `False` en producción. | -| `DB_USER` / `DB_PASSWORD` | Credenciales MySQL (compartidas por las 4 BBDD). | -| `DB_HOST` / `DB_PORT` | Host y puerto de MySQL. | -| `DB_NAME_DEFAULT` | BD del portal (por defecto `django_wow`). | -| `DB_NAME_AUTH` / `DB_NAME_CHARACTERS` / `DB_NAME_WORLD` | BBDD de AzerothCore. | -| `AC_SOAP_*` | Conexión SOAP a AzerothCore (crear cuentas, etc.). | -| `SUMUP_*` / `STRIPE_*` | Pasarelas de pago. | -| `EMAIL_HOST_USER` / `EMAIL_HOST_PASSWORD` | SMTP de Gmail (App Password). | +| `DB_USER` / `DB_PASSWORD` / `DB_HOST` / `DB_PORT` | Credenciales MySQL (compartidas por las 5 BBDD) | +| `DB_NAME_DEFAULT` | BD del portal (`django_wow`) | +| `DB_NAME_AUTH` / `DB_NAME_CHARACTERS` / `DB_NAME_WORLD` / `DB_NAME_WEB` | BBDD de AzerothCore y del foro | +| `SESSION_SECRET` | Firma de la sesión | +| `SITE_URL` | Raíz pública del sitio. **De aquí cuelgan todos los enlaces e imágenes de los correos**, así que cambiar de dominio es tocar solo esta variable | +| `EMAIL_*` | SMTP (hoy Proton Mail, `smtp.protonmail.ch:587` con STARTTLS). `EMAIL_HOST_USER` es además el remitente, y Proton exige que coincida con la dirección del token | +| `AC_SOAP_*` | Conexión SOAP a AzerothCore (envío de ítems, etc.) | +| `STRIPE_*` / `SUMUP_*` | Pasarelas de pago | +| `NEXT_PUBLIC_TURNSTILE_SITE_KEY` / `TURNSTILE_SECRET_KEY` | Captcha de Cloudflare | +| `ADMIN_EMAILS` / `ADMIN_GMLEVEL` | Acceso al panel de administración | +| `CRON_SECRET` | Protege el endpoint de reconciliación de SumUp | -Generar una `SECRET_KEY` nueva: +## 3. Producción + +- **Servicio:** `novawow-next.service` → `next start -p 3001` +- **Proxy:** Caddy. `nightspire.gg` redirige a `www.nightspire.gg`, que va a `127.0.0.1:3001`; + `next.nightspire.gg` apunta al mismo sitio. +- **Timer:** `novawow-dpoints-reconcile.timer` llama cada 2 min a `/api/dpoints/reconcile` + (SumUp no manda webhooks, así que se reconcilia sondeando). ```bash -python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())" +cd web-next && npm run build && systemctl restart novawow-next ``` ---- +## 4. Estructura -## 4. Producción - -```bash -# Recopilar estáticos -python manage.py collectstatic --noinput - -# Servir con gunicorn (4 workers) -gunicorn novawow.wsgi:application --bind 0.0.0.0:8000 --workers 4 +``` +web-next/ + app/[locale]/ páginas (una carpeta por ruta) + components/ componentes React + lib/ acceso a datos y lógica (SQL directo, sin ORM) + messages/ traducciones ES/EN + public/ estáticos, incluido el tema nw-ryu + sql/db2/ utilidades para leer los DB2 del cliente 3.4.3 +docs/ documentación (foro, migración a 3.4.3) +sql/ esquemas y semillas (foro, precios, etc.) ``` -Recomendaciones: +## 5. Notas -- `DJANGO_DEBUG=False` y una `DJANGO_SECRET_KEY` propia y secreta. -- Poner **Nginx** delante como proxy inverso y para servir `staticfiles/`. -- Revisar `ALLOWED_HOSTS` y `CSRF_TRUSTED_ORIGINS` en `novawow/settings.py`. +- En 3.4.3 **no existe `item_template`**: los datos de ítems salen de los `.db2` del cliente + (ver `web-next/sql/db2/`). +- La tienda vive en `/store-` en minúsculas (p. ej. `/store-trinity`); `/store` a secas da 404. +- Los correos se construyen en `web-next/lib/emails.ts`: una sola maqueta compartida por las 8 + plantillas, con todas las URLs derivadas de `SITE_URL`. ---- +## 6. Seguridad -## 5. Docker - -Alternativamente, se puede levantar todo con Docker (web + MySQL): - -```bash -cp .env.example .env # ajusta los secretos (no las variables DB_*, las fija compose) -docker compose up --build -``` - -- La web queda en http://127.0.0.1:8000 -- El servicio `db` crea automáticamente las 4 bases de datos. -- El `entrypoint` espera a MySQL, aplica migraciones y recopila estáticos antes de arrancar gunicorn. - -Ver `docker-compose.yml` y `Dockerfile` para los detalles. - ---- - -## 6. Notas de seguridad - -- Los secretos que estuvieron hardcodeados en el historial de git deben **rotarse** - (contraseña MySQL/SOAP, App Password de Gmail, claves de Stripe/SumUp). -- Nunca subas el archivo `.env` al repositorio. +- Los secretos que estuvieron hardcodeados en el historial de git deben **rotarse**: contraseña de + MySQL/SOAP, credenciales SMTP y claves de Stripe/SumUp. +- Ni `.env.local` ni ningún fichero con secretos se versionan. diff --git a/db.sqlite3 b/db.sqlite3 deleted file mode 100644 index e69de29..0000000 diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 3074e8f..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,44 +0,0 @@ -services: - db: - image: mysql:8.4 - restart: unless-stopped - environment: - MYSQL_ROOT_PASSWORD: rootpass - MYSQL_DATABASE: django_wow - MYSQL_USER: novawow - MYSQL_PASSWORD: novawow_pass - volumes: - - db_data:/var/lib/mysql - - ./docker/mysql-init.sql:/docker-entrypoint-initdb.d/init.sql:ro - healthcheck: - test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-prootpass"] - interval: 5s - timeout: 5s - retries: 20 - - web: - build: . - restart: unless-stopped - # Secretos de la app (Stripe, SumUp, SOAP, email, SECRET_KEY...) desde .env - env_file: - - path: .env - required: false - # Las variables de BD las fija compose para apuntar al servicio 'db' - environment: - DJANGO_DEBUG: "False" - DB_HOST: db - DB_PORT: "3306" - DB_USER: novawow - DB_PASSWORD: novawow_pass - DB_NAME_DEFAULT: django_wow - DB_NAME_AUTH: acore_auth - DB_NAME_CHARACTERS: acore_characters - DB_NAME_WORLD: acore_world - ports: - - "8000:8000" - depends_on: - db: - condition: service_healthy - -volumes: - db_data: diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh deleted file mode 100755 index 0571939..0000000 --- a/docker/entrypoint.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env bash -set -e - -# Espera a que MySQL esté disponible -if [ -n "$DB_HOST" ]; then - echo "Esperando a MySQL en $DB_HOST:${DB_PORT:-3306}..." - until nc -z "$DB_HOST" "${DB_PORT:-3306}"; do - sleep 1 - done - echo "MySQL disponible." -fi - -# Migraciones (solo la base 'default' del portal) -echo "Aplicando migraciones..." -python manage.py migrate --noinput - -# Estáticos -echo "Recopilando archivos estáticos..." -python manage.py collectstatic --noinput - -# Ejecuta el comando indicado (gunicorn por defecto) -exec "$@" diff --git a/docker/mysql-init.sql b/docker/mysql-init.sql deleted file mode 100644 index af15fde..0000000 --- a/docker/mysql-init.sql +++ /dev/null @@ -1,14 +0,0 @@ --- Crea las bases de datos que necesita el portal. --- La del portal (django_wow) y las de AzerothCore. --- Las de AzerothCore quedan vacías; en un entorno real las provee el core del juego. -CREATE DATABASE IF NOT EXISTS `django_wow` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -CREATE DATABASE IF NOT EXISTS `acore_auth` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -CREATE DATABASE IF NOT EXISTS `acore_characters` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -CREATE DATABASE IF NOT EXISTS `acore_world` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - --- Concede permisos al usuario de la aplicación sobre las 4 bases -GRANT ALL PRIVILEGES ON `django_wow`.* TO 'novawow'@'%'; -GRANT ALL PRIVILEGES ON `acore_auth`.* TO 'novawow'@'%'; -GRANT ALL PRIVILEGES ON `acore_characters`.* TO 'novawow'@'%'; -GRANT ALL PRIVILEGES ON `acore_world`.* TO 'novawow'@'%'; -FLUSH PRIVILEGES; diff --git a/forum/__init__.py b/forum/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/forum/apps.py b/forum/apps.py deleted file mode 100644 index c84a22c..0000000 --- a/forum/apps.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.apps import AppConfig - - -class ForumConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'forum' - verbose_name = 'Foro' diff --git a/forum/db.py b/forum/db.py deleted file mode 100644 index 53cb6d3..0000000 --- a/forum/db.py +++ /dev/null @@ -1,414 +0,0 @@ -""" -Acceso a datos del foro (BD ``acore_web``), con SQL directo — mismo estilo que -el resto del proyecto (``connections['acore_web']``). - -Tablas: forum_categories, forums, forum_topics, forum_posts, forum_reads. -""" - -from django.db import connections - - -def _dictfetchall(cursor): - cols = [c[0] for c in cursor.description] - return [dict(zip(cols, row)) for row in cursor.fetchall()] - - -def _dictfetchone(cursor): - row = cursor.fetchone() - if not row: - return None - cols = [c[0] for c in cursor.description] - return dict(zip(cols, row)) - - -def _conn(): - return connections['acore_web'] - - -# Detección perezosa de la columna opcional forum_topics.views (contador de visitas) -_views_supported = None - - -def has_views_column(): - global _views_supported - if _views_supported is None: - try: - with _conn().cursor() as cursor: - cursor.execute("SELECT views FROM forum_topics LIMIT 1") - _views_supported = True - except Exception: - _views_supported = False - return _views_supported - - -def increment_views(topic_id): - """Suma 1 a las visitas del tema (si existe la columna).""" - if not has_views_column(): - return - try: - with _conn().cursor() as cursor: - cursor.execute( - "UPDATE forum_topics SET views = views + 1 WHERE id = %s", [topic_id] - ) - except Exception: - pass - - -# -------------------------------------------------------------------------- -# Portada: categorías + foros (con estadísticas) -# -------------------------------------------------------------------------- - -def get_index(include_hidden=False): - """ - Devuelve las categorías ordenadas, cada una con sus foros (visibles y no - borrados salvo include_hidden) y estadísticas: nº de temas, nº de posts y - último tema. - """ - with _conn().cursor() as cursor: - cursor.execute( - "SELECT id, `order`, name FROM forum_categories ORDER BY `order`, id" - ) - categories = _dictfetchall(cursor) - - vis = "" if include_hidden else "AND f.visibility = 1 AND f.deleted = 0" - cursor.execute( - f""" - SELECT f.id, f.category_id, f.`order`, f.name, f.description, f.icon, - f.colortitle, f.type, f.visibility, f.deleted, - (SELECT COUNT(*) FROM forum_topics t - WHERE t.forum_id = f.id AND t.deleted = 0) AS topic_count, - (SELECT COUNT(*) FROM forum_posts p - JOIN forum_topics t ON p.topic_id = t.id - WHERE t.forum_id = f.id AND p.deleted = 0 AND t.deleted = 0) AS post_count, - (SELECT t2.id FROM forum_topics t2 - WHERE t2.forum_id = f.id AND t2.deleted = 0 - ORDER BY t2.updated_at DESC, t2.id DESC LIMIT 1) AS last_topic_id, - (SELECT t3.name FROM forum_topics t3 - WHERE t3.forum_id = f.id AND t3.deleted = 0 - ORDER BY t3.updated_at DESC, t3.id DESC LIMIT 1) AS last_topic_name, - (SELECT t4.poster FROM forum_topics t4 - WHERE t4.forum_id = f.id AND t4.deleted = 0 - ORDER BY t4.updated_at DESC, t4.id DESC LIMIT 1) AS last_topic_poster, - (SELECT t5.updated_at FROM forum_topics t5 - WHERE t5.forum_id = f.id AND t5.deleted = 0 - ORDER BY t5.updated_at DESC, t5.id DESC LIMIT 1) AS last_topic_time - FROM forums f - WHERE 1=1 {vis} - ORDER BY f.`order`, f.id - """ - ) - forums = _dictfetchall(cursor) - - by_cat = {} - for f in forums: - by_cat.setdefault(f['category_id'], []).append(f) - for c in categories: - c['forums'] = by_cat.get(c['id'], []) - # Solo categorías con foros visibles (salvo include_hidden) - if not include_hidden: - categories = [c for c in categories if c['forums']] - return categories - - -# -------------------------------------------------------------------------- -# Foro individual y sus temas (paginado) -# -------------------------------------------------------------------------- - -def get_forum(forum_id, include_hidden=False): - with _conn().cursor() as cursor: - cursor.execute( - "SELECT id, category_id, `order`, name, description, icon, colortitle, " - "type, visibility, deleted FROM forums WHERE id = %s", - [forum_id], - ) - forum = _dictfetchone(cursor) - if not forum: - return None - if not include_hidden and (forum['deleted'] or not forum['visibility']): - return None - return forum - - -def count_topics(forum_id): - with _conn().cursor() as cursor: - cursor.execute( - "SELECT COUNT(*) FROM forum_topics WHERE forum_id = %s AND deleted = 0", - [forum_id], - ) - return cursor.fetchone()[0] - - -def get_topics(forum_id, offset, limit): - views_sel = "t.views," if has_views_column() else "0 AS views," - with _conn().cursor() as cursor: - cursor.execute( - f""" - SELECT t.id, t.name, t.poster, t.poster_id, t.created, t.sticky, - t.locked, t.moved, t.updated_at, {views_sel} - (SELECT COUNT(*) FROM forum_posts p - WHERE p.topic_id = t.id AND p.deleted = 0) AS post_count, - (SELECT p2.poster FROM forum_posts p2 - WHERE p2.topic_id = t.id AND p2.deleted = 0 - ORDER BY p2.time DESC, p2.id DESC LIMIT 1) AS last_poster, - (SELECT p3.time FROM forum_posts p3 - WHERE p3.topic_id = t.id AND p3.deleted = 0 - ORDER BY p3.time DESC, p3.id DESC LIMIT 1) AS last_time - FROM forum_topics t - WHERE t.forum_id = %s AND t.deleted = 0 - ORDER BY t.sticky DESC, t.updated_at DESC, t.id DESC - LIMIT %s OFFSET %s - """, - [forum_id, limit, offset], - ) - return _dictfetchall(cursor) - - -# -------------------------------------------------------------------------- -# Tema individual y sus posts (paginado) -# -------------------------------------------------------------------------- - -def get_topic(topic_id, include_hidden=False): - views_sel = ", views" if has_views_column() else ", 0 AS views" - with _conn().cursor() as cursor: - cursor.execute( - "SELECT id, forum_id, name, poster, poster_id, created, sticky, " - f"locked, deleted, moved, updated_at{views_sel} FROM forum_topics WHERE id = %s", - [topic_id], - ) - topic = _dictfetchone(cursor) - if not topic: - return None - if not include_hidden and topic['deleted']: - return None - return topic - - -def count_posts(topic_id, include_deleted=False): - delf = "" if include_deleted else "AND deleted = 0" - with _conn().cursor() as cursor: - cursor.execute( - f"SELECT COUNT(*) FROM forum_posts WHERE topic_id = %s {delf}", - [topic_id], - ) - return cursor.fetchone()[0] - - -def get_posts(topic_id, offset, limit, include_deleted=False): - delf = "" if include_deleted else "AND deleted = 0" - with _conn().cursor() as cursor: - cursor.execute( - f""" - SELECT id, topic_id, poster, poster_id, text, time, deleted, updated_at - FROM forum_posts - WHERE topic_id = %s {delf} - ORDER BY time ASC, id ASC - LIMIT %s OFFSET %s - """, - [topic_id, limit, offset], - ) - return _dictfetchall(cursor) - - -def get_post(post_id): - with _conn().cursor() as cursor: - cursor.execute( - "SELECT id, topic_id, poster, poster_id, text, time, deleted, updated_at " - "FROM forum_posts WHERE id = %s", - [post_id], - ) - return _dictfetchone(cursor) - - -# -------------------------------------------------------------------------- -# Escritura: crear tema, responder, editar, borrar/restaurar -# -------------------------------------------------------------------------- - -def create_topic(forum_id, name, poster, poster_id, text): - """Crea un tema y su primer post. Devuelve el id del tema.""" - with _conn().cursor() as cursor: - cursor.execute( - "INSERT INTO forum_topics " - "(forum_id, name, poster, poster_id, created, created_at, updated_at, locked, sticky, deleted) " - "VALUES (%s, %s, %s, %s, NOW(), NOW(), NOW(), 0, 0, 0)", - [forum_id, name, poster, poster_id], - ) - cursor.execute("SELECT LAST_INSERT_ID()") - topic_id = cursor.fetchone()[0] - cursor.execute( - "INSERT INTO forum_posts (topic_id, poster, poster_id, text, time, created_at, deleted) " - "VALUES (%s, %s, %s, %s, NOW(), NOW(), 0)", - [topic_id, poster, poster_id, text], - ) - return topic_id - - -def create_post(topic_id, poster, poster_id, text): - """Añade una respuesta y actualiza la marca de tiempo del tema.""" - with _conn().cursor() as cursor: - cursor.execute( - "INSERT INTO forum_posts (topic_id, poster, poster_id, text, time, created_at, deleted) " - "VALUES (%s, %s, %s, %s, NOW(), NOW(), 0)", - [topic_id, poster, poster_id, text], - ) - cursor.execute("SELECT LAST_INSERT_ID()") - post_id = cursor.fetchone()[0] - cursor.execute( - "UPDATE forum_topics SET updated_at = NOW() WHERE id = %s", [topic_id] - ) - return post_id - - -def get_author_info(poster_id): - """ - Datos del autor para mostrar en los posts: nivel GM, fecha de registro y - nº de mensajes. Cruza acore_auth (account/account_access) y acore_web. - """ - info = {'gmlevel': 0, 'joindate': None, 'post_count': 0, 'status': 'Miembro', - 'display_name': None} - if not poster_id: - return info - info['display_name'] = get_display_name(poster_id) - try: - with connections['acore_auth'].cursor() as cursor: - cursor.execute("SELECT MAX(gmlevel) FROM account_access WHERE id = %s", [poster_id]) - row = cursor.fetchone() - info['gmlevel'] = int(row[0]) if row and row[0] is not None else 0 - cursor.execute("SELECT joindate FROM account WHERE id = %s", [poster_id]) - row = cursor.fetchone() - if row: - info['joindate'] = row[0] - except Exception: - pass - try: - with _conn().cursor() as cursor: - cursor.execute( - "SELECT COUNT(*) FROM forum_posts WHERE poster_id = %s AND deleted = 0", - [poster_id], - ) - info['post_count'] = cursor.fetchone()[0] - except Exception: - pass - if info['gmlevel'] >= 16: - info['status'] = 'Administrador' - elif info['gmlevel'] >= 1: - info['status'] = 'GM' - return info - - -def update_post(post_id, text): - with _conn().cursor() as cursor: - cursor.execute( - "UPDATE forum_posts SET text = %s, updated_at = NOW() WHERE id = %s", - [text, post_id], - ) - - -def set_post_deleted(post_id, deleted): - with _conn().cursor() as cursor: - cursor.execute( - "UPDATE forum_posts SET deleted = %s WHERE id = %s", - [1 if deleted else 0, post_id], - ) - - -def set_topic_flag(topic_id, field, value): - """field ∈ {sticky, locked, deleted, moved}.""" - if field not in ('sticky', 'locked', 'deleted', 'moved'): - raise ValueError('campo no permitido') - with _conn().cursor() as cursor: - cursor.execute( - f"UPDATE forum_topics SET {field} = %s WHERE id = %s", - [1 if value else 0, topic_id], - ) - - -def move_topic(topic_id, new_forum_id): - with _conn().cursor() as cursor: - cursor.execute( - "UPDATE forum_topics SET forum_id = %s, moved = 1 WHERE id = %s", - [new_forum_id, topic_id], - ) - - -def mark_read(user_id, topic_id): - if not user_id: - return - with _conn().cursor() as cursor: - cursor.execute( - "INSERT INTO forum_reads (user_id, topic_id, read_at) " - "VALUES (%s, %s, NOW()) " - "ON DUPLICATE KEY UPDATE read_at = NOW()", - [user_id, topic_id], - ) - - -def get_display_name(poster_id): - """ - Nombre legible del autor a partir de su cuenta: parte local del email - (antes de la @). Si no hay email, devuelve None (la plantilla usa el poster). - """ - if not poster_id: - return None - try: - with connections['acore_auth'].cursor() as cursor: - cursor.execute( - "SELECT email, reg_mail FROM account WHERE id = %s", [poster_id] - ) - row = cursor.fetchone() - except Exception: - return None - if not row: - return None - email = (row[0] or row[1] or '').strip() - if '@' in email: - return email.split('@', 1)[0] - return None - - -def count_search_topics(query): - like = f"%{query}%" - with _conn().cursor() as cursor: - cursor.execute( - """ - SELECT COUNT(DISTINCT t.id) - FROM forum_topics t - JOIN forums f ON f.id = t.forum_id AND f.visibility = 1 AND f.deleted = 0 - LEFT JOIN forum_posts p ON p.topic_id = t.id AND p.deleted = 0 - WHERE t.deleted = 0 AND (t.name LIKE %s OR p.text LIKE %s) - """, - [like, like], - ) - return cursor.fetchone()[0] - - -def search_topics(query, offset=0, limit=20): - """Busca temas por título o por contenido de sus posts (foros visibles).""" - like = f"%{query}%" - with _conn().cursor() as cursor: - cursor.execute( - """ - SELECT DISTINCT t.id, t.name, t.poster, t.poster_id, t.forum_id, - t.created, t.updated_at, f.name AS forum_name - FROM forum_topics t - JOIN forums f ON f.id = t.forum_id AND f.visibility = 1 AND f.deleted = 0 - LEFT JOIN forum_posts p ON p.topic_id = t.id AND p.deleted = 0 - WHERE t.deleted = 0 AND (t.name LIKE %s OR p.text LIKE %s) - ORDER BY t.updated_at DESC - LIMIT %s OFFSET %s - """, - [like, like, limit, offset], - ) - return _dictfetchall(cursor) - - -def list_forums_for_move(exclude_forum_id=None): - """Lista de foros visibles para el desplegable de 'mover tema'.""" - with _conn().cursor() as cursor: - cursor.execute( - "SELECT id, name FROM forums WHERE visibility = 1 AND deleted = 0 " - "ORDER BY `order`, id" - ) - forums = _dictfetchall(cursor) - if exclude_forum_id: - forums = [f for f in forums if f['id'] != exclude_forum_id] - return forums diff --git a/forum/forms.py b/forum/forms.py deleted file mode 100644 index 7e4a34a..0000000 --- a/forum/forms.py +++ /dev/null @@ -1,23 +0,0 @@ -""" -Formularios del foro con el editor enriquecido CKEditor 5 (el mismo que usa el -resto de NovaWoW). El HTML resultante se sanea con nh3 al guardar (ver views). -""" - -from django import forms -from django_ckeditor_5.widgets import CKEditor5Widget - - -class TopicForm(forms.Form): - name = forms.CharField( - max_length=45, - widget=forms.TextInput(attrs={'placeholder': 'Título del tema', 'maxlength': 45}), - ) - text = forms.CharField(widget=CKEditor5Widget(config_name='default')) - - -class ReplyForm(forms.Form): - text = forms.CharField(widget=CKEditor5Widget(config_name='default')) - - -class EditPostForm(forms.Form): - text = forms.CharField(widget=CKEditor5Widget(config_name='default')) diff --git a/forum/permissions.py b/forum/permissions.py deleted file mode 100644 index 5e511f5..0000000 --- a/forum/permissions.py +++ /dev/null @@ -1,71 +0,0 @@ -""" -Modelo de permisos del foro (versión simplificada y mantenible). - -En lugar de la matriz group_level×permission del proyecto original, usamos -reglas claras basadas en: - * si el usuario ha iniciado sesión y tiene una cuenta de juego seleccionada - (``request.session['account_id']``), - * su nivel GM (``acore_auth.account_access.gmlevel``), - * y la propiedad del contenido (autor == usuario). -""" - -from django.conf import settings -from django.db import connections - - -def get_account_id(request): - """Id de la cuenta de juego seleccionada en sesión, o None.""" - return request.session.get('account_id') - - -def is_authenticated(request): - return bool(request.session.get('account_id')) - - -def get_gmlevel(account_id): - """Nivel GM máximo de una cuenta (acore_auth.account_access). 0 si no tiene.""" - if not account_id: - return 0 - try: - with connections['acore_auth'].cursor() as cursor: - cursor.execute( - "SELECT MAX(gmlevel) FROM account_access WHERE id = %s", - [account_id], - ) - row = cursor.fetchone() - return int(row[0]) if row and row[0] is not None else 0 - except Exception: - return 0 - - -def is_moderator(request): - """¿El usuario puede moderar el foro?""" - account_id = get_account_id(request) - if not account_id: - return False - return get_gmlevel(account_id) >= settings.FORUM_MOD_GMLEVEL - - -def can_post(request): - """¿Puede crear temas / responder? Cualquier usuario autenticado.""" - return is_authenticated(request) - - -def can_edit_post(request, post): - """Puede editar/borrar un post: su autor o un moderador.""" - account_id = get_account_id(request) - if not account_id: - return False - if post and int(post.get('poster_id') or 0) == int(account_id): - return True - return is_moderator(request) - - -def forum_context(request): - """Datos comunes de permisos para las plantillas.""" - return { - 'forum_is_authenticated': is_authenticated(request), - 'forum_is_moderator': is_moderator(request), - 'forum_account_id': get_account_id(request), - 'forum_username': request.session.get('username'), - } diff --git a/forum/sanitize.py b/forum/sanitize.py deleted file mode 100644 index 3beba9e..0000000 --- a/forum/sanitize.py +++ /dev/null @@ -1,52 +0,0 @@ -""" -Saneado del HTML de los posts del foro (el editor del cliente puede enviar HTML). - -El proyecto original guardaba el HTML sin sanear y lo renderizaba con |raw -(XSS almacenado). Aquí lo saneamos con nh3 (allowlist) al guardar, de modo que -la plantilla puede renderizarlo con |safe sin riesgo. -""" - -import nh3 - -# Etiquetas permitidas (formato básico de foro) -_ALLOWED_TAGS = { - "p", "br", "hr", "span", "div", - "strong", "b", "em", "i", "u", "s", "strike", "sub", "sup", - "ul", "ol", "li", "blockquote", "code", "pre", - "h1", "h2", "h3", "h4", "h5", "h6", - "a", "img", - "table", "thead", "tbody", "tr", "th", "td", -} - -_ALLOWED_ATTRIBUTES = { - "a": {"href", "title", "target"}, - "img": {"src", "alt", "title", "width", "height"}, - "span": {"style"}, - "div": {"style"}, - "td": {"colspan", "rowspan"}, - "th": {"colspan", "rowspan"}, -} - -# Solo esquemas de URL seguros -_ALLOWED_URL_SCHEMES = {"http", "https", "mailto"} - - -def clean_post_html(html): - """Devuelve el HTML saneado y seguro para renderizar con |safe.""" - if not html: - return "" - return nh3.clean( - html, - tags=_ALLOWED_TAGS, - attributes=_ALLOWED_ATTRIBUTES, - url_schemes=_ALLOWED_URL_SCHEMES, - link_rel="noopener noreferrer nofollow", - ) - - -def plain_length(html): - """Longitud del texto plano (sin etiquetas), para validar longitud mínima.""" - if not html: - return 0 - text = nh3.clean(html, tags=set(), attributes={}) - return len(text.strip()) diff --git a/forum/templates/forum/_style.html b/forum/templates/forum/_style.html deleted file mode 100644 index 67faf4a..0000000 --- a/forum/templates/forum/_style.html +++ /dev/null @@ -1,45 +0,0 @@ - -{% if messages %} -
- {% for message in messages %} -
{{ message }}
- {% endfor %} -
-{% endif %} diff --git a/forum/templates/forum/create_topic.html b/forum/templates/forum/create_topic.html deleted file mode 100644 index f9a126b..0000000 --- a/forum/templates/forum/create_topic.html +++ /dev/null @@ -1,35 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - -
-
-
-

Nuevo tema

-
-
- {% include 'forum/_style.html' %} -
-

Foro: {{ forum.name }}

-
- {% csrf_token %} - {{ form.name }} - {{ form.text }} -
- - Cancelar -
-
- {{ form.media }} -
-
-
-
-
-
- - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} diff --git a/forum/templates/forum/edit_post.html b/forum/templates/forum/edit_post.html deleted file mode 100644 index 1a421f4..0000000 --- a/forum/templates/forum/edit_post.html +++ /dev/null @@ -1,33 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - -
-
-
-

Editar mensaje

-
-
- {% include 'forum/_style.html' %} -
-
- {% csrf_token %} - {{ form.text }} -
- - Cancelar -
-
- {{ form.media }} -
-
-
-
-
-
- - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} diff --git a/forum/templates/forum/forum.html b/forum/templates/forum/forum.html deleted file mode 100644 index f78ecd4..0000000 --- a/forum/templates/forum/forum.html +++ /dev/null @@ -1,63 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - -
-
-
-

{{ forum.name }}

-
-
- {% include 'forum/_style.html' %} -
-

{{ forum.description }}

-
- ← Volver al foro - {% if forum_is_authenticated %} - Crear nuevo tema - {% endif %} -
- - {% for topic in topics %} -
- -
-
{{ topic.post_count }} respuestas{% if has_views %} · {{ topic.views }} vistas{% endif %}
- {% if topic.last_poster %}
último: {{ topic.last_poster }}
{% endif %} - {% if topic.last_time %}
{{ topic.last_time }}
{% endif %} -
-
- {% empty %} -

No hay temas todavía. ¡Sé el primero en publicar!

- {% endfor %} - - {% if pagination.total_pages > 1 %} -
- {% if pagination.has_prev %}« Anterior{% endif %} - {% for p in pagination.page_range %} - {% if p == pagination.page %}{{ p }} - {% else %}{{ p }}{% endif %} - {% endfor %} - {% if pagination.has_next %}Siguiente »{% endif %} -
- {% endif %} -
-
-
-
-
-
- - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} diff --git a/forum/templates/forum/index.html b/forum/templates/forum/index.html deleted file mode 100644 index e11cc75..0000000 --- a/forum/templates/forum/index.html +++ /dev/null @@ -1,55 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - -
-
-
-

Foro de la comunidad

-
-
- {% include 'forum/_style.html' %} -
- - {% for category in categories %} -
-
{{ category.name }}
- {% for forum in category.forums %} -
-
- - {{ forum.name }} - - {% if not forum.visibility %}Oculto{% endif %} -
{{ forum.description }}
-
-
-
{{ forum.topic_count }} temas
-
{{ forum.post_count }} mensajes
- {% if forum.last_topic_name %} - - {% endif %} -
-
- {% endfor %} -
- {% empty %} -

Todavía no hay foros disponibles.

- {% endfor %} -
-
-
-
-
-
- - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} diff --git a/forum/templates/forum/profile.html b/forum/templates/forum/profile.html deleted file mode 100644 index a71cd7e..0000000 --- a/forum/templates/forum/profile.html +++ /dev/null @@ -1,30 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - -
-
-
-

Perfil de {{ profile.username }}

-
-
- {% include 'forum/_style.html' %} -
-

Temas creados: {{ profile.topic_count }}

-

Mensajes publicados: {{ profile.post_count }}

- {% if profile.joindate %}

Registro: {{ profile.joindate|date:"d/m/Y" }}

{% endif %} - -
-
-
-
-
-
- - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} diff --git a/forum/templates/forum/search.html b/forum/templates/forum/search.html deleted file mode 100644 index 707d223..0000000 --- a/forum/templates/forum/search.html +++ /dev/null @@ -1,60 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - -
-
-
-

Buscar en el foro

-
-
- {% include 'forum/_style.html' %} -
- - - {% if query %} -

Resultados para «{{ query }}»: {{ results|length }}

- {% for topic in results %} -
-
- {{ topic.name }} -
en {{ topic.forum_name }} · por {{ topic.display_poster }} · {{ topic.created }}
-
-
- {% empty %} -

No se encontraron temas.

- {% endfor %} - - {% if pagination and pagination.total_pages > 1 %} -
- {% if pagination.has_prev %}« Anterior{% endif %} - {% for p in pagination.page_range %} - {% if p == pagination.page %}{{ p }} - {% else %}{{ p }}{% endif %} - {% endfor %} - {% if pagination.has_next %}Siguiente »{% endif %} -
- {% endif %} - {% else %} -

Escribe al menos 2 caracteres para buscar.

- {% endif %} - - -
-
-
-
-
-
- - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} diff --git a/forum/templates/forum/topic.html b/forum/templates/forum/topic.html deleted file mode 100644 index 6302f10..0000000 --- a/forum/templates/forum/topic.html +++ /dev/null @@ -1,122 +0,0 @@ - -{% load forum_extras %} - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - -
-
-
-

{{ topic.name }}

-
-
- {% include 'forum/_style.html' %} -
-
- {% if forum %}← {{ forum.name }}{% endif %} - {% if topic.has_views %}{{ topic.views }} visitas{% endif %} - {% if topic.locked %}Tema bloqueado{% endif %} -
- - {% for post in posts %} -
-
- -
{{ post.author.status }}
-
- {% if post.author.joindate %}Registro: {{ post.author.joindate|date:"d/m/Y" }}
{% endif %} - Mensajes: {{ post.author.post_count }} -
-
-
- {% if post.deleted %} -
[Mensaje eliminado]
- {% else %} -
{{ post.text|forum_safe }}
- {% endif %} -
- {{ post.time }}{% if post.updated_at and post.updated_at != post.time %} · editado {{ post.updated_at }}{% endif %} - - {% if post.can_edit and not post.deleted %} - Editar - ·
{% csrf_token %}
- {% endif %} - {% if post.deleted and forum_is_moderator %} -
{% csrf_token %}
- {% endif %} -
-
-
-
- {% endfor %} - - {% if pagination.total_pages > 1 %} -
- {% if pagination.has_prev %}« Anterior{% endif %} - {% for p in pagination.page_range %} - {% if p == pagination.page %}{{ p }} - {% else %}{{ p }}{% endif %} - {% endfor %} - {% if pagination.has_next %}Siguiente »{% endif %} -
- {% endif %} - - - - {% if can_reply %} -
-

Responder

-
- {% csrf_token %} - {{ reply_form.text }} -
- -
-
- {{ reply_form.media }} -
- {% elif topic.locked %} -

Este tema está bloqueado. No se admiten nuevas respuestas.

- {% elif not forum_is_authenticated %} -

Inicia sesión para responder.

- {% endif %} - - {% if forum_is_moderator %} -
-

Moderación

-
- {% if topic.locked %} -
{% csrf_token %}
- {% else %} -
{% csrf_token %}
- {% endif %} -
{% csrf_token %}
- {% if topic.deleted %} -
{% csrf_token %}
- {% else %} -
{% csrf_token %}
- {% endif %} -
- {% if move_forums %} -
- {% csrf_token %} - - -
- {% endif %} -
- {% endif %} -
-
-
-
-
-
- - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} diff --git a/forum/templatetags/__init__.py b/forum/templatetags/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/forum/templatetags/forum_extras.py b/forum/templatetags/forum_extras.py deleted file mode 100644 index 7ffcce2..0000000 --- a/forum/templatetags/forum_extras.py +++ /dev/null @@ -1,18 +0,0 @@ -"""Filtros de plantilla del foro.""" - -from django import template -from django.utils.safestring import mark_safe - -from .. import sanitize - -register = template.Library() - - -@register.filter -def forum_safe(value): - """ - Renderiza el HTML de un post saneándolo con nh3 en el momento (defensa en - profundidad). Aunque el contenido nuevo ya se sanea al guardar, esto protege - frente a filas heredadas/importadas que pudieran contener HTML sin sanear. - """ - return mark_safe(sanitize.clean_post_html(value or '')) diff --git a/forum/urls.py b/forum/urls.py deleted file mode 100644 index 8db266a..0000000 --- a/forum/urls.py +++ /dev/null @@ -1,38 +0,0 @@ -from django.urls import path -from . import views - -# Se mantienen los nombres de ruta del proyecto original para coherencia. -# El orden importa: las rutas con literales (topic/create/edit/post) van antes -# que la genérica forum//... -urlpatterns = [ - path('forum', views.index, name='app_forum'), - path('forum/search', views.search, name='forum_search'), - - # Temas - path('forum/topic//reply', views.reply_to_topic, name='forum_post_reply'), - path('forum/topic//move', views.move_topic, name='forum_move_topic'), - path('forum/topic//lock', views.lock_topic, name='forum_lock_topic'), - path('forum/topic//unlock', views.unlock_topic, name='forum_unlock_topic'), - path('forum/topic//sticky', views.toggle_sticky, name='forum_toggle_sticky'), - path('forum/topic//restore', views.restore_topic, name='forum_restore_topic'), - path('forum/topic/delete/', views.delete_topic, name='forum_delete_topic'), - path('forum/topic//', views.view_topic, name='forum_topic'), - path('forum/topic/', views.view_topic, name='forum_topic_p1'), - - # Posts - path('forum/post/update/', views.update_post, name='forum_update_post'), - path('forum/post/delete/', views.delete_post, name='forum_delete_post'), - path('forum/post//restore', views.restore_post, name='forum_restore_post'), - path('forum/edit/', views.edit_post, name='forum_edit_post'), - - # Crear tema - path('forum/create//submit', views.submit_create_topic, name='forum_create_topic_submit'), - path('forum/create/', views.create_topic, name='forum_create_topic'), - - # Ver foro (paginado) — genérica, al final - path('forum//', views.view_forum, name='forum_view'), - path('forum/', views.view_forum, name='forum_view_p1'), - - # Perfil público - path('profile/', views.profile, name='forum_profile'), -] diff --git a/forum/views.py b/forum/views.py deleted file mode 100644 index 27448e8..0000000 --- a/forum/views.py +++ /dev/null @@ -1,453 +0,0 @@ -""" -Vistas del foro. Diseño integrado con NovaWoW (usa los partials del sitio). - -Identidad del autor = cuenta de juego en sesión (``username`` / ``account_id``). -Permisos: ver `permissions.py`. -""" - -import math - -from django.conf import settings -from django.contrib import messages -from django.http import JsonResponse -from django.shortcuts import redirect, render -from django.urls import reverse -from django.views.decorators.csrf import csrf_protect - -from . import db -from . import permissions as perm -from . import sanitize -from .forms import TopicForm, ReplyForm, EditPostForm - -MIN_TEXT_LEN = 5 - - -# -------------------------------------------------------------------------- -# Utilidades -# -------------------------------------------------------------------------- - -def _paginate(total, page, per_page): - total_pages = max(1, math.ceil(total / per_page)) if per_page else 1 - page = max(1, min(page, total_pages)) - offset = (page - 1) * per_page - return { - 'page': page, - 'total_pages': total_pages, - 'has_prev': page > 1, - 'has_next': page < total_pages, - 'prev_page': page - 1, - 'next_page': page + 1, - 'page_range': range(1, total_pages + 1), - 'total': total, - }, offset - - -def _base_context(request, extra=None): - ctx = dict(perm.forum_context(request)) - if extra: - ctx.update(extra) - return ctx - - -def _require_login(request): - if not perm.is_authenticated(request): - return redirect('login') - return None - - -def _topic_locked_for(request, topic_id): - """True si el tema está bloqueado y el usuario no es moderador.""" - if perm.is_moderator(request): - return False - topic = db.get_topic(topic_id, include_hidden=True) - return bool(topic and topic['locked']) - - -# -------------------------------------------------------------------------- -# Navegación -# -------------------------------------------------------------------------- - -def index(request): - categories = db.get_index(include_hidden=perm.is_moderator(request)) - return render(request, 'forum/index.html', _base_context(request, { - 'categories': categories, - })) - - -def view_forum(request, forum_id, page=1): - is_mod = perm.is_moderator(request) - forum = db.get_forum(forum_id, include_hidden=is_mod) - if not forum: - messages.error(request, 'Foro no encontrado.') - return redirect('app_forum') - - total = db.count_topics(forum_id) - pag, offset = _paginate(total, page, settings.FORUM_TOPICS_PER_PAGE) - topics = db.get_topics(forum_id, offset, settings.FORUM_TOPICS_PER_PAGE) - for t in topics: - t['display_poster'] = db.get_display_name(t['poster_id']) or t['poster'] - - return render(request, 'forum/forum.html', _base_context(request, { - 'forum': forum, - 'topics': topics, - 'pagination': pag, - 'has_views': db.has_views_column(), - })) - - -def search(request): - query = (request.GET.get('q') or '').strip() - try: - page = int(request.GET.get('page', 1)) - except (TypeError, ValueError): - page = 1 - results = [] - pag = None - if len(query) >= 2: - total = db.count_search_topics(query) - pag, offset = _paginate(total, page, settings.FORUM_TOPICS_PER_PAGE) - results = db.search_topics(query, offset, settings.FORUM_TOPICS_PER_PAGE) - for r in results: - r['display_poster'] = db.get_display_name(r['poster_id']) or r['poster'] - return render(request, 'forum/search.html', _base_context(request, { - 'query': query, - 'results': results, - 'pagination': pag, - })) - - -def view_topic(request, topic_id, page=1): - is_mod = perm.is_moderator(request) - topic = db.get_topic(topic_id, include_hidden=is_mod) - if not topic: - messages.error(request, 'Tema no encontrado.') - return redirect('app_forum') - - # El foro padre debe ser visible (salvo moderador); si no, no se accede al - # tema por URL directa aunque no aparezca en los listados. - forum = db.get_forum(topic['forum_id'], include_hidden=is_mod) - if not forum: - messages.error(request, 'Tema no disponible.') - return redirect('app_forum') - - # Contador de visitas (solo si la columna existe) - if db.has_views_column(): - db.increment_views(topic_id) - topic['views'] = (topic.get('views') or 0) + 1 - topic['has_views'] = True - else: - topic['has_views'] = False - - total = db.count_posts(topic_id, include_deleted=is_mod) - pag, offset = _paginate(total, page, settings.FORUM_POSTS_PER_PAGE) - posts = db.get_posts(topic_id, offset, settings.FORUM_POSTS_PER_PAGE, is_mod) - - # Marcar como leído - db.mark_read(perm.get_account_id(request), topic_id) - - # Permisos y datos de autor por post - for p in posts: - p['can_edit'] = perm.can_edit_post(request, p) - p['author'] = db.get_author_info(p['poster_id']) - - return render(request, 'forum/topic.html', _base_context(request, { - 'topic': topic, - 'forum': forum, - 'posts': posts, - 'pagination': pag, - 'can_reply': perm.can_post(request) and (not topic['locked'] or is_mod), - 'move_forums': db.list_forums_for_move(topic['forum_id']) if is_mod else [], - 'reply_form': ReplyForm(), - })) - - -# -------------------------------------------------------------------------- -# Crear tema / responder -# -------------------------------------------------------------------------- - -def create_topic(request, forum_id): - login = _require_login(request) - if login: - return login - forum = db.get_forum(forum_id, include_hidden=perm.is_moderator(request)) - if not forum: - messages.error(request, 'Foro no encontrado.') - return redirect('app_forum') - return render(request, 'forum/create_topic.html', _base_context(request, { - 'forum': forum, - 'form': TopicForm(), - })) - - -@csrf_protect -def submit_create_topic(request, forum_id): - login = _require_login(request) - if login: - return login - if request.method != 'POST': - return redirect('forum_create_topic', forum_id=forum_id) - - forum = db.get_forum(forum_id, include_hidden=perm.is_moderator(request)) - if not forum: - messages.error(request, 'Foro no encontrado.') - return redirect('app_forum') - - name = (request.POST.get('name') or '').strip() - text = sanitize.clean_post_html((request.POST.get('text') or '').strip()) - if not name or sanitize.plain_length(text) < MIN_TEXT_LEN: - messages.error(request, 'El título y el contenido (mínimo 5 caracteres) son obligatorios.') - return redirect('forum_create_topic', forum_id=forum_id) - if len(name) > 45: - messages.error(request, 'El título no puede superar los 45 caracteres.') - return redirect('forum_create_topic', forum_id=forum_id) - - poster = request.session.get('username') - poster_id = perm.get_account_id(request) - topic_id = db.create_topic(forum_id, name, poster, poster_id, text) - messages.success(request, 'Tema creado.') - return redirect('forum_topic_p1', topic_id=topic_id) - - -@csrf_protect -def reply_to_topic(request, topic_id): - login = _require_login(request) - if login: - return login - if request.method != 'POST': - return redirect('forum_topic_p1', topic_id=topic_id) - - is_mod = perm.is_moderator(request) - topic = db.get_topic(topic_id) - if not topic: - messages.error(request, 'Tema no encontrado.') - return redirect('app_forum') - # El foro padre debe ser visible (salvo moderador) - if not db.get_forum(topic['forum_id'], include_hidden=is_mod): - messages.error(request, 'Tema no disponible.') - return redirect('app_forum') - if topic['locked'] and not is_mod: - messages.error(request, 'El tema está bloqueado.') - return redirect('forum_topic_p1', topic_id=topic_id) - - text = sanitize.clean_post_html((request.POST.get('text') or '').strip()) - if sanitize.plain_length(text) < MIN_TEXT_LEN: - messages.error(request, 'El mensaje debe tener al menos 5 caracteres.') - return redirect('forum_topic_p1', topic_id=topic_id) - - db.create_post(topic_id, request.session.get('username'), perm.get_account_id(request), text) - - # Ir a la última página - total = db.count_posts(topic_id) - last_page = max(1, math.ceil(total / settings.FORUM_POSTS_PER_PAGE)) - url = reverse('forum_topic', kwargs={'topic_id': topic_id, 'page': last_page}) - return redirect(f'{url}#post-bottom') - - -# -------------------------------------------------------------------------- -# Editar / borrar / restaurar posts -# -------------------------------------------------------------------------- - -def edit_post(request, post_id): - login = _require_login(request) - if login: - return login - post = db.get_post(post_id) - if not post: - messages.error(request, 'Post no encontrado.') - return redirect('app_forum') - if not perm.can_edit_post(request, post): - messages.error(request, 'No tienes permiso para editar este post.') - return redirect('forum_topic_p1', topic_id=post['topic_id']) - return render(request, 'forum/edit_post.html', _base_context(request, { - 'post': post, - 'form': EditPostForm(initial={'text': post['text']}), - })) - - -@csrf_protect -def update_post(request, post_id): - login = _require_login(request) - if login: - return login - post = db.get_post(post_id) - if not post: - messages.error(request, 'Post no encontrado.') - return redirect('app_forum') - if not perm.can_edit_post(request, post): - messages.error(request, 'No tienes permiso para editar este post.') - return redirect('forum_topic_p1', topic_id=post['topic_id']) - if _topic_locked_for(request, post['topic_id']): - messages.error(request, 'El tema está bloqueado.') - return redirect('forum_topic_p1', topic_id=post['topic_id']) - if request.method != 'POST': - return redirect('forum_edit_post', post_id=post_id) - - text = sanitize.clean_post_html((request.POST.get('text') or '').strip()) - if sanitize.plain_length(text) < MIN_TEXT_LEN: - messages.error(request, 'El mensaje debe tener al menos 5 caracteres.') - return redirect('forum_edit_post', post_id=post_id) - - db.update_post(post_id, text) - messages.success(request, 'Post actualizado.') - return redirect('forum_topic_p1', topic_id=post['topic_id']) - - -@csrf_protect -def delete_post(request, post_id): - login = _require_login(request) - if login: - return login - post = db.get_post(post_id) - if not post: - messages.error(request, 'Post no encontrado.') - return redirect('app_forum') - if not perm.can_edit_post(request, post): - messages.error(request, 'No tienes permiso.') - return redirect('forum_topic_p1', topic_id=post['topic_id']) - if _topic_locked_for(request, post['topic_id']): - messages.error(request, 'El tema está bloqueado.') - return redirect('forum_topic_p1', topic_id=post['topic_id']) - if request.method != 'POST': - return redirect('forum_topic_p1', topic_id=post['topic_id']) - db.set_post_deleted(post_id, True) - messages.success(request, 'Post borrado.') - return redirect('forum_topic_p1', topic_id=post['topic_id']) - - -@csrf_protect -def restore_post(request, post_id): - if not perm.is_moderator(request): - return redirect('app_forum') - post = db.get_post(post_id) - if not post: - return redirect('app_forum') - if request.method != 'POST': - return redirect('forum_topic_p1', topic_id=post['topic_id']) - db.set_post_deleted(post_id, False) - messages.success(request, 'Post restaurado.') - return redirect('forum_topic_p1', topic_id=post['topic_id']) - - -# -------------------------------------------------------------------------- -# Moderación de temas -# -------------------------------------------------------------------------- - -def _mod_only(request): - if not perm.is_moderator(request): - messages.error(request, 'Acción solo para moderadores.') - return redirect('app_forum') - return None - - -def _mod_post_only(request): - """Exige moderador y método POST (CSRF).""" - block = _mod_only(request) - if block: - return block - if request.method != 'POST': - return redirect('app_forum') - return None - - -@csrf_protect -def lock_topic(request, topic_id): - block = _mod_post_only(request) - if block: - return block - db.set_topic_flag(topic_id, 'locked', True) - messages.success(request, 'Tema bloqueado.') - return redirect('forum_topic_p1', topic_id=topic_id) - - -@csrf_protect -def unlock_topic(request, topic_id): - block = _mod_post_only(request) - if block: - return block - db.set_topic_flag(topic_id, 'locked', False) - messages.success(request, 'Tema desbloqueado.') - return redirect('forum_topic_p1', topic_id=topic_id) - - -@csrf_protect -def toggle_sticky(request, topic_id): - block = _mod_post_only(request) - if block: - return block - topic = db.get_topic(topic_id, include_hidden=True) - if topic: - db.set_topic_flag(topic_id, 'sticky', not topic['sticky']) - messages.success(request, 'Tema fijado.' if not topic['sticky'] else 'Tema no fijado.') - return redirect('forum_topic_p1', topic_id=topic_id) - - -@csrf_protect -def delete_topic(request, topic_id): - block = _mod_post_only(request) - if block: - return block - topic = db.get_topic(topic_id, include_hidden=True) - db.set_topic_flag(topic_id, 'deleted', True) - messages.success(request, 'Tema borrado.') - if topic: - return redirect('forum_view_p1', forum_id=topic['forum_id']) - return redirect('app_forum') - - -@csrf_protect -def restore_topic(request, topic_id): - block = _mod_post_only(request) - if block: - return block - db.set_topic_flag(topic_id, 'deleted', False) - messages.success(request, 'Tema restaurado.') - return redirect('forum_topic_p1', topic_id=topic_id) - - -@csrf_protect -def move_topic(request, topic_id): - block = _mod_only(request) - if block: - return block - if request.method != 'POST': - return redirect('forum_topic_p1', topic_id=topic_id) - try: - new_forum_id = int(request.POST.get('forum_id', '')) - except (TypeError, ValueError): - messages.error(request, 'Foro destino no válido.') - return redirect('forum_topic_p1', topic_id=topic_id) - if not db.get_forum(new_forum_id, include_hidden=True): - messages.error(request, 'Foro destino no válido.') - return redirect('forum_topic_p1', topic_id=topic_id) - db.move_topic(topic_id, new_forum_id) - messages.success(request, 'Tema movido.') - return redirect('forum_topic_p1', topic_id=topic_id) - - -# -------------------------------------------------------------------------- -# Perfil público -# -------------------------------------------------------------------------- - -def profile(request, username): - from django.db import connections - stats = {'username': username, 'post_count': 0, 'topic_count': 0, 'joindate': None} - with connections['acore_web'].cursor() as cursor: - cursor.execute( - "SELECT COUNT(*) FROM forum_posts WHERE poster = %s AND deleted = 0", [username] - ) - stats['post_count'] = cursor.fetchone()[0] - cursor.execute( - "SELECT COUNT(*) FROM forum_topics WHERE poster = %s AND deleted = 0", [username] - ) - stats['topic_count'] = cursor.fetchone()[0] - try: - with connections['acore_auth'].cursor() as cursor: - cursor.execute("SELECT joindate FROM account WHERE username = %s", [username]) - row = cursor.fetchone() - if row: - stats['joindate'] = row[0] - except Exception: - pass - return render(request, 'forum/profile.html', _base_context(request, { - 'profile': stats, - })) diff --git a/frontend/README.md b/frontend/README.md deleted file mode 100644 index 7a247df..0000000 --- a/frontend/README.md +++ /dev/null @@ -1,32 +0,0 @@ -# Frontend NovaWoW (islas React/TSX con Vite) - -Migración incremental de las plantillas HTML de Django a componentes React/TSX, -montados como **islas** sobre las páginas que sigue sirviendo Django. Los datos se -obtienen de endpoints JSON bajo `/api/` (ver `home/api_urls.py`). - -## Estructura -- `src/entries/*.tsx` — puntos de entrada de Vite; cada uno monta una isla en un - contenedor que deja la plantilla Django (p. ej. `#home-news-app`). -- `src/components/*.tsx` — componentes React. -- `vite.config.ts` — build a `../static/dist` con manifest (base `/static/dist/`). - -La plantilla carga la isla con `{% load django_vite %}` + `{% vite_asset 'src/entries/xxx.tsx' %}`. - -## Desarrollo (con HMR) -Requiere `DJANGO_DEBUG=True`. En un terminal: -``` -cd frontend && npm install && npm run dev -``` -django-vite (dev_mode = DEBUG) inyecta el cliente HMR y sirve desde el dev server. - -## Producción (build) -Con `DJANGO_DEBUG=False`, Django sirve el bundle construido vía manifest. Tras -cualquier cambio en `frontend/`: -``` -cd /root/NovaWoW/frontend && npm run build # genera ../static/dist + manifest -cd /root/NovaWoW && .venv/bin/python manage.py collectstatic --noinput -systemctl restart novawow -``` - -Los artefactos (`node_modules/`, `static/dist/`) están en `.gitignore`: se regeneran -con `npm run build`. diff --git a/frontend/package-lock.json b/frontend/package-lock.json deleted file mode 100644 index 103e3e1..0000000 --- a/frontend/package-lock.json +++ /dev/null @@ -1,1729 +0,0 @@ -{ - "name": "novawow-frontend", - "version": "0.1.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "novawow-frontend", - "version": "0.1.0", - "dependencies": { - "react": "^18.3.1", - "react-dom": "^18.3.1" - }, - "devDependencies": { - "@types/react": "^18.3.12", - "@types/react-dom": "^18.3.1", - "@vitejs/plugin-react": "^4.3.4", - "typescript": "^5.6.3", - "vite": "^6.0.7" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", - "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.29.7", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", - "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", - "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.7", - "@babel/helper-compilation-targets": "^7.29.7", - "@babel/helper-module-transforms": "^7.29.7", - "@babel/helpers": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/template": "^7.29.7", - "@babel/traverse": "^7.29.7", - "@babel/types": "^7.29.7", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/generator": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", - "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.29.7", - "@babel/types": "^7.29.7", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", - "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.29.7", - "@babel/helper-validator-option": "^7.29.7", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", - "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", - "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", - "dev": true, - "dependencies": { - "@babel/traverse": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", - "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", - "dev": true, - "dependencies": { - "@babel/helper-module-imports": "^7.29.7", - "@babel/helper-validator-identifier": "^7.29.7", - "@babel/traverse": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", - "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", - "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", - "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", - "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", - "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", - "dev": true, - "dependencies": { - "@babel/template": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", - "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.29.7" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", - "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", - "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/template": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", - "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", - "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.7", - "@babel/helper-globals": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/template": "^7.29.7", - "@babel/types": "^7.29.7", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", - "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", - "dev": true, - "dependencies": { - "@babel/helper-string-parser": "^7.29.7", - "@babel/helper-validator-identifier": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", - "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", - "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", - "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", - "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", - "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", - "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", - "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", - "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", - "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", - "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", - "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", - "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", - "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", - "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", - "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", - "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", - "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", - "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", - "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", - "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", - "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", - "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", - "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", - "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", - "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.27", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", - "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", - "dev": true - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", - "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", - "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", - "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", - "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", - "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", - "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", - "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", - "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", - "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", - "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", - "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", - "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", - "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", - "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", - "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", - "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", - "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", - "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", - "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", - "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", - "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", - "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", - "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", - "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", - "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", - "dev": true, - "dependencies": { - "@babel/types": "^7.28.2" - } - }, - "node_modules/@types/estree": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", - "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", - "dev": true - }, - "node_modules/@types/prop-types": { - "version": "15.7.15", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", - "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "dev": true - }, - "node_modules/@types/react": { - "version": "18.3.31", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", - "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", - "dev": true, - "dependencies": { - "@types/prop-types": "*", - "csstype": "^3.2.2" - } - }, - "node_modules/@types/react-dom": { - "version": "18.3.7", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", - "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", - "dev": true, - "peerDependencies": { - "@types/react": "^18.0.0" - } - }, - "node_modules/@vitejs/plugin-react": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", - "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", - "dev": true, - "dependencies": { - "@babel/core": "^7.28.0", - "@babel/plugin-transform-react-jsx-self": "^7.27.1", - "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-beta.27", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.17.0" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" - } - }, - "node_modules/baseline-browser-mapping": { - "version": "2.10.43", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.43.tgz", - "integrity": "sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ==", - "dev": true, - "bin": { - "baseline-browser-mapping": "dist/cli.cjs" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/browserslist": { - "version": "4.28.6", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.6.tgz", - "integrity": "sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "baseline-browser-mapping": "^2.10.42", - "caniuse-lite": "^1.0.30001803", - "electron-to-chromium": "^1.5.389", - "node-releases": "^2.0.51", - "update-browserslist-db": "^1.2.3" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001805", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001805.tgz", - "integrity": "sha512-52noaS3DubycKSXaU30TwPGIp+POyQSUVa5jBEq3vkRkY0kjyb3LQgvhU6WGyCcyXqVLWO0Cw0Q6BSdD0kUfVA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ] - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true - }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/electron-to-chromium": { - "version": "1.5.389", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.389.tgz", - "integrity": "sha512-cEto7aeOqBfU1D+c5py5pE+ooscKE75JifxLBdFUZsqAxRS6y7kebtxAZvICszSl05gPjYHDTjY+lXpyGvpJbg==", - "dev": true - }, - "node_modules/esbuild": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", - "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", - "dev": true, - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.12", - "@esbuild/android-arm": "0.25.12", - "@esbuild/android-arm64": "0.25.12", - "@esbuild/android-x64": "0.25.12", - "@esbuild/darwin-arm64": "0.25.12", - "@esbuild/darwin-x64": "0.25.12", - "@esbuild/freebsd-arm64": "0.25.12", - "@esbuild/freebsd-x64": "0.25.12", - "@esbuild/linux-arm": "0.25.12", - "@esbuild/linux-arm64": "0.25.12", - "@esbuild/linux-ia32": "0.25.12", - "@esbuild/linux-loong64": "0.25.12", - "@esbuild/linux-mips64el": "0.25.12", - "@esbuild/linux-ppc64": "0.25.12", - "@esbuild/linux-riscv64": "0.25.12", - "@esbuild/linux-s390x": "0.25.12", - "@esbuild/linux-x64": "0.25.12", - "@esbuild/netbsd-arm64": "0.25.12", - "@esbuild/netbsd-x64": "0.25.12", - "@esbuild/openbsd-arm64": "0.25.12", - "@esbuild/openbsd-x64": "0.25.12", - "@esbuild/openharmony-arm64": "0.25.12", - "@esbuild/sunos-x64": "0.25.12", - "@esbuild/win32-arm64": "0.25.12", - "@esbuild/win32-ia32": "0.25.12", - "@esbuild/win32-x64": "0.25.12" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, - "node_modules/nanoid": { - "version": "3.3.16", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", - "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/node-releases": { - "version": "2.0.51", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz", - "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", - "dev": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true - }, - "node_modules/picomatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", - "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/postcss": { - "version": "8.5.17", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.17.tgz", - "integrity": "sha512-J7EF+8X+CzRPaJPOv9Ck2wNWJvGnnl3PcNPAdGg6GTLjyVpyQ0yATMSXRFRV01BviT/9Gwuc3rjEyJbDJG9a4w==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "nanoid": "^3.3.12", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" - }, - "peerDependencies": { - "react": "^18.3.1" - } - }, - "node_modules/react-refresh": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", - "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/rollup": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", - "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", - "dev": true, - "dependencies": { - "@types/estree": "1.0.9" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.62.2", - "@rollup/rollup-android-arm64": "4.62.2", - "@rollup/rollup-darwin-arm64": "4.62.2", - "@rollup/rollup-darwin-x64": "4.62.2", - "@rollup/rollup-freebsd-arm64": "4.62.2", - "@rollup/rollup-freebsd-x64": "4.62.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", - "@rollup/rollup-linux-arm-musleabihf": "4.62.2", - "@rollup/rollup-linux-arm64-gnu": "4.62.2", - "@rollup/rollup-linux-arm64-musl": "4.62.2", - "@rollup/rollup-linux-loong64-gnu": "4.62.2", - "@rollup/rollup-linux-loong64-musl": "4.62.2", - "@rollup/rollup-linux-ppc64-gnu": "4.62.2", - "@rollup/rollup-linux-ppc64-musl": "4.62.2", - "@rollup/rollup-linux-riscv64-gnu": "4.62.2", - "@rollup/rollup-linux-riscv64-musl": "4.62.2", - "@rollup/rollup-linux-s390x-gnu": "4.62.2", - "@rollup/rollup-linux-x64-gnu": "4.62.2", - "@rollup/rollup-linux-x64-musl": "4.62.2", - "@rollup/rollup-openbsd-x64": "4.62.2", - "@rollup/rollup-openharmony-arm64": "4.62.2", - "@rollup/rollup-win32-arm64-msvc": "4.62.2", - "@rollup/rollup-win32-ia32-msvc": "4.62.2", - "@rollup/rollup-win32-x64-gnu": "4.62.2", - "@rollup/rollup-win32-x64-msvc": "4.62.2", - "fsevents": "~2.3.2" - } - }, - "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "dependencies": { - "loose-envify": "^1.1.0" - } - }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/tinyglobby": { - "version": "0.2.17", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", - "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", - "dev": true, - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/vite": { - "version": "6.4.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", - "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", - "dev": true, - "dependencies": { - "esbuild": "^0.25.0", - "fdir": "^6.4.4", - "picomatch": "^4.0.2", - "postcss": "^8.5.3", - "rollup": "^4.34.9", - "tinyglobby": "^0.2.13" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "jiti": ">=1.21.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true - } - } -} diff --git a/frontend/package.json b/frontend/package.json deleted file mode 100644 index 7373591..0000000 --- a/frontend/package.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "novawow-frontend", - "private": true, - "version": "0.1.0", - "type": "module", - "scripts": { - "dev": "vite", - "build": "vite build", - "preview": "vite preview" - }, - "dependencies": { - "react": "^18.3.1", - "react-dom": "^18.3.1" - }, - "devDependencies": { - "@types/react": "^18.3.12", - "@types/react-dom": "^18.3.1", - "@vitejs/plugin-react": "^4.3.4", - "typescript": "^5.6.3", - "vite": "^6.0.7" - } -} diff --git a/frontend/src/components/AccountDashboard.tsx b/frontend/src/components/AccountDashboard.tsx deleted file mode 100644 index fd4d157..0000000 --- a/frontend/src/components/AccountDashboard.tsx +++ /dev/null @@ -1,176 +0,0 @@ -import { useEffect, useState } from 'react' - -interface UserInfo { - bnet_email: string - username: string - reg_mail: string - email: string - last_ip: string - last_attempt_ip: string - joindate: string -} - -interface AccountStatus { - is_banned: boolean - unban_date?: string - remaining_time?: number - is_recruited?: boolean - recruited_count?: number -} - -interface Character { - name: string - level: number - gold: number - silver: number - copper: number - zone: string - class_css: string - image_url: string -} - -interface AccountData { - authenticated: boolean - error?: string - user_info: UserInfo - account_status: AccountStatus - dp: number - vp: number - battlepay_credits: number - token_status: string - token_date: string | null - characters: Character[] -} - -function fmtRemaining(secs: number): string { - if (secs <= 0) return '0h 0m 0s' - const h = Math.floor(secs / 3600) - const m = Math.floor((secs % 3600) / 60) - const s = secs % 60 - return `${h}h ${m}m ${s}s` -} - -export function AccountDashboard() { - const [data, setData] = useState(null) - const [error, setError] = useState(null) - const [remaining, setRemaining] = useState(null) - - useEffect(() => { - let cancelled = false - fetch('/api/account/me/', { headers: { Accept: 'application/json' }, credentials: 'same-origin' }) - .then((r) => r.json()) - .then((d: AccountData) => { - if (cancelled) return - if (d.error) setError(d.error) - else { - setData(d) - if (d.account_status?.is_banned && d.account_status.remaining_time) { - setRemaining(d.account_status.remaining_time) - } - } - }) - .catch(() => !cancelled && setError('No se pudieron cargar los datos de la cuenta.')) - return () => { - cancelled = true - } - }, []) - - useEffect(() => { - if (remaining === null) return - if (remaining <= 0) { - window.location.reload() - return - } - const id = setInterval(() => setRemaining((r) => (r === null ? null : r - 1)), 1000) - return () => clearInterval(id) - }, [remaining]) - - if (error) return

{error}

- if (!data) return

Cargando datos de la cuenta…

- - const { user_info: u, account_status: st } = data - - return ( - <> -
- Datos básicos -
-

Cuenta (Battle.net): {u.bnet_email}

-

Cuenta de juego: {u.username}

-

Correo de registro: {u.reg_mail}

-

Correo actual: {u.email}

-

Fecha de registro: {u.joindate}

-

Última IP (web): {u.last_ip}

-

Última IP (Servidor): {u.last_attempt_ip}

-
-
- -
- Estado de la cuenta -
- Nivel 1 -

PD: {data.dp}

-

PV: {data.vp}

-

Créditos Battlepay: {data.battlepay_credits}

-

- Token de Seguridad:{' '} - - {data.token_status === 'Solicitado' ? `${data.token_status} - ${data.token_date}` : data.token_status} - -

- - {st?.is_banned ? ( - <> -

Cuenta baneada:

-

Fin de la suspensión: {st.unban_date}

-

- Tiempo restante: {remaining !== null ? fmtRemaining(remaining) : ''} -

- - ) : ( - <> -

Cuenta baneada: No (Consultar historial)

-

Cuenta reclutada: {st?.is_recruited ? 'Sí' : 'No'}

-

Amigos reclutados: {st?.recruited_count ?? 0}

- - )} -
-
- - {data.characters.length > 0 && ( -
-
-

Mis personajes

-
-
- {data.characters.map((c) => ( -
-
- {c.name} -

{c.name}

-

Nivel {c.level}

-

{c.zone}

-

- - {c.gold} - {' '} - {c.silver} - {' '} - {c.copper} - - -

-
-
- ))} -
-
- )} - - ) -} diff --git a/frontend/src/components/ChangeEmailForm.tsx b/frontend/src/components/ChangeEmailForm.tsx deleted file mode 100644 index 8a96dd2..0000000 --- a/frontend/src/components/ChangeEmailForm.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import { useState } from 'react' - -interface Props { - url: string - csrfToken: string -} - -interface Field { - name: string - type: 'password' | 'email' | 'text' - placeholder: string - maxLength?: number - toggle?: boolean -} - -const FIELDS: Field[] = [ - { name: 'cur-password', type: 'password', placeholder: 'Contraseña actual', maxLength: 16, toggle: true }, - { name: 'cur-email', type: 'email', placeholder: 'Correo actual' }, - { name: 'new-email', type: 'email', placeholder: 'Nuevo correo' }, - { name: 'conf-new-email', type: 'email', placeholder: 'Confirmar nuevo correo' }, - { name: 'security-token', type: 'text', placeholder: 'Token de seguridad', maxLength: 6 }, -] - -export function ChangeEmailForm({ url, csrfToken }: Props) { - const [values, setValues] = useState>({}) - const [showPw, setShowPw] = useState(false) - const [busy, setBusy] = useState(false) - const [message, setMessage] = useState<{ ok: boolean; html: string } | null>(null) - - async function handleSubmit(e: React.FormEvent) { - e.preventDefault() - if (busy) return - setBusy(true) - setMessage(null) - - const body = new URLSearchParams() - for (const f of FIELDS) body.set(f.name, (values[f.name] || '').trim()) - body.set('csrfmiddlewaretoken', csrfToken) - - try { - const resp = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'X-CSRFToken': csrfToken, - Accept: 'application/json', - }, - credentials: 'same-origin', - body: body.toString(), - }) - if (!resp.ok) throw new Error(`HTTP ${resp.status}`) - const data: { success?: boolean; message?: string } = await resp.json() - setMessage({ ok: !!data.success, html: data.message || '' }) - } catch { - setMessage({ ok: false, html: 'Error en el servidor. Inténtalo más tarde.' }) - } finally { - setBusy(false) - } - } - - return ( -
- - - {FIELDS.map((f) => ( - - - - ))} - - - - -
- setValues((v) => ({ ...v, [f.name]: e.target.value }))} - /> - {f.toggle && ( - setShowPw((s) => !s)} - /> - )} -
- -
-
-
- {message && } -
-
- ) -} diff --git a/frontend/src/components/ChangePasswordForm.tsx b/frontend/src/components/ChangePasswordForm.tsx deleted file mode 100644 index b453674..0000000 --- a/frontend/src/components/ChangePasswordForm.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import { useState } from 'react' - -interface Props { - url: string - csrfToken: string - loginUrl: string -} - -interface Field { - key: 'cur' | 'new' | 'conf' | 'token' - name: string - placeholder: string - maxLength: number -} - -const FIELDS: Field[] = [ - { key: 'cur', name: 'cur-password', placeholder: 'Contraseña actual', maxLength: 16 }, - { key: 'new', name: 'new-password', placeholder: 'Contraseña nueva', maxLength: 16 }, - { key: 'conf', name: 'conf-new-password', placeholder: 'Confirmar contraseña nueva', maxLength: 16 }, - { key: 'token', name: 'security-token', placeholder: 'Token de seguridad', maxLength: 6 }, -] - -export function ChangePasswordForm({ url, csrfToken, loginUrl }: Props) { - const [values, setValues] = useState>({}) - const [shown, setShown] = useState>({}) - const [busy, setBusy] = useState(false) - const [done, setDone] = useState(false) - const [message, setMessage] = useState<{ ok: boolean; html: string } | null>(null) - - async function handleSubmit(e: React.FormEvent) { - e.preventDefault() - if (busy || done) return - setBusy(true) - setMessage(null) - - const body = new URLSearchParams() - for (const f of FIELDS) body.set(f.name, (values[f.key] || '').trim()) - body.set('csrfmiddlewaretoken', csrfToken) - - try { - const resp = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'X-CSRFToken': csrfToken, - Accept: 'application/json', - }, - credentials: 'same-origin', - body: body.toString(), - }) - if (!resp.ok) throw new Error(`HTTP ${resp.status}`) - const data: { success?: boolean; message?: string; redirect?: boolean } = await resp.json() - setMessage({ ok: !!data.success, html: data.message || '' }) - if (data.success && data.redirect) { - setDone(true) - setTimeout(() => { - window.location.href = loginUrl - }, 3000) - } else { - setBusy(false) - } - } catch { - setMessage({ ok: false, html: 'Error en el servidor. Inténtalo más tarde.' }) - setBusy(false) - } - } - - return ( - <> -
- - - {FIELDS.map((f) => ( - - - - ))} - - - - -
- setValues((v) => ({ ...v, [f.key]: e.target.value }))} - /> - setShown((s) => ({ ...s, [f.key]: !s[f.key] }))} - /> -
- -
-
-
-
- {message && } -
- - ) -} diff --git a/frontend/src/components/CharacterPurchaseForm.tsx b/frontend/src/components/CharacterPurchaseForm.tsx deleted file mode 100644 index 57e987f..0000000 --- a/frontend/src/components/CharacterPurchaseForm.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { useState } from 'react' -import { redirectToStripeCheckout } from '../stripe' - -interface Props { - url: string - csrfToken: string - characters: string[] - price: string - label: string - /** nombre del campo POST del personaje (por defecto "character") */ - field?: string - /** clase CSS del botón (por defecto "rename-button") */ - buttonClass?: string -} - -/** - * Formulario reutilizable para los servicios de personaje de pago: selector de - * personaje + botón con precio. Al enviar, la vista devuelve un session_id de - * Stripe y redirige al Checkout. Sirve para rename/customize/race/faction/level/ - * gold/transfer/... cambiando solo los data-* de la plantilla. - */ -export function CharacterPurchaseForm({ - url, - csrfToken, - characters, - price, - label, - field = 'character', - buttonClass = 'rename-button', -}: Props) { - const [character, setCharacter] = useState('') - const [busy, setBusy] = useState(false) - const [message, setMessage] = useState<{ ok: boolean; html: string } | null>(null) - - async function handleSubmit(e: React.FormEvent) { - e.preventDefault() - if (busy) return - setBusy(true) - setMessage(null) - - const body = new URLSearchParams() - body.set(field, character) - body.set('csrfmiddlewaretoken', csrfToken) - - try { - const resp = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'X-CSRFToken': csrfToken, - Accept: 'application/json', - }, - credentials: 'same-origin', - body: body.toString(), - }) - if (!resp.ok) throw new Error(`HTTP ${resp.status}`) - const data: { - success?: boolean - message?: string - session_id?: string - stripe_public_key?: string - } = await resp.json() - - if (data.success && data.session_id && data.stripe_public_key) { - await redirectToStripeCheckout(data.stripe_public_key, data.session_id) - return // redirigiendo a Stripe - } - setMessage({ ok: !!data.success, html: data.message || '' }) - setBusy(false) - } catch { - setMessage({ - ok: false, - html: 'Error al iniciar el pago. Inténtalo más tarde.', - }) - setBusy(false) - } - } - - return ( -
-
-

Escoge el personaje

-
-
- -
- -
-
-
-
-
- {message && } -
-
-
-
- ) -} diff --git a/frontend/src/components/GoldForm.tsx b/frontend/src/components/GoldForm.tsx deleted file mode 100644 index 05d3f0d..0000000 --- a/frontend/src/components/GoldForm.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import { useState } from 'react' -import { redirectToStripeCheckout } from '../stripe' - -interface GoldOption { - gold_amount: number - price: string -} - -interface Props { - url: string - csrfToken: string - characters: string[] - options: GoldOption[] -} - -export function GoldForm({ url, csrfToken, characters, options }: Props) { - const [character, setCharacter] = useState('') - const [amount, setAmount] = useState('') - const [busy, setBusy] = useState(false) - const [message, setMessage] = useState<{ ok: boolean; html: string } | null>(null) - - async function handleSubmit(e: React.FormEvent) { - e.preventDefault() - if (busy) return - setBusy(true) - setMessage(null) - - const body = new URLSearchParams() - body.set('character', character) - body.set('gold_amount', amount) - body.set('csrfmiddlewaretoken', csrfToken) - - try { - const resp = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'X-CSRFToken': csrfToken, - Accept: 'application/json', - }, - credentials: 'same-origin', - body: body.toString(), - }) - if (!resp.ok) throw new Error(`HTTP ${resp.status}`) - const data: { success?: boolean; message?: string; session_id?: string; stripe_public_key?: string } = - await resp.json() - if (data.success && data.session_id && data.stripe_public_key) { - await redirectToStripeCheckout(data.stripe_public_key, data.session_id) - return - } - setMessage({ ok: !!data.success, html: data.message || '' }) - setBusy(false) - } catch { - setMessage({ ok: false, html: 'Error al iniciar el pago. Inténtalo más tarde.' }) - setBusy(false) - } - } - - return ( -
-
- -
- -
- -
-
-
-
- {message && } -
-
-
- ) -} diff --git a/frontend/src/components/GuildRenameForm.tsx b/frontend/src/components/GuildRenameForm.tsx deleted file mode 100644 index a5206e8..0000000 --- a/frontend/src/components/GuildRenameForm.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import { useState } from 'react' -import { redirectToStripeCheckout } from '../stripe' - -interface Props { - url: string - csrfToken: string - guilds: string[] -} - -export function GuildRenameForm({ url, csrfToken, guilds }: Props) { - const [oldName, setOldName] = useState('') - const [newName, setNewName] = useState('') - const [confName, setConfName] = useState('') - const [password, setPassword] = useState('') - const [token, setToken] = useState('') - const [busy, setBusy] = useState(false) - const [message, setMessage] = useState<{ ok: boolean; html: string } | null>(null) - - async function handleSubmit(e: React.FormEvent) { - e.preventDefault() - if (busy) return - setBusy(true) - setMessage(null) - - const body = new URLSearchParams() - body.set('old-guild-name', oldName) - body.set('new-guild-name', newName.trim()) - body.set('conf-new-guild-name', confName.trim()) - body.set('cur-password', password.trim()) - body.set('security-token', token.trim()) - body.set('csrfmiddlewaretoken', csrfToken) - - try { - const resp = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'X-CSRFToken': csrfToken, - Accept: 'application/json', - }, - credentials: 'same-origin', - body: body.toString(), - }) - if (!resp.ok) throw new Error(`HTTP ${resp.status}`) - const data: { success?: boolean; message?: string; session_id?: string; stripe_public_key?: string } = - await resp.json() - if (data.success && data.session_id && data.stripe_public_key) { - await redirectToStripeCheckout(data.stripe_public_key, data.session_id) - return - } - setMessage({ ok: !!data.success, html: data.message || '' }) - setBusy(false) - } catch { - setMessage({ ok: false, html: 'Error al iniciar el pago. Inténtalo más tarde.' }) - setBusy(false) - } - } - - return ( - <> -
- - - - - - - - - - - - - - - - - - - - - -
- -
- setNewName(e.target.value)} required /> -
- setConfName(e.target.value)} required /> -
- setPassword(e.target.value)} required /> -
- setToken(e.target.value)} required /> -
- -
-
-
-
- {message && } -
- - ) -} diff --git a/frontend/src/components/HistoryTable.tsx b/frontend/src/components/HistoryTable.tsx deleted file mode 100644 index bd976bf..0000000 --- a/frontend/src/components/HistoryTable.tsx +++ /dev/null @@ -1,42 +0,0 @@ -interface Column { - key: string - label: string -} - -interface Props { - columns: Column[] - rows: Record[] - emptyText: string -} - -/** Tabla genérica de solo lectura para los historiales (seguridad, transacciones, baneos). */ -export function HistoryTable({ columns, rows, emptyText }: Props) { - return ( - - - - {columns.map((c) => ( - - ))} - - - - {rows.length > 0 ? ( - rows.map((row, i) => ( - - {columns.map((c) => ( - - ))} - - )) - ) : ( - - - - )} - -
{c.label}
{String(row[c.key] ?? '')}
- {emptyText} -
- ) -} diff --git a/frontend/src/components/LoginForm.tsx b/frontend/src/components/LoginForm.tsx deleted file mode 100644 index 70a9d27..0000000 --- a/frontend/src/components/LoginForm.tsx +++ /dev/null @@ -1,158 +0,0 @@ -import { useState } from 'react' -import { useTurnstile } from '../turnstile' - -interface Props { - loginUrl: string - successUrl: string - csrfToken: string - siteKey: string -} - -type LoginResponse = { - success?: boolean - alert?: boolean - locked?: boolean - error?: string -} - -export function LoginForm({ loginUrl, successUrl, csrfToken, siteKey }: Props) { - const [email, setEmail] = useState('') - const [password, setPassword] = useState('') - const [showPassword, setShowPassword] = useState(false) - const [buttonText, setButtonText] = useState('Conectar') - const [busy, setBusy] = useState(false) - const [message, setMessage] = useState<{ kind: 'ok' | 'err'; text: string } | null>(null) - const { ref: turnstileRef, token: captchaToken } = useTurnstile(siteKey) - - function transientError(text: string) { - setMessage({ kind: 'err', text }) - setBusy(false) - setButtonText('Conectar') - setTimeout(() => setMessage(null), 5000) - } - - async function handleSubmit(e: React.FormEvent) { - e.preventDefault() - if (busy) return - - if (email.trim() === '' || password.trim() === '') { - transientError('Por favor rellene todos los campos.') - return - } - - setBusy(true) - setButtonText('Conectando') - setMessage(null) - - const body = new URLSearchParams() - body.set('email', email.trim()) - body.set('password', password.trim()) - body.set('csrfmiddlewaretoken', csrfToken) - body.set('cf-turnstile-response', captchaToken) - - try { - const resp = await fetch(loginUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'X-CSRFToken': csrfToken, - Accept: 'application/json', - }, - credentials: 'same-origin', - body: body.toString(), - }) - if (!resp.ok) throw new Error(`HTTP ${resp.status}`) - const data: LoginResponse = await resp.json() - - if (data.success === true) { - setButtonText('Conectado') - setMessage({ kind: 'ok', text: 'Conexión exitosa. Redirigiendo…' }) - setTimeout(() => { - window.location.href = successUrl - }, 3000) - } else if (data.alert === true) { - setButtonText('Sancionado') - setBusy(false) - } else if (data.locked === true) { - setButtonText('Seguridad') - setBusy(false) - } else { - transientError(data.error || 'Nombre de usuario / Contraseña incorrectos.') - } - } catch { - setTimeout(() => { - alert('Algo ha salido mal. Por favor intente más tarde') - window.location.reload() - }, 2000) - } - } - - return ( - <> -
- - - - - - - - - - - - - - - -
- setEmail(e.target.value)} - /> -
- setPassword(e.target.value)} - /> - setShowPassword((v) => !v)} - /> -
-
-
- -
-
-
-
- {message && ( - - {message.text} - - )} -
- - ) -} diff --git a/frontend/src/components/NewsList.tsx b/frontend/src/components/NewsList.tsx deleted file mode 100644 index 6ec3d14..0000000 --- a/frontend/src/components/NewsList.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { useEffect, useState } from 'react' -import type { Noticia } from '../types' - -function formatFecha(iso: string | null): string { - if (!iso) return '' - const d = new Date(iso) - if (isNaN(d.getTime())) return iso - return d.toLocaleString('es-ES', { - day: '2-digit', - month: '2-digit', - year: 'numeric', - hour: '2-digit', - minute: '2-digit', - }) -} - -export function NewsList() { - const [noticias, setNoticias] = useState(null) - const [error, setError] = useState(null) - - useEffect(() => { - let cancelled = false - fetch('/api/home/news/', { headers: { Accept: 'application/json' } }) - .then((r) => { - if (!r.ok) throw new Error(`HTTP ${r.status}`) - return r.json() - }) - .then((data: { noticias: Noticia[] }) => { - if (!cancelled) setNoticias(data.noticias) - }) - .catch((e) => { - if (!cancelled) setError(String(e)) - }) - return () => { - cancelled = true - } - }, []) - - if (error) { - return

No se pudieron cargar las noticias.

- } - if (noticias === null) { - return

Cargando noticias…

- } - if (noticias.length === 0) { - return

No hay noticias disponibles en este momento.

- } - - return ( - <> - {noticias.map((n) => ( -
-

{n.titulo}

- {formatFecha(n.fecha)} -
- {n.enlace && ( -

- Enlace:{' '} - - aquí - -

- )} -
-
-
-
- ))} -

- * Mostrando las últimas 50 noticias -

-
- - ) -} diff --git a/frontend/src/components/RecoverForm.tsx b/frontend/src/components/RecoverForm.tsx deleted file mode 100644 index b8a58c7..0000000 --- a/frontend/src/components/RecoverForm.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { useState } from 'react' -import { useTurnstile } from '../turnstile' - -interface Props { - recoverUrl: string - csrfToken: string - siteKey: string -} - -const OPTIONS = [ - { value: 'password', label: 'Contraseña' }, - { value: 'accountname', label: 'Nombre de cuenta' }, - { value: 'activation', label: 'Enlace de activación' }, -] as const - -export function RecoverForm({ recoverUrl, csrfToken, siteKey }: Props) { - const { ref: turnstileRef, token: captchaToken } = useTurnstile(siteKey) - const [type, setType] = useState('password') - const [email, setEmail] = useState('') - const [busy, setBusy] = useState(false) - const [message, setMessage] = useState<{ ok: boolean; text: string } | null>(null) - - async function handleSubmit(e: React.FormEvent) { - e.preventDefault() - if (busy) return - setBusy(true) - setMessage(null) - - const body = new URLSearchParams() - body.set('type', type) - body.set('email', email.trim()) - body.set('csrfmiddlewaretoken', csrfToken) - body.set('cf-turnstile-response', captchaToken) - - try { - const resp = await fetch(recoverUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'X-CSRFToken': csrfToken, - Accept: 'application/json', - }, - credentials: 'same-origin', - body: body.toString(), - }) - if (!resp.ok) throw new Error(`HTTP ${resp.status}`) - const data: { success?: boolean; message?: string } = await resp.json() - setMessage({ ok: !!data.success, text: data.message || '' }) - } catch { - setMessage({ ok: false, text: 'Error en el servidor. Inténtalo de nuevo más tarde.' }) - } finally { - setBusy(false) - } - } - - return ( - <> -

Escoge la opción acorde a la información que quieres recuperar

-
-
- - - - - - - - - - - - - -
- setEmail(e.target.value)} - /> -
-
-
- -
-
-
-
- {message && ( - {message.text} - )} -
- - ) -} diff --git a/frontend/src/components/RegisterForm.tsx b/frontend/src/components/RegisterForm.tsx deleted file mode 100644 index 37c8c9c..0000000 --- a/frontend/src/components/RegisterForm.tsx +++ /dev/null @@ -1,215 +0,0 @@ -import { useState } from 'react' -import { useTurnstile } from '../turnstile' - -interface Props { - registerUrl: string - csrfToken: string - siteKey: string -} - -type RegisterResponse = { - success?: boolean - message?: string -} - -export function RegisterForm({ registerUrl, csrfToken, siteKey }: Props) { - const { ref: turnstileRef, token: captchaToken } = useTurnstile(siteKey) - const [password, setPassword] = useState('') - const [confPassword, setConfPassword] = useState('') - const [email, setEmail] = useState('') - const [confEmail, setConfEmail] = useState('') - const [recruiter, setRecruiter] = useState('') - const [accepted, setAccepted] = useState(false) - const [showPw, setShowPw] = useState(false) - const [showConfPw, setShowConfPw] = useState(false) - const [busy, setBusy] = useState(false) - const [response, setResponse] = useState<{ ok: boolean; html: string } | null>(null) - - async function handleSubmit(e: React.FormEvent) { - e.preventDefault() - if (busy || !accepted) return - - setBusy(true) - setResponse(null) - - const body = new URLSearchParams() - body.set('password', password.trim()) - body.set('conf-password', confPassword.trim()) - body.set('email', email.trim()) - body.set('conf-email', confEmail.trim()) - body.set('recruiter', recruiter.trim()) - body.set('csrfmiddlewaretoken', csrfToken) - body.set('cf-turnstile-response', captchaToken) - - try { - const resp = await fetch(registerUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'X-CSRFToken': csrfToken, - Accept: 'application/json', - }, - credentials: 'same-origin', - body: body.toString(), - }) - if (!resp.ok) throw new Error(`HTTP ${resp.status}`) - const data: RegisterResponse = await resp.json() - const msg = data.message ?? '' - // El mensaje de éxito ya viene como HTML; el de error es texto. - setResponse({ - ok: !!data.success, - html: data.success ? msg : `${msg}`, - }) - } catch { - setResponse({ - ok: false, - html: 'Error en el servidor. Inténtalo de nuevo más tarde.', - }) - } finally { - setBusy(false) - } - } - - return ( - <> -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- setPassword(e.target.value)} - /> - setShowPw((v) => !v)} - /> -
- setConfPassword(e.target.value)} - /> - setShowConfPw((v) => !v)} - /> -
- setEmail(e.target.value)} - /> -
- setConfEmail(e.target.value)} - /> -
-
-
Opcional: Sólo si eres reclutado por un amigo
- setRecruiter(e.target.value)} - /> -
- setAccepted(e.target.checked)} - /> - -
-
-
- -
-
-
-
- {response && } -
- - ) -} diff --git a/frontend/src/components/ResetPassword.tsx b/frontend/src/components/ResetPassword.tsx deleted file mode 100644 index add5dd4..0000000 --- a/frontend/src/components/ResetPassword.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import { useState } from 'react' - -interface Props { - valid: boolean - token: string - resetUrl: string - recoverUrl: string - csrfToken: string -} - -export function ResetPassword({ valid, token, resetUrl, recoverUrl, csrfToken }: Props) { - const [password, setPassword] = useState('') - const [confPassword, setConfPassword] = useState('') - const [showPw, setShowPw] = useState(false) - const [busy, setBusy] = useState(false) - const [message, setMessage] = useState<{ ok: boolean; text: string } | null>(null) - const [done, setDone] = useState(false) - - if (!valid) { - return ( -
-

El enlace no es válido o ha caducado.

-
-

- Puedes solicitar uno nuevo en recuperar cuenta. -

-
- ) - } - - async function handleSubmit(e: React.FormEvent) { - e.preventDefault() - if (busy || done) return - if (password.trim() === '' || password.trim() !== confPassword.trim()) { - setMessage({ ok: false, text: 'Las contraseñas no coinciden.' }) - return - } - setBusy(true) - setMessage(null) - - const body = new URLSearchParams() - body.set('token', token) - body.set('new-password', password.trim()) - body.set('conf-password', confPassword.trim()) - body.set('csrfmiddlewaretoken', csrfToken) - - try { - const resp = await fetch(resetUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'X-CSRFToken': csrfToken, - Accept: 'application/json', - }, - credentials: 'same-origin', - body: body.toString(), - }) - if (!resp.ok) throw new Error(`HTTP ${resp.status}`) - const data: { success?: boolean; message?: string; redirect?: string } = await resp.json() - setMessage({ ok: !!data.success, text: data.message || '' }) - if (data.success) { - setDone(true) - if (data.redirect) { - setTimeout(() => { - window.location.href = data.redirect as string - }, 2500) - } - } else { - setBusy(false) - } - } catch { - setMessage({ ok: false, text: 'Error en el servidor. Inténtalo de nuevo más tarde.' }) - setBusy(false) - } - } - - return ( -
-
- - - - - - - - - - - - -
- setPassword(e.target.value)} - /> - setShowPw((v) => !v)} - /> -
- setConfPassword(e.target.value)} - /> -
- -
-
-
-
- {message && ( - {message.text} - )} -
-
- ) -} diff --git a/frontend/src/components/RestoreCharacterForm.tsx b/frontend/src/components/RestoreCharacterForm.tsx deleted file mode 100644 index e49b1f6..0000000 --- a/frontend/src/components/RestoreCharacterForm.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { useState } from 'react' - -interface DeletedChar { - guid: number - name: string - level: number -} - -interface Props { - url: string - csrfToken: string - characters: DeletedChar[] -} - -export function RestoreCharacterForm({ url, csrfToken, characters }: Props) { - const [guid, setGuid] = useState('') - const [busy, setBusy] = useState(false) - const [done, setDone] = useState(false) - const [message, setMessage] = useState<{ ok: boolean; html: string } | null>(null) - - async function handleSubmit(e: React.FormEvent) { - e.preventDefault() - if (busy || done || !guid) return - setBusy(true) - setMessage(null) - - const body = new URLSearchParams() - body.set('guid', guid) - body.set('csrfmiddlewaretoken', csrfToken) - - try { - const resp = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'X-CSRFToken': csrfToken, - Accept: 'application/json', - }, - credentials: 'same-origin', - body: body.toString(), - }) - if (!resp.ok) throw new Error(`HTTP ${resp.status}`) - const data: { success?: boolean; message?: string; redirect?: string } = await resp.json() - setMessage({ ok: !!data.success, html: data.message || '' }) - if (data.success && data.redirect) { - setDone(true) - setTimeout(() => { - window.location.href = data.redirect as string - }, 2000) - } else { - setBusy(false) - } - } catch { - setMessage({ ok: false, html: 'Error en el servidor. Inténtalo más tarde.' }) - setBusy(false) - } - } - - return ( - <> -
- -
- -
-
- {message && } -
- - ) -} diff --git a/frontend/src/components/RestoreItemsForm.tsx b/frontend/src/components/RestoreItemsForm.tsx deleted file mode 100644 index 94b085c..0000000 --- a/frontend/src/components/RestoreItemsForm.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import { useState } from 'react' - -interface DeletedItem { - recover_id: string - item_name: string - item_id: string - count: string -} - -interface Props { - url: string - csrfToken: string - characters: string[] -} - -export function RestoreItemsForm({ url, csrfToken, characters }: Props) { - const [character, setCharacter] = useState('') - const [items, setItems] = useState(null) - const [recoverId, setRecoverId] = useState('') - const [busy, setBusy] = useState(false) - const [message, setMessage] = useState<{ ok: boolean; html: string } | null>(null) - - async function doSearch(char: string, keepMessage = false) { - if (!char) return - setBusy(true) - if (!keepMessage) setMessage(null) - setItems(null) - setRecoverId('') - try { - const r = await fetch(`${url}?character_name=${encodeURIComponent(char)}`, { - headers: { 'X-Requested-With': 'XMLHttpRequest', Accept: 'application/json' }, - credentials: 'same-origin', - }) - const d: { items?: DeletedItem[]; error?: string } = await r.json() - if (d.error) setMessage({ ok: false, html: `${d.error}` }) - else setItems(d.items || []) - } catch { - setMessage({ ok: false, html: 'Error al buscar los ítems.' }) - } finally { - setBusy(false) - } - } - - function search(e: React.FormEvent) { - e.preventDefault() - if (busy) return - void doSearch(character) - } - - async function recover(e: React.FormEvent) { - e.preventDefault() - if (!recoverId || busy) return - setBusy(true) - setMessage(null) - const body = new URLSearchParams() - body.set('recover_id', recoverId) - body.set('character_name', character) - body.set('csrfmiddlewaretoken', csrfToken) - try { - const r = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'X-Requested-With': 'XMLHttpRequest', - 'X-CSRFToken': csrfToken, - Accept: 'application/json', - }, - credentials: 'same-origin', - body: body.toString(), - }) - const d: { success?: boolean; message?: string } = await r.json() - setMessage({ ok: !!d.success, html: d.message || '' }) - if (d.success) { - // refrescar la lista de ítems tras recuperar uno (conservando el mensaje) - void doSearch(character, true) - } - } catch { - setMessage({ ok: false, html: 'Error al recuperar el ítem.' }) - } finally { - setBusy(false) - } - } - - return ( - <> -
- - -
- - {items !== null && ( - items.length > 0 ? ( -
- - - -
- ) : ( -

No hay ítems eliminados para este personaje.

- ) - )} - -
- {message && } -
- - ) -} diff --git a/frontend/src/components/SecurityTokenForm.tsx b/frontend/src/components/SecurityTokenForm.tsx deleted file mode 100644 index 1deed41..0000000 --- a/frontend/src/components/SecurityTokenForm.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { useState } from 'react' - -interface Props { - url: string - csrfToken: string - initialDate: string -} - -export function SecurityTokenForm({ url, csrfToken, initialDate }: Props) { - const [date, setDate] = useState(initialDate) - const [busy, setBusy] = useState(false) - const [message, setMessage] = useState<{ ok: boolean; html: string } | null>(null) - - async function handleSubmit(e: React.FormEvent) { - e.preventDefault() - if (busy) return - setBusy(true) - setMessage(null) - - const body = new URLSearchParams() - body.set('csrfmiddlewaretoken', csrfToken) - - try { - const resp = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'X-CSRFToken': csrfToken, - Accept: 'application/json', - }, - credentials: 'same-origin', - body: body.toString(), - }) - if (!resp.ok) throw new Error(`HTTP ${resp.status}`) - const data: { success?: boolean; message?: string; token_date?: string } = await resp.json() - setMessage({ ok: !!data.success, html: data.message || '' }) - if (data.success && data.token_date) setDate(data.token_date) - } catch { - setMessage({ ok: false, html: 'Error en el servidor. Inténtalo más tarde.' }) - } finally { - setBusy(false) - } - } - - return ( - <> -

Fecha de solicitud de Token de seguridad:

-

{date}

-
-
- -
-
-
-
- {message && } -
- - ) -} diff --git a/frontend/src/components/SelectAccount.tsx b/frontend/src/components/SelectAccount.tsx deleted file mode 100644 index d10d95b..0000000 --- a/frontend/src/components/SelectAccount.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { useState } from 'react' - -export interface GameAccount { - id: number - username: string - index: number -} - -interface Props { - accounts: GameAccount[] - selectUrl: string -} - -export function SelectAccount({ accounts, selectUrl }: Props) { - const [busy, setBusy] = useState(null) - const [error, setError] = useState(null) - - async function select(id: number) { - if (busy !== null) return - setBusy(id) - setError(null) - try { - const r = await fetch(`${selectUrl}?account_id=${id}`, { - headers: { Accept: 'application/json' }, - credentials: 'same-origin', - }) - const d: { success?: boolean; redirect?: string; error?: string } = await r.json() - if (d.success && d.redirect) { - window.location.href = d.redirect - } else { - setError(d.error || 'No se pudo seleccionar la cuenta.') - setBusy(null) - } - } catch { - setError('Error de red. Inténtalo de nuevo.') - setBusy(null) - } - } - - return ( - - - {accounts.map((a) => ( - - - - ))} - {error && ( - - - - )} - -
- -
- {error} -
- ) -} diff --git a/frontend/src/components/ServerStatus.tsx b/frontend/src/components/ServerStatus.tsx deleted file mode 100644 index 0bee0f0..0000000 --- a/frontend/src/components/ServerStatus.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { useEffect, useState } from 'react' -import type { ServerStatusData } from '../types' - -function useClock(): string { - const [time, setTime] = useState(() => new Date().toLocaleTimeString('es-ES')) - useEffect(() => { - const id = setInterval(() => setTime(new Date().toLocaleTimeString('es-ES')), 1000) - return () => clearInterval(id) - }, []) - return time -} - -export function ServerStatus() { - const [data, setData] = useState(null) - const clock = useClock() - - useEffect(() => { - let cancelled = false - fetch('/api/home/status/', { headers: { Accept: 'application/json' } }) - .then((r) => { - if (!r.ok) throw new Error(`HTTP ${r.status}`) - return r.json() - }) - .then((d: ServerStatusData) => { - if (!cancelled) setData(d) - }) - .catch(() => { - if (!cancelled) setData(null) - }) - return () => { - cancelled = true - } - }, []) - - const online = data ? data.online_characters : 0 - const status = data ? data.status : 'Offline' - const loginClass = status === 'Online' ? 'green-info' : 'red-info' - - return ( - - - - - - - - - - - - - - - - - - - - - -
- {data?.server_name ?? '—'}{' '} - {data?.expansion ?? ''} - {online}
Login{status}
Hora - {clock} -
-
-
-

- {data?.address ?? ''} -

-
- ) -} diff --git a/frontend/src/components/TransferForm.tsx b/frontend/src/components/TransferForm.tsx deleted file mode 100644 index c62d6a2..0000000 --- a/frontend/src/components/TransferForm.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import { useState } from 'react' -import { redirectToStripeCheckout } from '../stripe' - -interface Props { - url: string - csrfToken: string - characters: string[] - price: string -} - -export function TransferForm({ url, csrfToken, characters, price }: Props) { - const [character, setCharacter] = useState('') - const [destination, setDestination] = useState('') - const [token, setToken] = useState('') - const [busy, setBusy] = useState(false) - const [message, setMessage] = useState<{ ok: boolean; html: string } | null>(null) - - async function handleSubmit(e: React.FormEvent) { - e.preventDefault() - if (busy) return - setBusy(true) - setMessage(null) - - const body = new URLSearchParams() - body.set('character', character) - body.set('destination_account', destination.trim()) - body.set('security_token', token.trim()) - body.set('csrfmiddlewaretoken', csrfToken) - - try { - const resp = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'X-CSRFToken': csrfToken, - Accept: 'application/json', - }, - credentials: 'same-origin', - body: body.toString(), - }) - if (!resp.ok) throw new Error(`HTTP ${resp.status}`) - const data: { success?: boolean; message?: string; session_id?: string; stripe_public_key?: string } = - await resp.json() - if (data.success && data.session_id && data.stripe_public_key) { - await redirectToStripeCheckout(data.stripe_public_key, data.session_id) - return - } - setMessage({ ok: !!data.success, html: data.message || '' }) - setBusy(false) - } catch { - setMessage({ ok: false, html: 'Error al iniciar el pago. Inténtalo más tarde.' }) - setBusy(false) - } - } - - return ( -
-
- -
- setDestination(e.target.value)} - /> -
- setToken(e.target.value)} - /> -
- -
-
-
-
- {message && } -
-
-
- ) -} diff --git a/frontend/src/components/VotePanel.tsx b/frontend/src/components/VotePanel.tsx deleted file mode 100644 index 39dbcc1..0000000 --- a/frontend/src/components/VotePanel.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import { useState } from 'react' - -interface VoteSite { - name: string - image_url: string - points: number - url: string -} - -interface Props { - voteUrl: string - csrfToken: string - sites: VoteSite[] -} - -export function VotePanel({ voteUrl, csrfToken, sites }: Props) { - const [busyUrl, setBusyUrl] = useState(null) - const [message, setMessage] = useState<{ ok: boolean; html: string } | null>(null) - - async function vote(url: string) { - if (busyUrl) return - // Abrir el sitio de votación en otra pestaña - window.open(url, '_blank', 'noopener,noreferrer') - setBusyUrl(url) - setMessage(null) - - const body = new URLSearchParams() - body.set('vote', url) - body.set('csrfmiddlewaretoken', csrfToken) - - try { - const resp = await fetch(voteUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'X-CSRFToken': csrfToken, - Accept: 'application/json', - }, - credentials: 'same-origin', - body: body.toString(), - }) - if (!resp.ok) throw new Error(`HTTP ${resp.status}`) - const data: { success?: boolean; message?: string } = await resp.json() - // El backend manda mensajes con o sin HTML; los envolvemos en verde si es éxito. - const html = data.success ? `${data.message ?? ''}` : (data.message ?? '') - setMessage({ ok: !!data.success, html }) - } catch { - setMessage({ ok: false, html: 'Error al registrar el voto. Inténtalo más tarde.' }) - } finally { - setBusyUrl(null) - } - } - - return ( - <> -

- Nota: Gtop100 puede demorar unos minutos en acreditar el voto. -

-
-
- {sites.map((s) => ( -
- - - - - - - - - - - - - - - -
- {s.name} -
{s.name}
- PV: {s.points} -
- -
-
- ))} -
-
-
- {message && } -
- - ) -} diff --git a/frontend/src/entries/change_email.tsx b/frontend/src/entries/change_email.tsx deleted file mode 100644 index 269e5fa..0000000 --- a/frontend/src/entries/change_email.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import { ChangeEmailForm } from '../components/ChangeEmailForm' - -const el = document.getElementById('change-email-app') -if (el) { - createRoot(el).render( - - - , - ) -} diff --git a/frontend/src/entries/change_password.tsx b/frontend/src/entries/change_password.tsx deleted file mode 100644 index b6c5a67..0000000 --- a/frontend/src/entries/change_password.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import { ChangePasswordForm } from '../components/ChangePasswordForm' - -const el = document.getElementById('change-password-app') -if (el) { - createRoot(el).render( - - - , - ) -} diff --git a/frontend/src/entries/character_purchase.tsx b/frontend/src/entries/character_purchase.tsx deleted file mode 100644 index cc64cf5..0000000 --- a/frontend/src/entries/character_purchase.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import { CharacterPurchaseForm } from '../components/CharacterPurchaseForm' - -// Entry genérico para todos los servicios de personaje de pago. La plantilla -// configura el formulario con data-* y una lista de personajes en json_script. -const el = document.getElementById('character-purchase-app') -if (el) { - const dataEl = document.getElementById('character-purchase-data') - const characters: string[] = dataEl ? JSON.parse(dataEl.textContent || '[]') : [] - createRoot(el).render( - - - , - ) -} diff --git a/frontend/src/entries/gold_character.tsx b/frontend/src/entries/gold_character.tsx deleted file mode 100644 index 55c0415..0000000 --- a/frontend/src/entries/gold_character.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import { GoldForm } from '../components/GoldForm' - -const el = document.getElementById('gold-app') -if (el) { - const cEl = document.getElementById('gold-characters-data') - const oEl = document.getElementById('gold-options-data') - const characters: string[] = cEl ? JSON.parse(cEl.textContent || '[]') : [] - const options = oEl ? JSON.parse(oEl.textContent || '[]') : [] - createRoot(el).render( - - - , - ) -} diff --git a/frontend/src/entries/history_table.tsx b/frontend/src/entries/history_table.tsx deleted file mode 100644 index 01ba4d2..0000000 --- a/frontend/src/entries/history_table.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import { HistoryTable } from '../components/HistoryTable' - -// Entry genérico para los historiales de solo lectura. La plantilla define las -// columnas en data-columns (JSON) y las filas en un json_script. -const el = document.getElementById('history-table-app') -if (el) { - const dEl = document.getElementById('history-table-data') - const rows = dEl ? JSON.parse(dEl.textContent || '[]') : [] - const columns = JSON.parse(el.dataset.columns || '[]') - createRoot(el).render( - - - , - ) -} diff --git a/frontend/src/entries/home.tsx b/frontend/src/entries/home.tsx deleted file mode 100644 index f5340cb..0000000 --- a/frontend/src/entries/home.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import { NewsList } from '../components/NewsList' -import { ServerStatus } from '../components/ServerStatus' - -// Islas de la portada: cada componente React se monta donde Django deja su -// contenedor. El resto de la página sigue siendo Django. -function mount(id: string, node: React.ReactElement) { - const el = document.getElementById(id) - if (el) { - createRoot(el).render({node}) - } -} - -mount('home-news-app', ) -mount('home-status-app', ) diff --git a/frontend/src/entries/login.tsx b/frontend/src/entries/login.tsx deleted file mode 100644 index c8e54bf..0000000 --- a/frontend/src/entries/login.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import { LoginForm } from '../components/LoginForm' - -const el = document.getElementById('login-app') -if (el) { - createRoot(el).render( - - - , - ) -} diff --git a/frontend/src/entries/my_account.tsx b/frontend/src/entries/my_account.tsx deleted file mode 100644 index def06da..0000000 --- a/frontend/src/entries/my_account.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import { AccountDashboard } from '../components/AccountDashboard' - -const el = document.getElementById('account-dashboard-app') -if (el) { - createRoot(el).render( - - - , - ) -} diff --git a/frontend/src/entries/recover.tsx b/frontend/src/entries/recover.tsx deleted file mode 100644 index a21ec79..0000000 --- a/frontend/src/entries/recover.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import { RecoverForm } from '../components/RecoverForm' - -const el = document.getElementById('recover-app') -if (el) { - createRoot(el).render( - - - , - ) -} diff --git a/frontend/src/entries/register.tsx b/frontend/src/entries/register.tsx deleted file mode 100644 index f1f3225..0000000 --- a/frontend/src/entries/register.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import { RegisterForm } from '../components/RegisterForm' - -const el = document.getElementById('register-app') -if (el) { - createRoot(el).render( - - - , - ) -} diff --git a/frontend/src/entries/rename_guild.tsx b/frontend/src/entries/rename_guild.tsx deleted file mode 100644 index 8ad5550..0000000 --- a/frontend/src/entries/rename_guild.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import { GuildRenameForm } from '../components/GuildRenameForm' - -const el = document.getElementById('rename-guild-app') -if (el) { - const dEl = document.getElementById('rename-guild-data') - const guilds: string[] = dEl ? JSON.parse(dEl.textContent || '[]') : [] - createRoot(el).render( - - - , - ) -} diff --git a/frontend/src/entries/reset_password.tsx b/frontend/src/entries/reset_password.tsx deleted file mode 100644 index 5b99c09..0000000 --- a/frontend/src/entries/reset_password.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import { ResetPassword } from '../components/ResetPassword' - -const el = document.getElementById('reset-password-app') -if (el) { - createRoot(el).render( - - - , - ) -} diff --git a/frontend/src/entries/restore_character.tsx b/frontend/src/entries/restore_character.tsx deleted file mode 100644 index 8ebeed4..0000000 --- a/frontend/src/entries/restore_character.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import { RestoreCharacterForm } from '../components/RestoreCharacterForm' - -const el = document.getElementById('restore-character-app') -if (el) { - const dEl = document.getElementById('restore-character-data') - const characters = dEl ? JSON.parse(dEl.textContent || '[]') : [] - createRoot(el).render( - - - , - ) -} diff --git a/frontend/src/entries/restore_items.tsx b/frontend/src/entries/restore_items.tsx deleted file mode 100644 index 0285aa0..0000000 --- a/frontend/src/entries/restore_items.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import { RestoreItemsForm } from '../components/RestoreItemsForm' - -const el = document.getElementById('restore-items-app') -if (el) { - const dEl = document.getElementById('restore-items-data') - const characters: string[] = dEl ? JSON.parse(dEl.textContent || '[]') : [] - createRoot(el).render( - - - , - ) -} diff --git a/frontend/src/entries/security_token.tsx b/frontend/src/entries/security_token.tsx deleted file mode 100644 index 09f56a6..0000000 --- a/frontend/src/entries/security_token.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import { SecurityTokenForm } from '../components/SecurityTokenForm' - -const el = document.getElementById('security-token-app') -if (el) { - createRoot(el).render( - - - , - ) -} diff --git a/frontend/src/entries/select_account.tsx b/frontend/src/entries/select_account.tsx deleted file mode 100644 index 6687737..0000000 --- a/frontend/src/entries/select_account.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import { SelectAccount, type GameAccount } from '../components/SelectAccount' - -const dataEl = document.getElementById('select-account-data') -const accounts: GameAccount[] = dataEl ? JSON.parse(dataEl.textContent || '[]') : [] - -const el = document.getElementById('select-account-app') -if (el) { - createRoot(el).render( - - - , - ) -} diff --git a/frontend/src/entries/transfer_character.tsx b/frontend/src/entries/transfer_character.tsx deleted file mode 100644 index 5092a9f..0000000 --- a/frontend/src/entries/transfer_character.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import { TransferForm } from '../components/TransferForm' - -const el = document.getElementById('transfer-app') -if (el) { - const cEl = document.getElementById('transfer-characters-data') - const characters: string[] = cEl ? JSON.parse(cEl.textContent || '[]') : [] - createRoot(el).render( - - - , - ) -} diff --git a/frontend/src/entries/vote_points.tsx b/frontend/src/entries/vote_points.tsx deleted file mode 100644 index e5582bc..0000000 --- a/frontend/src/entries/vote_points.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import { VotePanel } from '../components/VotePanel' - -const el = document.getElementById('vote-points-app') -if (el) { - const dEl = document.getElementById('vote-points-data') - const sites = dEl ? JSON.parse(dEl.textContent || '[]') : [] - createRoot(el).render( - - - , - ) -} diff --git a/frontend/src/stripe.ts b/frontend/src/stripe.ts deleted file mode 100644 index b200ce0..0000000 --- a/frontend/src/stripe.ts +++ /dev/null @@ -1,47 +0,0 @@ -interface StripeInstance { - redirectToCheckout: (opts: { sessionId: string }) => Promise<{ error?: { message: string } }> -} - -declare global { - interface Window { - Stripe?: (key: string) => StripeInstance - } -} - -const SCRIPT_ID = 'stripe-js' - -/** - * Carga Stripe.js (una vez) y redirige al Checkout con el session_id dado. - * Rechaza si Stripe no carga o devuelve error. - */ -export function redirectToStripeCheckout(publicKey: string, sessionId: string): Promise { - return new Promise((resolve, reject) => { - function go() { - if (!window.Stripe) return reject(new Error('Stripe no está disponible')) - const stripe = window.Stripe(publicKey) - stripe - .redirectToCheckout({ sessionId }) - .then((r) => (r.error ? reject(new Error(r.error.message)) : resolve())) - .catch(reject) - } - - if (window.Stripe) return go() - - if (!document.getElementById(SCRIPT_ID)) { - const s = document.createElement('script') - s.id = SCRIPT_ID - s.src = 'https://js.stripe.com/v3/' - document.head.appendChild(s) - } - let waited = 0 - const iv = window.setInterval(() => { - if (window.Stripe) { - window.clearInterval(iv) - go() - } else if ((waited += 150) > 8000) { - window.clearInterval(iv) - reject(new Error('No se pudo cargar Stripe.')) - } - }, 150) - }) -} diff --git a/frontend/src/turnstile.ts b/frontend/src/turnstile.ts deleted file mode 100644 index ca34c50..0000000 --- a/frontend/src/turnstile.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { useEffect, useRef, useState } from 'react' - -interface TurnstileOptions { - sitekey: string - callback?: (token: string) => void - 'error-callback'?: () => void - 'expired-callback'?: () => void - theme?: 'auto' | 'light' | 'dark' -} - -declare global { - interface Window { - turnstile?: { - render: (el: HTMLElement, opts: TurnstileOptions) => string - reset: (id?: string) => void - remove: (id: string) => void - } - } -} - -const SCRIPT_ID = 'cf-turnstile-script' - -/** - * Carga el script de Cloudflare Turnstile (una vez) y renderiza el widget en el - * div referenciado. Devuelve el ref para el contenedor y el token resuelto. - * Si sitekey es vacío, no hace nada (captcha desactivado). - */ -export function useTurnstile(sitekey: string) { - const ref = useRef(null) - const [token, setToken] = useState('') - const rendered = useRef(false) - - useEffect(() => { - if (!sitekey) return - let cancelled = false - - function tryRender() { - if (cancelled || rendered.current || !ref.current || !window.turnstile) return - rendered.current = true - window.turnstile.render(ref.current, { - sitekey, - theme: 'dark', - callback: (t) => setToken(t), - 'error-callback': () => setToken(''), - 'expired-callback': () => setToken(''), - }) - } - - if (window.turnstile) { - tryRender() - return () => { - cancelled = true - } - } - - if (!document.getElementById(SCRIPT_ID)) { - const s = document.createElement('script') - s.id = SCRIPT_ID - s.src = 'https://challenges.cloudflare.com/turnstile/v0/api.js' - s.async = true - s.defer = true - document.head.appendChild(s) - } - const iv = window.setInterval(() => { - if (window.turnstile) { - window.clearInterval(iv) - tryRender() - } - }, 200) - return () => { - cancelled = true - window.clearInterval(iv) - } - }, [sitekey]) - - return { ref, token } -} diff --git a/frontend/src/types.ts b/frontend/src/types.ts deleted file mode 100644 index 1334904..0000000 --- a/frontend/src/types.ts +++ /dev/null @@ -1,15 +0,0 @@ -export interface Noticia { - id: number - titulo: string - fecha: string | null - contenido: string - enlace: string | null -} - -export interface ServerStatusData { - server_name: string | null - address: string | null - expansion: string - online_characters: number - status: 'Online' | 'Offline' -} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json deleted file mode 100644 index d109555..0000000 --- a/frontend/tsconfig.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2020", - "useDefineForClassFields": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], - "module": "ESNext", - "skipLibCheck": true, - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "resolveJsonModule": true, - "isolatedModules": true, - "moduleDetection": "force", - "noEmit": true, - "jsx": "react-jsx", - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true - }, - "include": ["src"] -} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts deleted file mode 100644 index eaa409e..0000000 --- a/frontend/vite.config.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react' -import { resolve } from 'node:path' - -// El bundle se construye en ../static/dist (que está en STATICFILES_DIRS), -// con manifest para que django-vite resuelva los assets en producción. -// base '/static/dist/' → coincide con static_url_prefix='dist' de django-vite. -export default defineConfig({ - plugins: [react()], - base: '/static/dist/', - build: { - manifest: true, - outDir: resolve(__dirname, '../static/dist'), - emptyOutDir: true, - rollupOptions: { - input: { - home: resolve(__dirname, 'src/entries/home.tsx'), - login: resolve(__dirname, 'src/entries/login.tsx'), - register: resolve(__dirname, 'src/entries/register.tsx'), - select_account: resolve(__dirname, 'src/entries/select_account.tsx'), - recover: resolve(__dirname, 'src/entries/recover.tsx'), - reset_password: resolve(__dirname, 'src/entries/reset_password.tsx'), - my_account: resolve(__dirname, 'src/entries/my_account.tsx'), - change_password: resolve(__dirname, 'src/entries/change_password.tsx'), - security_token: resolve(__dirname, 'src/entries/security_token.tsx'), - change_email: resolve(__dirname, 'src/entries/change_email.tsx'), - character_purchase: resolve(__dirname, 'src/entries/character_purchase.tsx'), - gold_character: resolve(__dirname, 'src/entries/gold_character.tsx'), - transfer_character: resolve(__dirname, 'src/entries/transfer_character.tsx'), - rename_guild: resolve(__dirname, 'src/entries/rename_guild.tsx'), - vote_points: resolve(__dirname, 'src/entries/vote_points.tsx'), - history_table: resolve(__dirname, 'src/entries/history_table.tsx'), - restore_character: resolve(__dirname, 'src/entries/restore_character.tsx'), - restore_items: resolve(__dirname, 'src/entries/restore_items.tsx'), - }, - }, - }, -}) diff --git a/home/__init__.py b/home/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/home/ac_soap.py b/home/ac_soap.py deleted file mode 100644 index 795c70f..0000000 --- a/home/ac_soap.py +++ /dev/null @@ -1,51 +0,0 @@ -# ac_soap.py -import requests -import base64 -from django.conf import settings - -def execute_soap_command(command): - """ - Ejecuta un comando en el servidor AzerothCore usando SOAP. - - Parameters: - - command (str): El comando a ejecutar, por ejemplo, ".additem 3200" - - Returns: - - str: La respuesta del servidor si el comando se ejecuta correctamente. - - None: Si ocurre algún error durante la ejecución. - """ - try: - # Crear la carga SOAP - soap_command = f''' - - - - {command} - - - ''' - - # Autenticación básica usando las credenciales en settings.py - auth_header = f"Basic {base64.b64encode(f'{settings.AC_SOAP_USER}:{settings.AC_SOAP_PASSWORD}'.encode()).decode()}" - - headers = { - 'Authorization': auth_header, - 'Content-Type': 'application/xml' - } - - # Realizar la solicitud SOAP al servidor de AzerothCore - response = requests.post(settings.AC_SOAP_URL, data=soap_command, headers=headers, timeout=5) - - # Verificar si la respuesta fue exitosa - if response.status_code == 200: - return response.text - else: - print(f"[ERROR] Comando SOAP fallido: {response.status_code} - {response.text}") - return None - - except requests.exceptions.RequestException as e: - print(f"[EXCEPCIÓN] Error de conexión con el servidor SOAP: {str(e)}") - return None - except Exception as e: - print(f"[EXCEPCIÓN] Error inesperado al ejecutar el comando SOAP: {str(e)}") - return None diff --git a/home/admin.py b/home/admin.py deleted file mode 100644 index d2441fe..0000000 --- a/home/admin.py +++ /dev/null @@ -1,204 +0,0 @@ -# home/admin.py -from django.contrib import admin -from django import forms -from .models import ( - Noticia, ClienteCategoria, SistemaOperativo, ServerSelection, - RecruitAFriend, DownloadClientPage, ContentCreator, RecruitReward, GuildRenameSettings, VoteSite, RenamePrice, CustomizePrice, ChangeRacePrice, ChangeFactionPrice, LevelUpPrice, GoldPrice, TransferPrice, MaintenanceMode, - StripeLog, Pedido -) -from django.db import connections -import logging -from django.shortcuts import redirect -from django_ckeditor_5.widgets import CKEditor5Widget - -logger = logging.getLogger(__name__) - -@admin.register(StripeLog) -class StripeLogAdmin(admin.ModelAdmin): - list_display = ( - 'account_id', 'username', 'email', 'acore_ip', 'stripe_ip', 'product_name', - 'amount', 'session_id', 'mode', 'character_name', 'timestamp' - ) # Añadido stripe_ip - list_filter = ('mode', 'timestamp') # Filtros por campo en la parte lateral - search_fields = ('username', 'product_name', 'session_id', 'email', 'stripe_ip') # Ahora incluye 'stripe_ip' en la búsqueda - ordering = ('-timestamp',) # Ordenar por timestamp descendentes - -@admin.register(Pedido) -class PedidoLogAdmin(admin.ModelAdmin): # Ahora esta es una clase de admin - list_display = ('transaction_id', 'email_cliente', 'estado', 'monto') # Campos que se mostrarán en el panel - search_fields = ('transaction_id', 'email_cliente') # Habilita búsqueda por ID y email - list_filter = ('estado',) # Filtro por estado de pago - - -@admin.register(ChangeFactionPrice) -class ChangeFactionPriceAdmin(admin.ModelAdmin): - list_display = ('price',) - - -@admin.register(MaintenanceMode) -class MaintenanceModeAdmin(admin.ModelAdmin): - list_display = ('is_active',) - -@admin.register(GoldPrice) -class GoldPriceAdmin(admin.ModelAdmin): - list_display = ('gold_amount', 'price') - ordering = ('gold_amount',) - - -@admin.register(TransferPrice) -class TransferPriceAdmin(admin.ModelAdmin): - list_display = ('price',) - -@admin.register(LevelUpPrice) -class LevelUpPriceAdmin(admin.ModelAdmin): - list_display = ('price',) - -# Formulario para el modelo Noticia -class NoticiaAdminForm(forms.ModelForm): - class Meta: - model = Noticia - fields = ['titulo', 'contenido', 'fecha_fin_evento', 'enlace'] - widgets = { - 'contenido': CKEditor5Widget(config_name='default'), - } - -@admin.register(RenamePrice) -class RenamePriceAdmin(admin.ModelAdmin): - list_display = ("price",) - - -@admin.register(CustomizePrice) -class CustomizePriceAdmin(admin.ModelAdmin): - list_display = ("price",) - -@admin.register(ChangeRacePrice) -class ChangeRacePriceAdmin(admin.ModelAdmin): - list_display = ('price',) - -@admin.register(Noticia) -class NoticiaAdmin(admin.ModelAdmin): - form = NoticiaAdminForm - list_display = ['titulo', 'fecha_publicacion'] - readonly_fields = ['fecha_publicacion'] - fields = ['titulo', 'contenido', 'fecha_fin_evento', 'enlace'] - - -# Formulario para el modelo RecruitAFriend -class RecruitAFriendAdminForm(forms.ModelForm): - content = forms.CharField(widget=CKEditor5Widget(config_name='default')) - - class Meta: - model = RecruitAFriend - fields = ['title', 'information', 'content'] - -@admin.register(RecruitAFriend) -class RecruitAFriendAdmin(admin.ModelAdmin): - form = RecruitAFriendAdminForm - list_display = ['title', 'updated_at'] - fields = ['title', 'information', 'content'] - - -# Formulario para el modelo DownloadClientPage -class DownloadClientPageAdminForm(forms.ModelForm): - content = forms.CharField(widget=CKEditor5Widget(config_name='default')) - mac_instructions = forms.CharField(widget=CKEditor5Widget(config_name='default')) - - class Meta: - model = DownloadClientPage - fields = ['title', 'content', 'mac_instructions'] - -@admin.register(DownloadClientPage) -class DownloadClientPageAdmin(admin.ModelAdmin): - form = DownloadClientPageAdminForm - list_display = ['title', 'updated_at'] - fields = ['title', 'content', 'mac_instructions'] - - -# Formulario para el modelo ContentCreator -class ContentCreatorAdminForm(forms.ModelForm): - description = forms.CharField(widget=CKEditor5Widget(config_name='default')) - - class Meta: - model = ContentCreator - fields = [ - 'title', 'description', 'content_type', 'subtitle', - 'youtube_video_url', 'avatar_image_url', - 'youtube_channel_url', 'facebook_page_url' - ] - -@admin.register(ContentCreator) -class ContentCreatorAdmin(admin.ModelAdmin): - form = ContentCreatorAdminForm - list_display = ['title', 'updated_at'] - fields = [ - 'title', 'description', 'content_type', 'subtitle', - 'youtube_video_url', 'avatar_image_url', - 'youtube_channel_url', 'facebook_page_url' - ] - - -# Administración para ServerSelection -class ServerSelectionAdmin(admin.ModelAdmin): - list_display = ['name', 'address', 'port', 'gamebuild'] - change_list_template = "admin/server_selection.html" - - def changelist_view(self, request, extra_context=None): - servers = [] - try: - with connections['acore_auth'].cursor() as cursor: - cursor.execute("SELECT id, name, address, port, gamebuild FROM realmlist") - servers = cursor.fetchall() - except Exception as e: - logger.error("Error al obtener servidores: %s", e) - - if request.method == "POST" and "server_selection" in request.POST: - selected_server_id = int(request.POST["server_selection"]) - selected_server = next((s for s in servers if s[0] == selected_server_id), None) - if selected_server: - ServerSelection.objects.all().delete() - ServerSelection.objects.create( - server_id=selected_server[0], - name=selected_server[1], - address=selected_server[2], - port=selected_server[3], - gamebuild=selected_server[4] - ) - return redirect("admin:home_serverselection_changelist") - - extra_context = extra_context or {} - extra_context['servers'] = servers - return super().changelist_view(request, extra_context=extra_context) - - -# Inlines para ClienteCategoria -class SistemaOperativoInline(admin.TabularInline): - model = SistemaOperativo - extra = 1 - -class ClienteCategoriaAdmin(admin.ModelAdmin): - inlines = [SistemaOperativoInline] - - -@admin.register(RecruitReward) -class RecruitRewardAdmin(admin.ModelAdmin): - list_display = ('required_friends', 'reward_name', 'item_id', 'item_quantity', 'icon_class') - search_fields = ('reward_name',) - -@admin.register(GuildRenameSettings) -class GuildRenameSettingsAdmin(admin.ModelAdmin): - list_display = ('id', 'cost') # Asegúrate de incluir 'id' antes de 'cost' - list_display_links = ('id',) # Permite hacer clic en 'id' para editar - list_editable = ('cost',) # Ahora 'cost' es editable en línea - -@admin.register(VoteSite) -class VoteSiteAdmin(admin.ModelAdmin): - list_display = ('name', 'points', 'url', 'image_url') - search_fields = ('name',) - list_editable = ('points',) - list_display_links = ('name',) - - -# Registro de modelos en el admin -admin.site.register(ClienteCategoria, ClienteCategoriaAdmin) -admin.site.register(SistemaOperativo) -admin.site.register(ServerSelection, ServerSelectionAdmin) diff --git a/home/api_urls.py b/home/api_urls.py deleted file mode 100644 index ab26867..0000000 --- a/home/api_urls.py +++ /dev/null @@ -1,10 +0,0 @@ -"""Rutas de la API JSON (islas React). Se montan bajo /api/ fuera de i18n_patterns.""" -from django.urls import path -from . import views - -urlpatterns = [ - path('home/news/', views.home_news_api, name='api_home_news'), - path('home/status/', views.home_status_api, name='api_home_status'), - path('account/select/', views.account_select_api, name='api_account_select'), - path('account/me/', views.account_me_api, name='api_account_me'), -] diff --git a/home/apps.py b/home/apps.py deleted file mode 100644 index e5ea0af..0000000 --- a/home/apps.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.apps import AppConfig - - -class HomeConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'home' diff --git a/home/bnet.py b/home/bnet.py deleted file mode 100644 index 069ea4a..0000000 --- a/home/bnet.py +++ /dev/null @@ -1,216 +0,0 @@ -""" -Utilidades de autenticación para servidores 3.4.3 (TrinityCore) con cuentas -Battle.net. - -Dos algoritmos SRP6 distintos conviven: - - * ``battlenet_accounts`` -> SRP6 **v2**: PBKDF2-HMAC-SHA512, N de 2048 bits, - g = 2. El "usuario" SRP es HEX(SHA256(UPPER(email))). Es el que usa el - cliente 3.4.3 para conectarse (login por email). - - * ``account`` (cuenta de juego) -> SRP6 **Grunt/SHA1**: el clásico de - AzerothCore/3.3.5a. Cada cuenta bnet tiene 1..N cuentas de juego con - username ``#``. - -Referencias del propio source 3.4.3: - src/common/Cryptography/Authentication/SRP6.cpp (CalculateX / CalculateVerifier) - src/server/game/Accounts/BattlenetAccountMgr.cpp (CreateBattlenetAccount) -""" - -import hashlib -import os - -# --------------------------------------------------------------------------- -# SRP6 v2 (battlenet_accounts) — PBKDF2-HMAC-SHA512 -# --------------------------------------------------------------------------- - -# BnetSRP6v2Base::N (2048 bits) y g, tal cual en SRP6.cpp -_BNET_N = int( - "AC6BDB41324A9A9BF166DE5E1389582FAF72B6651987EE07FC3192943DB56050" - "A37329CBB4A099ED8193E0757767A13DD52312AB4B03310DCD7F48A9DA04FD50" - "E8083969EDB767B0CF6095179A163AB3661A05FBD5FAAAE82918A9962F0B93B8" - "55F97993EC975EEAA80D740ADBF4FF747359D041D5C33EA71D281E446B14773B" - "CA97B43A23FB801676BD207A436C6481F1D2B9078717461A5B9D32E688F87748" - "544523B524B0D57D5EA77A2775D2ECFA032CFBDBF52FB3786160279004E57AE6" - "AF874E7303CE53299CCC041C7BC308D82A5698F3A8D0C38271AE35F8E9DBFBB6" - "94B5C803D89F7AE435DE236D525F54759B65E372FCD68EF20FA7111F9E4AFF73", - 16, -) -_BNET_g = 2 -_BNET_SALT_LEN = 32 -_BNET_ITERATIONS = 15000 # GetXIterations() para v2 -_BNET_SRP_VERSION = 2 -_TWO_POW_512 = 1 << 512 - -# TrinityCore almacena el verifier con BigNumber::ToByteVector(), que por -# defecto es little-endian. Si al comparar contra una cuenta creada por el -# propio servidor (`.bnetaccount create`) el login del CLIENTE fallara, prueba a -# poner esto en False (big-endian). El login del PORTAL funciona en cualquier -# caso porque registra y verifica con el mismo criterio. -_BNET_VERIFIER_LITTLE_ENDIAN = True - - -def bnet_srp_username(email): - """SRP username de bnet = HEX(SHA256(UPPER(email))) en mayúsculas.""" - email_up = email.strip().upper() - return hashlib.sha256(email_up.encode("utf-8")).hexdigest().upper() - - -def _bnet_calculate_x(email, password, salt): - """Replica BnetSRP6v2Base::CalculateX (SRP6.cpp).""" - srp_user = bnet_srp_username(email) - tmp = (srp_user + ":" + password).encode("utf-8") - x_bytes = hashlib.pbkdf2_hmac("sha512", tmp, salt, _BNET_ITERATIONS, dklen=64) - x = int.from_bytes(x_bytes, "big") - if x_bytes[0] & 0x80: # si el bit alto está puesto, resta 2^512 - x -= _TWO_POW_512 - return x % (_BNET_N - 1) - - -def _int_to_verifier_bytes(value): - """Emula BigNumber::ToByteVector(): longitud mínima, endianness configurable.""" - length = max(1, (value.bit_length() + 7) // 8) - order = "little" if _BNET_VERIFIER_LITTLE_ENDIAN else "big" - return value.to_bytes(length, order) - - -def _verifier_bytes_to_int(data): - order = "little" if _BNET_VERIFIER_LITTLE_ENDIAN else "big" - return int.from_bytes(data, order) - - -def bnet_make_registration(email, password): - """ - Genera (salt, verifier, srp_version) para una nueva cuenta Battle.net. - - salt: 32 bytes aleatorios (se guarda tal cual). - verifier: g^x mod N en longitud mínima (como TrinityCore::ToByteVector). - """ - salt = os.urandom(_BNET_SALT_LEN) - x = _bnet_calculate_x(email, password, salt) - v = pow(_BNET_g, x, _BNET_N) - return salt, _int_to_verifier_bytes(v), _BNET_SRP_VERSION - - -def bnet_verify(email, password, salt, stored_verifier): - """Comprueba la contraseña de una cuenta Battle.net (login por email).""" - if salt is None or stored_verifier is None: - return False - if isinstance(salt, memoryview): - salt = bytes(salt) - if isinstance(stored_verifier, memoryview): - stored_verifier = bytes(stored_verifier) - x = _bnet_calculate_x(email, password, salt) - v = pow(_BNET_g, x, _BNET_N) - # Comparación numérica (robusta frente a padding). - return v == _verifier_bytes_to_int(stored_verifier) - - -# --------------------------------------------------------------------------- -# SRP6 Grunt/SHA1 (cuenta de juego `account`) — el clásico de 3.3.5a -# --------------------------------------------------------------------------- - -_GRUNT_g = 7 -_GRUNT_N = int("894B645E89E1535BBDAD5B8B290650530801B18EBFBF5E8FAB3C82872A3E9BB7", 16) -_GRUNT_SALT_LEN = 32 - - -def _sha1(data): - return hashlib.sha1(data).digest() - - -def game_calculate_verifier(username, password, salt): - """ - SRP6 Grunt (SHA1) para la tabla `account`. ``username`` y ``password`` se - pasan en mayúsculas (TrinityCore hace Utf8ToUpperOnlyLatin en ambos). - Devuelve el verifier en bytes little-endian (32). - """ - if isinstance(salt, memoryview): - salt = bytes(salt) - h1 = _sha1((username.upper() + ":" + password.upper()).encode("utf-8")) - h2 = _sha1(salt + h1) - h2_int = int.from_bytes(h2, "little") - verifier = pow(_GRUNT_g, h2_int, _GRUNT_N) - return verifier.to_bytes(32, "little") - - -def game_make_registration(username, password): - """Genera (salt, verifier) Grunt para una cuenta de juego nueva.""" - salt = os.urandom(_GRUNT_SALT_LEN) - verifier = game_calculate_verifier(username, password[:16], salt) - return salt, verifier - - -def game_verify(username, password, salt, stored_verifier): - """Comprueba la contraseña de una cuenta de juego (SRP6 Grunt).""" - if salt is None or stored_verifier is None: - return False - if isinstance(stored_verifier, memoryview): - stored_verifier = bytes(stored_verifier) - calc = game_calculate_verifier(username, password[:16], salt) - return int.from_bytes(calc, "little") == int.from_bytes(stored_verifier, "little") - - -# --------------------------------------------------------------------------- -# Helpers de acceso a la BD auth (bnet <-> cuentas de juego) -# --------------------------------------------------------------------------- - -def normalize_email(email): - """El email en battlenet_accounts se almacena en MAYÚSCULAS.""" - return email.strip().upper() - - -def get_bnet_account_by_email(cursor, email): - """Devuelve dict {id, email, srp_version, salt, verifier} o None.""" - cursor.execute( - "SELECT id, email, srp_version, salt, verifier " - "FROM battlenet_accounts WHERE email = %s", - [normalize_email(email)], - ) - row = cursor.fetchone() - if not row: - return None - return { - "id": row[0], - "email": row[1], - "srp_version": row[2], - "salt": row[3], - "verifier": row[4], - } - - -def get_game_accounts_for_bnet(cursor, bnet_id): - """Lista de cuentas de juego de una cuenta bnet: [{id, username, index}].""" - cursor.execute( - "SELECT id, username, battlenet_index " - "FROM account WHERE battlenet_account = %s ORDER BY battlenet_index", - [bnet_id], - ) - return [{"id": r[0], "username": r[1], "index": r[2]} for r in cursor.fetchall()] - - -def get_next_battlenet_index(cursor, bnet_id): - """Siguiente battlenet_index libre para una cuenta bnet (1-based).""" - cursor.execute( - "SELECT MAX(battlenet_index) FROM account WHERE battlenet_account = %s", - [bnet_id], - ) - row = cursor.fetchone() - return (row[0] or 0) + 1 - - -def make_game_account_username(bnet_id, index=1): - """Nombre de la cuenta de juego: ``#`` (como TrinityCore).""" - return f"{bnet_id}#{index}" - - -def get_battlepay_credits(cursor, bnet_id): - """Créditos Battlepay de la cuenta bnet (0 si la columna no existe).""" - if not bnet_id: - return 0 - try: - cursor.execute("SELECT battlePayCredits FROM battlenet_accounts WHERE id = %s", [bnet_id]) - row = cursor.fetchone() - return row[0] if row and row[0] is not None else 0 - except Exception: - return 0 diff --git a/home/context_processors.py b/home/context_processors.py deleted file mode 100644 index aed81b9..0000000 --- a/home/context_processors.py +++ /dev/null @@ -1,40 +0,0 @@ -from django.conf import settings -from .models import ServerSelection -from django.db import connections -import socket - -def url_principal(request): - """Devuelve la URL principal del sitio.""" - current_domain = f"{request.scheme}://{request.get_host()}" - return {'URL_PRINCIPAL': current_domain} - -def configuracion_global(request): - """Devuelve configuraciones globales como el nombre del servidor y el año actual.""" - return { - 'NOMBRE_SERVIDOR': settings.NOMBRE_SERVIDOR, - 'ANIO_ACTUAL': settings.ANIO_ACTUAL, - 'TURNSTILE_SITE_KEY': settings.TURNSTILE_SITE_KEY, - } - -def get_server_info(request): - """Devuelve información del servidor para que esté disponible en todas las plantillas.""" - server_selection = ServerSelection.objects.first() - - online_count = 0 - if server_selection: - # Obtener la cantidad de personajes en línea - try: - with connections['acore_characters'].cursor() as cursor: - cursor.execute("SELECT COUNT(*) FROM characters WHERE online = 1") - result = cursor.fetchone() - if result is not None: - online_count = result[0] - except Exception as e: - # Manejar errores de conexión o consulta - print(f"Error al obtener personajes en línea: {e}") - - return { - 'server': server_selection, - 'online_characters': online_count, - 'expansion': server_selection.expansion if server_selection else 'Unknown', - } diff --git a/home/library_correo.py b/home/library_correo.py deleted file mode 100644 index 580d624..0000000 --- a/home/library_correo.py +++ /dev/null @@ -1,35 +0,0 @@ -import os -from django.core.mail import send_mail -from django.conf import settings -from django.template.loader import render_to_string -from django.utils.html import strip_tags - -def enviar_correo(subject, to_email, template, context): - """ - Envía un correo electrónico utilizando una plantilla HTML. - - Args: - subject (str): El asunto del correo. - to_email (str): Dirección de correo del destinatario. - template (str): Ruta de la plantilla HTML. - context (dict): Contexto para renderizar la plantilla. - """ - try: - # Renderizar el contenido del correo desde una plantilla - html_message = render_to_string(template, context) - plain_message = strip_tags(html_message) - from_email = settings.DEFAULT_FROM_EMAIL - - # Enviar el correo - send_mail( - subject, - plain_message, - from_email, - [to_email], - html_message=html_message, - fail_silently=False, - ) - return True - except Exception as e: - print(f"Error al enviar el correo: {e}") - return False diff --git a/home/middleware.py b/home/middleware.py deleted file mode 100644 index 5f08c09..0000000 --- a/home/middleware.py +++ /dev/null @@ -1,28 +0,0 @@ -from django.shortcuts import redirect -from django.urls import resolve, reverse -from .models import MaintenanceMode - -class MaintenanceMiddleware: - def __init__(self, get_response): - self.get_response = get_response - - def __call__(self, request): - # Excluir todas las rutas bajo /admin y la propia página de mantenimiento - excluded_prefixes = [ - reverse('admin:index').rstrip('/'), # Asegura que todas las rutas bajo /admin estén excluidas - reverse('maintenance').rstrip('/'), # Página de mantenimiento - ] - - # Verificar si la ruta actual está en los prefijos excluidos - if any(request.path.startswith(prefix) for prefix in excluded_prefixes): - return self.get_response(request) - - # Verificar si el modo de mantenimiento está activo - try: - maintenance = MaintenanceMode.objects.first() - if maintenance and maintenance.is_active: - return redirect('maintenance') - except MaintenanceMode.DoesNotExist: - pass - - return self.get_response(request) diff --git a/home/migrations/0001_initial.py b/home/migrations/0001_initial.py deleted file mode 100644 index 13b0e22..0000000 --- a/home/migrations/0001_initial.py +++ /dev/null @@ -1,114 +0,0 @@ -# Generated by Django 5.1.2 on 2024-11-11 16:11 - -import django.db.models.deletion -import django_ckeditor_5.fields -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ] - - operations = [ - migrations.CreateModel( - name='ClienteCategoria', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('nombre', models.CharField(max_length=100)), - ('descripcion', models.CharField(blank=True, max_length=200, null=True)), - ], - ), - migrations.CreateModel( - name='ContentCreator', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('title', models.CharField(max_length=200)), - ('description', django_ckeditor_5.fields.CKEditor5Field(verbose_name='Descripción')), - ('content_type', models.CharField(default='Contenido general', max_length=60)), - ('subtitle', models.CharField(default='Nova WoW', max_length=60)), - ('youtube_video_url', models.URLField(blank=True, max_length=300, null=True)), - ('avatar_image_url', models.URLField(blank=True, max_length=300, null=True)), - ('youtube_channel_url', models.URLField(blank=True, max_length=300, null=True)), - ('facebook_page_url', models.URLField(blank=True, max_length=300, null=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ], - ), - migrations.CreateModel( - name='DownloadClientPage', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('title', models.CharField(default='Descarga del cliente', max_length=255)), - ('content', django_ckeditor_5.fields.CKEditor5Field(verbose_name='Contenido')), - ('mac_instructions', django_ckeditor_5.fields.CKEditor5Field(blank=True, null=True, verbose_name='Instrucciones para macOS')), - ('updated_at', models.DateTimeField(auto_now=True)), - ], - ), - migrations.CreateModel( - name='Noticia', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('titulo', models.CharField(max_length=200)), - ('contenido', django_ckeditor_5.fields.CKEditor5Field(verbose_name='Contenido')), - ('fecha_publicacion', models.DateTimeField(auto_now=True)), - ('fecha_fin_evento', models.DateField(blank=True, null=True)), - ('enlace', models.URLField(blank=True, null=True)), - ], - ), - migrations.CreateModel( - name='RecruitAFriend', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('title', models.CharField(default='Recluta a un amigo', max_length=255)), - ('information', models.CharField(default='Información', max_length=255)), - ('content', django_ckeditor_5.fields.CKEditor5Field(verbose_name='Contenido')), - ('updated_at', models.DateTimeField(auto_now=True)), - ], - ), - migrations.CreateModel( - name='RecruitReward', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('required_friends', models.IntegerField()), - ('reward_name', models.CharField(max_length=100)), - ('item_id', models.IntegerField()), - ('item_quantity', models.IntegerField(default=1)), - ('item_link', models.URLField()), - ('icon_class', models.CharField(help_text="Clase CSS para el icono (ej. 'icontinyl q3')", max_length=50)), - ('claimed_by', models.CharField(blank=True, max_length=100, null=True)), - ], - ), - migrations.CreateModel( - name='ServerSelection', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('server_id', models.IntegerField()), - ('name', models.CharField(max_length=100)), - ('address', models.CharField(max_length=100)), - ('port', models.IntegerField()), - ('gamebuild', models.IntegerField()), - ], - ), - migrations.CreateModel( - name='ClaimedReward', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('account_id', models.IntegerField()), - ('character_name', models.CharField(max_length=100)), - ('username', models.CharField(max_length=100)), - ('claimed_at', models.DateTimeField(auto_now_add=True)), - ('recruit_reward', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='claimed_rewards', to='home.recruitreward')), - ], - ), - migrations.CreateModel( - name='SistemaOperativo', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('nombre', models.CharField(max_length=100)), - ('url_descarga', models.URLField()), - ('categoria', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sistemas_operativos', to='home.clientecategoria')), - ], - ), - ] diff --git a/home/migrations/0002_accountactivation_remove_recruitreward_claimed_by_and_more.py b/home/migrations/0002_accountactivation_remove_recruitreward_claimed_by_and_more.py deleted file mode 100644 index d8b4045..0000000 --- a/home/migrations/0002_accountactivation_remove_recruitreward_claimed_by_and_more.py +++ /dev/null @@ -1,37 +0,0 @@ -# Generated by Django 5.1.2 on 2024-11-12 12:56 - -import django.utils.timezone -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('home', '0001_initial'), - ] - - operations = [ - migrations.CreateModel( - name='AccountActivation', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('username', models.CharField(max_length=17)), - ('email', models.EmailField(max_length=254)), - ('password', models.CharField(max_length=64)), - ('salt', models.CharField(max_length=64)), - ('verifier', models.CharField(max_length=128)), - ('recruiter_id', models.IntegerField(blank=True, null=True)), - ('hash', models.CharField(max_length=32, unique=True)), - ('created_at', models.DateTimeField(default=django.utils.timezone.now)), - ], - ), - migrations.RemoveField( - model_name='recruitreward', - name='claimed_by', - ), - migrations.AddField( - model_name='claimedreward', - name='ip_address', - field=models.CharField(default='127.0.0.1', max_length=45), - ), - ] diff --git a/home/migrations/0003_alter_accountactivation_salt.py b/home/migrations/0003_alter_accountactivation_salt.py deleted file mode 100644 index 9d1de9f..0000000 --- a/home/migrations/0003_alter_accountactivation_salt.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.1.2 on 2024-11-12 13:23 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('home', '0002_accountactivation_remove_recruitreward_claimed_by_and_more'), - ] - - operations = [ - migrations.AlterField( - model_name='accountactivation', - name='salt', - field=models.CharField(max_length=128), - ), - ] diff --git a/home/migrations/0004_alter_accountactivation_salt_and_more.py b/home/migrations/0004_alter_accountactivation_salt_and_more.py deleted file mode 100644 index 361ee3a..0000000 --- a/home/migrations/0004_alter_accountactivation_salt_and_more.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 5.1.2 on 2024-11-12 14:02 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('home', '0003_alter_accountactivation_salt'), - ] - - operations = [ - migrations.AlterField( - model_name='accountactivation', - name='salt', - field=models.BinaryField(max_length=32), - ), - migrations.AlterField( - model_name='accountactivation', - name='verifier', - field=models.BinaryField(max_length=32), - ), - ] diff --git a/home/migrations/0005_securitytoken.py b/home/migrations/0005_securitytoken.py deleted file mode 100644 index 7759b59..0000000 --- a/home/migrations/0005_securitytoken.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 5.1.2 on 2024-11-13 10:45 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('home', '0004_alter_accountactivation_salt_and_more'), - ] - - operations = [ - migrations.CreateModel( - name='SecurityToken', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('user_id', models.IntegerField()), - ('token', models.CharField(max_length=6)), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('expires_at', models.DateTimeField()), - ('ip_address', models.GenericIPAddressField()), - ], - ), - ] diff --git a/home/migrations/0006_changefactionprice_changeraceprice_customizeprice_and_more.py b/home/migrations/0006_changefactionprice_changeraceprice_customizeprice_and_more.py deleted file mode 100644 index 1b186ca..0000000 --- a/home/migrations/0006_changefactionprice_changeraceprice_customizeprice_and_more.py +++ /dev/null @@ -1,186 +0,0 @@ -# Generated by Django 5.1.4 on 2024-12-26 21:07 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('home', '0005_securitytoken'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='ChangeFactionPrice', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('price', models.DecimalField(decimal_places=2, default=10.0, max_digits=6)), - ], - ), - migrations.CreateModel( - name='ChangeRacePrice', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('price', models.DecimalField(decimal_places=2, max_digits=5)), - ], - ), - migrations.CreateModel( - name='CustomizePrice', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('price', models.DecimalField(decimal_places=2, default=1.0, max_digits=5)), - ], - ), - migrations.CreateModel( - name='GoldPrice', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('gold_amount', models.IntegerField()), - ('price', models.DecimalField(decimal_places=2, max_digits=10)), - ], - ), - migrations.CreateModel( - name='GuildRenameSettings', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('cost', models.IntegerField(default=1000, help_text='Costo en Donation Points (DP) para renombrar una hermandad.')), - ], - options={ - 'verbose_name': 'Configuración de Renombrar Hermandad', - 'verbose_name_plural': 'Configuraciones de Renombrar Hermandad', - }, - ), - migrations.CreateModel( - name='HomeApiPoints', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('accountID', models.IntegerField(unique=True)), - ('vp', models.IntegerField(default=0)), - ('dp', models.IntegerField(default=0)), - ], - options={ - 'db_table': 'home_api_points', - }, - ), - migrations.CreateModel( - name='LevelUpPrice', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('price', models.DecimalField(decimal_places=2, default=10.0, max_digits=10)), - ], - ), - migrations.CreateModel( - name='RenamePrice', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('price', models.DecimalField(decimal_places=2, default=2.0, max_digits=5)), - ], - ), - migrations.CreateModel( - name='ReviveHistory', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('character_name', models.CharField(max_length=50)), - ('used_at', models.DateTimeField()), - ], - ), - migrations.CreateModel( - name='TransferPrice', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('price', models.DecimalField(decimal_places=2, max_digits=5)), - ], - ), - migrations.CreateModel( - name='UnstuckHistory', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('character_name', models.CharField(max_length=50)), - ('used_at', models.DateTimeField()), - ], - ), - migrations.CreateModel( - name='VoteSite', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=100, unique=True)), - ('url', models.URLField(help_text='URL del sitio de votación')), - ('image_url', models.URLField(help_text='URL completa de la imagen', max_length=300)), - ('points', models.IntegerField(default=1, help_text='Puntos otorgados por votar')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ], - ), - migrations.RemoveField( - model_name='securitytoken', - name='user_id', - ), - migrations.AddField( - model_name='accountactivation', - name='is_new_email_used', - field=models.BooleanField(default=False), - ), - migrations.AddField( - model_name='accountactivation', - name='is_used', - field=models.BooleanField(default=False), - ), - migrations.AddField( - model_name='accountactivation', - name='old_email', - field=models.EmailField(blank=True, max_length=254, null=True), - ), - migrations.AddField( - model_name='accountactivation', - name='old_email_hash', - field=models.CharField(blank=True, max_length=32, null=True), - ), - migrations.AddField( - model_name='securitytoken', - name='user', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), - ), - migrations.CreateModel( - name='StoreCategory', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=100)), - ('parent_category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='home.storecategory')), - ], - ), - migrations.CreateModel( - name='StoreItem', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=200)), - ('item_id', models.IntegerField()), - ('image_url', models.URLField()), - ('price', models.DecimalField(decimal_places=2, max_digits=6)), - ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='home.storecategory')), - ], - ), - migrations.CreateModel( - name='Cart', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('username', models.CharField(max_length=150)), - ('added_at', models.DateTimeField(auto_now_add=True)), - ('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='home.storeitem')), - ], - ), - migrations.CreateModel( - name='VoteLog', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('account_id', models.IntegerField()), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('last_vote_time', models.DateTimeField(blank=True, null=True)), - ('processed', models.BooleanField(default=False)), - ('processed_at', models.DateTimeField(blank=True, null=True)), - ('vote_site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='home.votesite')), - ], - ), - ] diff --git a/home/migrations/0007_maintenancemode.py b/home/migrations/0007_maintenancemode.py deleted file mode 100644 index ab37484..0000000 --- a/home/migrations/0007_maintenancemode.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 4.2.17 on 2024-12-28 21:22 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('home', '0006_changefactionprice_changeraceprice_customizeprice_and_more'), - ] - - operations = [ - migrations.CreateModel( - name='MaintenanceMode', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('is_active', models.BooleanField(default=False)), - ], - ), - ] diff --git a/home/migrations/0008_category_remove_storecategory_parent_category_and_more.py b/home/migrations/0008_category_remove_storecategory_parent_category_and_more.py deleted file mode 100644 index 397991b..0000000 --- a/home/migrations/0008_category_remove_storecategory_parent_category_and_more.py +++ /dev/null @@ -1,49 +0,0 @@ -# Generated by Django 5.1.4 on 2025-02-15 18:04 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('home', '0007_maintenancemode'), - ] - - operations = [ - migrations.CreateModel( - name='Category', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=100, unique=True)), - ], - ), - migrations.RemoveField( - model_name='storecategory', - name='parent_category', - ), - migrations.RemoveField( - model_name='storeitem', - name='category', - ), - migrations.CreateModel( - name='Item', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=200)), - ('price', models.DecimalField(decimal_places=2, max_digits=10)), - ('item_id', models.IntegerField(unique=True)), - ('image_url', models.URLField()), - ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='home.category')), - ], - ), - migrations.DeleteModel( - name='Cart', - ), - migrations.DeleteModel( - name='StoreCategory', - ), - migrations.DeleteModel( - name='StoreItem', - ), - ] diff --git a/home/migrations/0009_stripelog.py b/home/migrations/0009_stripelog.py deleted file mode 100644 index 187eb4b..0000000 --- a/home/migrations/0009_stripelog.py +++ /dev/null @@ -1,30 +0,0 @@ -# Generated by Django 5.1.4 on 2025-02-16 18:16 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('home', '0008_category_remove_storecategory_parent_category_and_more'), - ] - - operations = [ - migrations.CreateModel( - name='StripeLog', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('account_id', models.IntegerField()), - ('username', models.CharField(max_length=100)), - ('email', models.EmailField(max_length=254)), - ('acore_ip', models.GenericIPAddressField()), - ('stripe_ip', models.GenericIPAddressField(blank=True, null=True)), - ('product_name', models.TextField()), - ('amount', models.DecimalField(decimal_places=2, max_digits=10)), - ('session_id', models.CharField(max_length=255)), - ('mode', models.CharField(choices=[('test', 'Test'), ('live', 'Live')], max_length=10)), - ('character_name', models.CharField(max_length=100)), - ('timestamp', models.DateTimeField(auto_now_add=True)), - ], - ), - ] diff --git a/home/migrations/0010_loginattempt.py b/home/migrations/0010_loginattempt.py deleted file mode 100644 index 3f008a9..0000000 --- a/home/migrations/0010_loginattempt.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 5.1.4 on 2025-02-18 10:37 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('home', '0009_stripelog'), - ] - - operations = [ - migrations.CreateModel( - name='LoginAttempt', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('username', models.CharField(max_length=100)), - ('status', models.CharField(max_length=50)), - ('ip_address', models.GenericIPAddressField()), - ('timestamp', models.DateTimeField(auto_now_add=True)), - ], - ), - ] diff --git a/home/migrations/0011_pedido_alter_guildrenamesettings_options_and_more.py b/home/migrations/0011_pedido_alter_guildrenamesettings_options_and_more.py deleted file mode 100644 index cbfec1c..0000000 --- a/home/migrations/0011_pedido_alter_guildrenamesettings_options_and_more.py +++ /dev/null @@ -1,32 +0,0 @@ -# Generated by Django 5.1.4 on 2025-03-02 16:23 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('home', '0010_loginattempt'), - ] - - operations = [ - migrations.CreateModel( - name='Pedido', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('transaction_id', models.CharField(blank=True, max_length=100, null=True, unique=True)), - ('email_cliente', models.EmailField(max_length=254)), - ('estado', models.CharField(default='Pendiente', max_length=20)), - ('monto', models.DecimalField(decimal_places=2, max_digits=10)), - ], - ), - migrations.AlterModelOptions( - name='guildrenamesettings', - options={}, - ), - migrations.AlterField( - model_name='guildrenamesettings', - name='cost', - field=models.DecimalField(decimal_places=2, default=10, help_text='Costo en euros para renombrar una hermandad.', max_digits=10), - ), - ] diff --git a/home/migrations/0012_alter_accountactivation_salt_and_more.py b/home/migrations/0012_alter_accountactivation_salt_and_more.py deleted file mode 100644 index 0127f10..0000000 --- a/home/migrations/0012_alter_accountactivation_salt_and_more.py +++ /dev/null @@ -1,28 +0,0 @@ -# Generated by Django 6.0.7 on 2026-07-12 16:33 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('home', '0011_pedido_alter_guildrenamesettings_options_and_more'), - ] - - operations = [ - migrations.AlterField( - model_name='accountactivation', - name='salt', - field=models.BinaryField(blank=True, max_length=32, null=True), - ), - migrations.AlterField( - model_name='accountactivation', - name='username', - field=models.CharField(blank=True, max_length=17, null=True), - ), - migrations.AlterField( - model_name='accountactivation', - name='verifier', - field=models.BinaryField(blank=True, max_length=256, null=True), - ), - ] diff --git a/home/migrations/0013_stripelog_fulfilled.py b/home/migrations/0013_stripelog_fulfilled.py deleted file mode 100644 index 3cef38e..0000000 --- a/home/migrations/0013_stripelog_fulfilled.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 6.0.7 on 2026-07-12 17:55 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('home', '0012_alter_accountactivation_salt_and_more'), - ] - - operations = [ - migrations.AddField( - model_name='stripelog', - name='fulfilled', - field=models.BooleanField(default=False), - ), - ] diff --git a/home/migrations/0014_passwordreset.py b/home/migrations/0014_passwordreset.py deleted file mode 100644 index 9f98f29..0000000 --- a/home/migrations/0014_passwordreset.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 6.0.7 on 2026-07-12 20:53 - -import django.utils.timezone -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('home', '0013_stripelog_fulfilled'), - ] - - operations = [ - migrations.CreateModel( - name='PasswordReset', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('email', models.EmailField(max_length=254)), - ('token', models.CharField(max_length=64, unique=True)), - ('created_at', models.DateTimeField(default=django.utils.timezone.now)), - ('used', models.BooleanField(default=False)), - ], - ), - ] diff --git a/home/migrations/__init__.py b/home/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/home/models/__init__.py b/home/models/__init__.py deleted file mode 100644 index e10e522..0000000 --- a/home/models/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -"""Modelos de la app `home`, divididos por secciones. - -Antes un único home/models.py. El `import *` de cada submódulo re-exporta -los modelos para que `from home.models import X`, el admin y las migraciones -(app_label=`home`, nombres de modelo intactos) sigan funcionando igual. -""" -from .pricing import * # noqa: F401,F403 -from .store import * # noqa: F401,F403 -from .site import * # noqa: F401,F403 -from .recruit import * # noqa: F401,F403 -from .accounts import * # noqa: F401,F403 -from .voting import * # noqa: F401,F403 -from .history import * # noqa: F401,F403 diff --git a/home/models/accounts.py b/home/models/accounts.py deleted file mode 100644 index 8148d72..0000000 --- a/home/models/accounts.py +++ /dev/null @@ -1,62 +0,0 @@ -from django.db import models -from django.contrib.auth.models import User -from django.utils import timezone - - -class AccountActivation(models.Model): - # En 3.4.3 (Battle.net) la identidad es el email; el username de la cuenta - # de juego se genera al activar (#1), por eso es opcional aquí. - username = models.CharField(max_length=17, null=True, blank=True) - email = models.EmailField() - old_email = models.EmailField(null=True, blank=True) - password = models.CharField(max_length=64) - # salt/verifier ya no se precalculan en el registro (se calculan al activar, - # tanto el de bnet como el de la cuenta de juego). Se conservan opcionales - # por compatibilidad con el flujo de cambio de email. - salt = models.BinaryField(max_length=32, null=True, blank=True) - verifier = models.BinaryField(max_length=256, null=True, blank=True) - recruiter_id = models.IntegerField(null=True, blank=True) - hash = models.CharField(max_length=32, unique=True) - old_email_hash = models.CharField(max_length=32, null=True, blank=True) - is_used = models.BooleanField(default=False) # Indica si el old_email_hash ha sido usado - is_new_email_used = models.BooleanField(default=False) # Indica si el hash del nuevo correo ha sido usado - created_at = models.DateTimeField(default=timezone.now) - - def is_expired(self): - return timezone.now() > self.created_at + timezone.timedelta(hours=1) - - def __str__(self): - return f"Activation for {self.username}" - -class PasswordReset(models.Model): - """Token de restablecimiento de contraseña (cuenta Battle.net, por email).""" - email = models.EmailField() - token = models.CharField(max_length=64, unique=True) - created_at = models.DateTimeField(default=timezone.now) - used = models.BooleanField(default=False) - - def is_expired(self): - return timezone.now() > self.created_at + timezone.timedelta(hours=1) - - def __str__(self): - return f"PasswordReset for {self.email}" - - -class SecurityToken(models.Model): - user = models.ForeignKey(User, on_delete=models.CASCADE, null=True, blank=True) - token = models.CharField(max_length=6) - created_at = models.DateTimeField(auto_now_add=True) - expires_at = models.DateTimeField() - ip_address = models.GenericIPAddressField() - - def __str__(self): - return f"Token for {self.user.username if self.user else 'Desconocido'}" - -class LoginAttempt(models.Model): - username = models.CharField(max_length=100) - status = models.CharField(max_length=50) # Ejemplo: 'Éxito' o 'Fracaso' - ip_address = models.GenericIPAddressField() # IP del intento de inicio de sesión - timestamp = models.DateTimeField(auto_now_add=True) - - def __str__(self): - return f"{self.username} - {self.status} - {self.timestamp}" diff --git a/home/models/history.py b/home/models/history.py deleted file mode 100644 index daabf88..0000000 --- a/home/models/history.py +++ /dev/null @@ -1,13 +0,0 @@ -from django.db import models - - -class UnstuckHistory(models.Model): - character_name = models.CharField(max_length=50) - used_at = models.DateTimeField() - -class ReviveHistory(models.Model): - character_name = models.CharField(max_length=50) - used_at = models.DateTimeField() - - def __str__(self): - return f"{self.character_name} - {self.used_at}" diff --git a/home/models/pricing.py b/home/models/pricing.py deleted file mode 100644 index 73dbac8..0000000 --- a/home/models/pricing.py +++ /dev/null @@ -1,51 +0,0 @@ -from django.db import models - - -class ChangeFactionPrice(models.Model): - price = models.DecimalField(max_digits=6, decimal_places=2, default=10.00) - - def __str__(self): - return f"Precio para cambiar facción: {self.price} EUR" - -class LevelUpPrice(models.Model): - price = models.DecimalField(max_digits=10, decimal_places=2, default=10.00) - - def __str__(self): - return f"Subir a nivel 80: {self.price} EUR" - -class GoldPrice(models.Model): - gold_amount = models.IntegerField() # Cantidad de oro - price = models.DecimalField(max_digits=10, decimal_places=2) # Precio en EUR - - def __str__(self): - return f"{self.gold_amount} oro - {self.price} EUR" - -class TransferPrice(models.Model): - price = models.DecimalField(max_digits=5, decimal_places=2) - - def __str__(self): - return f"Precio de transferencia: {self.price} EUR" - -class RenamePrice(models.Model): - price = models.DecimalField(max_digits=5, decimal_places=2, default=2.00) - - def __str__(self): - return f"{self.price} EUR" - -class CustomizePrice(models.Model): - price = models.DecimalField(max_digits=5, decimal_places=2, default=1.00) - - def __str__(self): - return f"{self.price} EUR" - -class ChangeRacePrice(models.Model): - price = models.DecimalField(max_digits=5, decimal_places=2) - - def __str__(self): - return f"Cambiar raza: {self.price} EUR" - -class GuildRenameSettings(models.Model): - cost = models.DecimalField(max_digits=10, decimal_places=2, default=10, help_text="Costo en euros para renombrar una hermandad.") - - def __str__(self): - return f"Configuración de renombrar hermandad - Costo: {self.cost} €" diff --git a/home/models/recruit.py b/home/models/recruit.py deleted file mode 100644 index 21e2588..0000000 --- a/home/models/recruit.py +++ /dev/null @@ -1,34 +0,0 @@ -from django.db import models -from django_ckeditor_5.fields import CKEditor5Field - - -class RecruitAFriend(models.Model): - title = models.CharField(max_length=255, default="Recluta a un amigo") - information = models.CharField(max_length=255, default="Información") - content = CKEditor5Field('Contenido', config_name='default') - updated_at = models.DateTimeField(auto_now=True) - - def __str__(self): - return self.title - -class RecruitReward(models.Model): - required_friends = models.IntegerField() - reward_name = models.CharField(max_length=100) - item_id = models.IntegerField() - item_quantity = models.IntegerField(default=1) - item_link = models.URLField() - icon_class = models.CharField(max_length=50, help_text="Clase CSS para el icono (ej. 'icontinyl q3')") - - def __str__(self): - return f"{self.required_friends} amigos - {self.reward_name}" - -class ClaimedReward(models.Model): - recruit_reward = models.ForeignKey(RecruitReward, on_delete=models.CASCADE, related_name='claimed_rewards') - account_id = models.IntegerField() - character_name = models.CharField(max_length=100) - username = models.CharField(max_length=100) - claimed_at = models.DateTimeField(auto_now_add=True) - ip_address = models.CharField(max_length=45, default='127.0.0.1') # Añadir un valor predeterminado - - def __str__(self): - return f"{self.username} - {self.recruit_reward.reward_name} - {self.claimed_at}" diff --git a/home/models/site.py b/home/models/site.py deleted file mode 100644 index a6ebaeb..0000000 --- a/home/models/site.py +++ /dev/null @@ -1,95 +0,0 @@ -from django.db import models -from django_ckeditor_5.fields import CKEditor5Field - - -def expansion_from_gamebuild(gamebuild): - """Etiqueta de expansión a partir del build del cliente. - - Distingue el retail antiguo del relanzamiento WoW Classic: - - 12340 -> 'WotLK' (3.3.5a, retail/privado clásico) - - 44832..54261 -> 'WotLK Classic' (3.4.x; 3.4.3 = 54261) - - 15595 -> 'Cataclysm' (4.3.4 retail) - - 54332..60000 -> 'Cataclysm Classic' (4.4.x) - WotLK Classic llega a 3.4.3=54261 y Cataclysm Classic empieza en 4.4.0=54332, - están muy pegados: de ahí el corte en 54261. Nova WoW corre 3.4.3 -> WotLK Classic. - """ - if gamebuild is None: - return 'Unknown' - if gamebuild == 12340: - return 'WotLK' - if 44000 <= gamebuild <= 54261: - return 'WotLK Classic' - if gamebuild == 15595: - return 'Cataclysm' - if 54262 <= gamebuild <= 60000: - return 'Cataclysm Classic' - return 'Unknown' - - -class Noticia(models.Model): - titulo = models.CharField(max_length=200) - contenido = CKEditor5Field('Contenido', config_name='default') - fecha_publicacion = models.DateTimeField(auto_now=True) # Cambiado a DateTimeField - fecha_fin_evento = models.DateField(null=True, blank=True) - enlace = models.URLField(max_length=200, blank=True, null=True) - - def __str__(self): - return self.titulo - -class MaintenanceMode(models.Model): - is_active = models.BooleanField(default=False) - - def __str__(self): - return "Maintenance Mode" - -class ClienteCategoria(models.Model): - nombre = models.CharField(max_length=100) - descripcion = models.CharField(max_length=200, blank=True, null=True) - - def __str__(self): - return self.nombre - -class SistemaOperativo(models.Model): - categoria = models.ForeignKey(ClienteCategoria, related_name='sistemas_operativos', on_delete=models.CASCADE) - nombre = models.CharField(max_length=100) - url_descarga = models.URLField() - - def __str__(self): - return f"{self.nombre} - {self.categoria.nombre}" - -class ServerSelection(models.Model): - server_id = models.IntegerField() - name = models.CharField(max_length=100) - address = models.CharField(max_length=100) - port = models.IntegerField() - gamebuild = models.IntegerField() - - @property - def expansion(self): - return expansion_from_gamebuild(self.gamebuild) - - def __str__(self): - return f"{self.name} ({self.expansion})" - -class DownloadClientPage(models.Model): - title = models.CharField(max_length=255, default="Descarga del cliente") - content = CKEditor5Field('Contenido', config_name='default') - mac_instructions = CKEditor5Field('Instrucciones para macOS', blank=True, null=True, config_name='default') - updated_at = models.DateTimeField(auto_now=True) - - def __str__(self): - return self.title - -class ContentCreator(models.Model): - title = models.CharField(max_length=200) - description = CKEditor5Field('Descripción', config_name='default') - content_type = models.CharField(max_length=60, default="Contenido general") - subtitle = models.CharField(max_length=60, default="Nova WoW") - youtube_video_url = models.URLField(max_length=300, blank=True, null=True) - avatar_image_url = models.URLField(max_length=300, blank=True, null=True) - youtube_channel_url = models.URLField(max_length=300, blank=True, null=True) - facebook_page_url = models.URLField(max_length=300, blank=True, null=True) - updated_at = models.DateTimeField(auto_now=True) - - def __str__(self): - return self.title diff --git a/home/models/store.py b/home/models/store.py deleted file mode 100644 index 113f2c6..0000000 --- a/home/models/store.py +++ /dev/null @@ -1,44 +0,0 @@ -from django.db import models - - -class Category(models.Model): - name = models.CharField(max_length=100, unique=True) - - def __str__(self): - return self.name - -class Item(models.Model): - category = models.ForeignKey(Category, on_delete=models.CASCADE, related_name="items") - name = models.CharField(max_length=200) - price = models.DecimalField(max_digits=10, decimal_places=2) # Precio en euros - item_id = models.IntegerField(unique=True) # ID del ítem en el juego - image_url = models.URLField() # URL de la imagen del ítem - - def __str__(self): - return self.name - -class StripeLog(models.Model): - account_id = models.IntegerField() # ID de la cuenta en acore_auth - username = models.CharField(max_length=100) # Nombre de la cuenta - email = models.EmailField() # Correo electrónico - acore_ip = models.GenericIPAddressField() # IP de acore_auth - stripe_ip = models.GenericIPAddressField(blank=True, null=True) # IP detectada por Stripe - product_name = models.TextField() # Producto comprado - amount = models.DecimalField(max_digits=10, decimal_places=2) # Precio total - session_id = models.CharField(max_length=255) # ID de la sesión de Stripe - mode = models.CharField(max_length=10, choices=[("test", "Test"), ("live", "Live")]) # Modo de Stripe - character_name = models.CharField(max_length=100) # Nombre del personaje que compró - timestamp = models.DateTimeField(auto_now_add=True) # Fecha y hora - fulfilled = models.BooleanField(default=False) # Si la entrega ya se procesó (evita re-entregas) - - def __str__(self): - return f"{self.username} compró {self.product_name} por {self.amount}€ ({self.mode})" - -class Pedido(models.Model): - transaction_id = models.CharField(max_length=100, unique=True, null=True, blank=True) - email_cliente = models.EmailField() - estado = models.CharField(max_length=20, default="Pendiente") - monto = models.DecimalField(max_digits=10, decimal_places=2) - - def __str__(self): - return f"Pedido {self.transaction_id} - {self.estado}" diff --git a/home/models/voting.py b/home/models/voting.py deleted file mode 100644 index d65ca06..0000000 --- a/home/models/voting.py +++ /dev/null @@ -1,35 +0,0 @@ -from django.db import models - - -class VoteSite(models.Model): - name = models.CharField(max_length=100, unique=True) - url = models.URLField(max_length=200, help_text="URL del sitio de votación") - image_url = models.URLField(max_length=300, help_text="URL completa de la imagen") - points = models.IntegerField(default=1, help_text="Puntos otorgados por votar") - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - def __str__(self): - return self.name - -class VoteLog(models.Model): - account_id = models.IntegerField() - vote_site = models.ForeignKey('VoteSite', on_delete=models.CASCADE) - created_at = models.DateTimeField(auto_now_add=True) - last_vote_time = models.DateTimeField(null=True, blank=True) - processed = models.BooleanField(default=False) - processed_at = models.DateTimeField(null=True, blank=True) - - def __str__(self): - return f"Voto para {self.vote_site.name} por cuenta {self.account_id}" - -class HomeApiPoints(models.Model): - accountID = models.IntegerField(unique=True) - vp = models.IntegerField(default=0) - dp = models.IntegerField(default=0) - - class Meta: - db_table = 'home_api_points' - - def __str__(self): - return f"Cuenta {self.accountID} - PV: {self.vp}, DP: {self.dp}" diff --git a/home/pagos_stripe.py b/home/pagos_stripe.py deleted file mode 100644 index 3da337b..0000000 --- a/home/pagos_stripe.py +++ /dev/null @@ -1,63 +0,0 @@ -import stripe -from django.conf import settings -from .models import StripeLog -from django.utils.timezone import now -stripe.api_key = settings.STRIPE_SECRET_KEY - -def is_session_paid(session_id): - """True si la sesión de Stripe existe y está realmente pagada.""" - if not session_id: - return False - try: - s = stripe.checkout.Session.retrieve(session_id) - return bool(s and getattr(s, "payment_status", None) == "paid") - except Exception: - return False - - -def create_checkout_session(account_id, username, email, acore_ip, amount, character_name, currency="eur", success_url=None, cancel_url=None, product_name=""): - try: - # Añadir el id de sesión a la success_url para poder verificar el pago - # al volver (Stripe sustituye {CHECKOUT_SESSION_ID} por el id real). - if success_url: - sep = "&" if "?" in success_url else "?" - success_url = f"{success_url}{sep}session_id={{CHECKOUT_SESSION_ID}}" - - # 🔹 Crear sesión de pago en Stripe - session = stripe.checkout.Session.create( - payment_method_types=["card"], - line_items=[{ - "price_data": { - "currency": currency, - "product_data": {"name": product_name}, - "unit_amount": int(amount * 100), - }, - "quantity": 1, - }], - mode="payment", - success_url=success_url, - cancel_url=cancel_url, - ) - - # 🔹 Determinar si Stripe está en modo TEST o LIVE - mode = "live" if settings.STRIPE_SECRET_KEY.startswith("sk_live") else "test" - - # 🔹 Guardar el log en la base de datos - StripeLog.objects.create( - account_id=account_id, - username=username, - email=email, - acore_ip=acore_ip, - stripe_ip=None, # Se actualizará cuando llegue el webhook - product_name=product_name, - amount=amount, - session_id=session.id, - mode=mode, - character_name=character_name, - timestamp=now() - ) - - return {"success": True, "session_id": session.id} - - except Exception as e: - return {"success": False, "error": str(e)} \ No newline at end of file diff --git a/home/pagos_sumup.py b/home/pagos_sumup.py deleted file mode 100644 index 733e61f..0000000 --- a/home/pagos_sumup.py +++ /dev/null @@ -1,72 +0,0 @@ -from django.conf import settings -import requests - - -SUMUP_CLIENT_ID = settings.SUMUP_CLIENT_ID -SUMUP_CLIENT_SECRET = settings.SUMUP_CLIENT_SECRET -SUMUP_EMAIL = settings.SUMUP_MERCHANT_EMAIL - -API_BASE = "https://api.sumup.com" - - -def obtener_token(): - """Obtiene un access token de SumUp (client_credentials).""" - url = f"{API_BASE}/token" - data = { - "grant_type": "client_credentials", - "client_id": SUMUP_CLIENT_ID, - "client_secret": SUMUP_CLIENT_SECRET, - } - headers = {"Content-Type": "application/x-www-form-urlencoded"} - - response = requests.post(url, headers=headers, data=data, timeout=15) - if response.status_code == 200: - return response.json().get("access_token") - raise Exception(f"Error al obtener token: {response.text}") - - -def crear_checkout_battlepay(reference, monto, descripcion, moneda="EUR", return_url=None): - """ - Crea un checkout de SumUp para una orden Battlepay concreta. - - ``reference`` es la referencia única de la orden (battlepay_orders.reference); - se usa como ``checkout_reference`` para poder mapear el webhook a la orden. - Devuelve el JSON del checkout (incluye ``id``). - """ - access_token = obtener_token() - url = f"{API_BASE}/v0.1/checkouts" - headers = { - "Authorization": f"Bearer {access_token}", - "Content-Type": "application/json", - } - data = { - "checkout_reference": reference, - "amount": float(monto), - "currency": moneda, - "pay_to_email": SUMUP_EMAIL, - "description": descripcion, - } - if return_url: - data["return_url"] = return_url - - response = requests.post(url, headers=headers, json=data, timeout=15) - if response.status_code == 201: - return response.json() - raise Exception(f"Error al crear checkout: {response.text}") - - -def obtener_checkout(checkout_id): - """Consulta un checkout por id. Devuelve su JSON (status, checkout_reference...).""" - access_token = obtener_token() - url = f"{API_BASE}/v0.1/checkouts/{checkout_id}" - headers = {"Authorization": f"Bearer {access_token}"} - response = requests.get(url, headers=headers, timeout=15) - if response.status_code == 200: - return response.json() - raise Exception(f"Error al consultar checkout: {response.text}") - - -def crear_checkout(monto, moneda="EUR"): - """Compatibilidad: checkout genérico de prueba. Devuelve el checkout_id.""" - checkout = crear_checkout_battlepay("pedido-prueba", monto, "Pago de prueba", moneda) - return checkout.get("id") diff --git a/home/templates/404.html b/home/templates/404.html deleted file mode 100644 index 4165d2f..0000000 --- a/home/templates/404.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% include 'partials/not_found.html' %} - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} \ No newline at end of file diff --git a/home/templates/account/ban_history.html b/home/templates/account/ban_history.html deleted file mode 100644 index 20a44e7..0000000 --- a/home/templates/account/ban_history.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% include 'partials/ban_history.html' %} - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} \ No newline at end of file diff --git a/home/templates/account/battlepay.html b/home/templates/account/battlepay.html deleted file mode 100644 index 2ab4588..0000000 --- a/home/templates/account/battlepay.html +++ /dev/null @@ -1,60 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - -
-
-
-
-

Tienda del cliente (Battlepay)

-
-
-
-

Compras pendientes de pago de la cuenta {{ username }}.

-
- - {% if orders %} - - - - - - - - - - {% for order in orders %} - - - - - - {% endfor %} - -
ProductoPrecio
{{ order.product_name }}{{ order.price_eur }} € -
- {% csrf_token %} - - -
-
- {% else %} -

No tienes compras pendientes.

-
-

Para comprar, pulsa Comprar en la tienda del cliente dentro del juego; - la compra aparecerá aquí para completar el pago.

- {% endif %} - -
-

Volver a mi cuenta

-
-
-
-
-
- - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} diff --git a/home/templates/account/change_email.html b/home/templates/account/change_email.html deleted file mode 100644 index c224cff..0000000 --- a/home/templates/account/change_email.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% include 'partials/change_email.html' %} - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} \ No newline at end of file diff --git a/home/templates/account/change_faction.html b/home/templates/account/change_faction.html deleted file mode 100644 index aea0f0c..0000000 --- a/home/templates/account/change_faction.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% include 'partials/change_faction.html' %} - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} \ No newline at end of file diff --git a/home/templates/account/change_faction_cancel.html b/home/templates/account/change_faction_cancel.html deleted file mode 100644 index 67bc908..0000000 --- a/home/templates/account/change_faction_cancel.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% include 'partials/change_faction_cancel.html' %} - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} \ No newline at end of file diff --git a/home/templates/account/change_faction_success.html b/home/templates/account/change_faction_success.html deleted file mode 100644 index 54b72a2..0000000 --- a/home/templates/account/change_faction_success.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% include 'partials/change_faction_success.html' %} - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} \ No newline at end of file diff --git a/home/templates/account/change_password.html b/home/templates/account/change_password.html deleted file mode 100644 index 1a25cf1..0000000 --- a/home/templates/account/change_password.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% include 'partials/change_password.html' %} - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} \ No newline at end of file diff --git a/home/templates/account/change_race.html b/home/templates/account/change_race.html deleted file mode 100644 index 105bd6d..0000000 --- a/home/templates/account/change_race.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% include 'partials/change_race.html' %} - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} \ No newline at end of file diff --git a/home/templates/account/change_race_cancel.html b/home/templates/account/change_race_cancel.html deleted file mode 100644 index a8c733c..0000000 --- a/home/templates/account/change_race_cancel.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% include 'partials/change_race_success.html' %} - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} \ No newline at end of file diff --git a/home/templates/account/change_race_success.html b/home/templates/account/change_race_success.html deleted file mode 100644 index a8c733c..0000000 --- a/home/templates/account/change_race_success.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% include 'partials/change_race_success.html' %} - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} \ No newline at end of file diff --git a/home/templates/account/confirm_new_email_success.html b/home/templates/account/confirm_new_email_success.html deleted file mode 100644 index 40745ea..0000000 --- a/home/templates/account/confirm_new_email_success.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% include 'partials/confirm_new_email_success.html' %} - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} \ No newline at end of file diff --git a/home/templates/account/confirm_old_email_success.html b/home/templates/account/confirm_old_email_success.html deleted file mode 100644 index e507d48..0000000 --- a/home/templates/account/confirm_old_email_success.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% include 'partials/confirm_old_email_success.html' %} - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} \ No newline at end of file diff --git a/home/templates/account/customize_character.html b/home/templates/account/customize_character.html deleted file mode 100644 index 8f28f6e..0000000 --- a/home/templates/account/customize_character.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% include 'partials/customize_character.html' %} - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} \ No newline at end of file diff --git a/home/templates/account/customize_success.html b/home/templates/account/customize_success.html deleted file mode 100644 index 871800a..0000000 --- a/home/templates/account/customize_success.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% include 'partials/customize_success.html' %} - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} \ No newline at end of file diff --git a/home/templates/account/d_points.html b/home/templates/account/d_points.html deleted file mode 100644 index ca53a3e..0000000 --- a/home/templates/account/d_points.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% include 'partials/d_points.html' %} - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} \ No newline at end of file diff --git a/home/templates/account/expired_link.html b/home/templates/account/expired_link.html deleted file mode 100644 index 1e11adf..0000000 --- a/home/templates/account/expired_link.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% include 'partials/expired_link.html' %} - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} \ No newline at end of file diff --git a/home/templates/account/gold_cancel.html b/home/templates/account/gold_cancel.html deleted file mode 100644 index fa9adf7..0000000 --- a/home/templates/account/gold_cancel.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% include 'partials/gold_cancel.html' %} - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} \ No newline at end of file diff --git a/home/templates/account/gold_character.html b/home/templates/account/gold_character.html deleted file mode 100644 index 729f360..0000000 --- a/home/templates/account/gold_character.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% include 'partials/gold_character.html' %} - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} \ No newline at end of file diff --git a/home/templates/account/gold_success.html b/home/templates/account/gold_success.html deleted file mode 100644 index 5bfc409..0000000 --- a/home/templates/account/gold_success.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% include 'partials/gold_success.html' %} - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} \ No newline at end of file diff --git a/home/templates/account/guild_rename_cancel.html b/home/templates/account/guild_rename_cancel.html deleted file mode 100644 index 27abea5..0000000 --- a/home/templates/account/guild_rename_cancel.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% include 'partials/guild_rename_cancel.html' %} - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} \ No newline at end of file diff --git a/home/templates/account/guild_rename_success.html b/home/templates/account/guild_rename_success.html deleted file mode 100644 index d721a4b..0000000 --- a/home/templates/account/guild_rename_success.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% include 'partials/guild_rename_success.html' %} - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} \ No newline at end of file diff --git a/home/templates/account/level_up.html b/home/templates/account/level_up.html deleted file mode 100644 index 2361013..0000000 --- a/home/templates/account/level_up.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% include 'partials/level_up.html' %} - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} \ No newline at end of file diff --git a/home/templates/account/level_up_cancel.html b/home/templates/account/level_up_cancel.html deleted file mode 100644 index 0d3de10..0000000 --- a/home/templates/account/level_up_cancel.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% include 'partials/level_up_cancel.html' %} - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} \ No newline at end of file diff --git a/home/templates/account/level_up_success.html b/home/templates/account/level_up_success.html deleted file mode 100644 index 0898ee6..0000000 --- a/home/templates/account/level_up_success.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% include 'partials/level_up_success.html' %} - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} \ No newline at end of file diff --git a/home/templates/account/pago_sumup.html b/home/templates/account/pago_sumup.html deleted file mode 100644 index 4aff5c4..0000000 --- a/home/templates/account/pago_sumup.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% include 'partials/pago_sumup.html' %} - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} \ No newline at end of file diff --git a/home/templates/account/points_history.html b/home/templates/account/points_history.html deleted file mode 100644 index aabb39a..0000000 --- a/home/templates/account/points_history.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% include 'partials/points_history.html' %} - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} \ No newline at end of file diff --git a/home/templates/account/promo_code.html b/home/templates/account/promo_code.html deleted file mode 100644 index a753315..0000000 --- a/home/templates/account/promo_code.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% include 'partials/promo_code.html' %} - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} \ No newline at end of file diff --git a/home/templates/account/quest_character.html b/home/templates/account/quest_character.html deleted file mode 100644 index 3828f07..0000000 --- a/home/templates/account/quest_character.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% include 'partials/quest_character.html' %} - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} \ No newline at end of file diff --git a/home/templates/account/rename_cancel.html b/home/templates/account/rename_cancel.html deleted file mode 100644 index b5c1281..0000000 --- a/home/templates/account/rename_cancel.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% include 'partials/rename_cancel.html' %} - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} \ No newline at end of file diff --git a/home/templates/account/rename_character.html b/home/templates/account/rename_character.html deleted file mode 100644 index 94ad7f2..0000000 --- a/home/templates/account/rename_character.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% include 'partials/rename_character.html' %} - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} \ No newline at end of file diff --git a/home/templates/account/rename_guild.html b/home/templates/account/rename_guild.html deleted file mode 100644 index da55ea3..0000000 --- a/home/templates/account/rename_guild.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% include 'partials/rename_guild.html' %} - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} \ No newline at end of file diff --git a/home/templates/account/rename_success.html b/home/templates/account/rename_success.html deleted file mode 100644 index 4b62dbf..0000000 --- a/home/templates/account/rename_success.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% include 'partials/rename_success.html' %} - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} \ No newline at end of file diff --git a/home/templates/account/restore_character.html b/home/templates/account/restore_character.html deleted file mode 100644 index 9540966..0000000 --- a/home/templates/account/restore_character.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% include 'partials/restore_character.html' %} - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} \ No newline at end of file diff --git a/home/templates/account/restore_items.html b/home/templates/account/restore_items.html deleted file mode 100644 index 42665dc..0000000 --- a/home/templates/account/restore_items.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% include 'partials/restore_items.html' %} - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} \ No newline at end of file diff --git a/home/templates/account/revive_character.html b/home/templates/account/revive_character.html deleted file mode 100644 index f3a0c0e..0000000 --- a/home/templates/account/revive_character.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% include 'partials/revive_character.html' %} - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} \ No newline at end of file diff --git a/home/templates/account/security_history.html b/home/templates/account/security_history.html deleted file mode 100644 index d2f8e5a..0000000 --- a/home/templates/account/security_history.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% include 'partials/security_history.html' %} - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} \ No newline at end of file diff --git a/home/templates/account/security_token.html b/home/templates/account/security_token.html deleted file mode 100644 index 88656e0..0000000 --- a/home/templates/account/security_token.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% include 'partials/security_token.html' %} - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} \ No newline at end of file diff --git a/home/templates/account/send_gift.html b/home/templates/account/send_gift.html deleted file mode 100644 index 83ee602..0000000 --- a/home/templates/account/send_gift.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% include 'partials/send_gift.html' %} - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} \ No newline at end of file diff --git a/home/templates/account/trade_points.html b/home/templates/account/trade_points.html deleted file mode 100644 index 23cf620..0000000 --- a/home/templates/account/trade_points.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% include 'partials/trade_points.html' %} - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} \ No newline at end of file diff --git a/home/templates/account/trans_history.html b/home/templates/account/trans_history.html deleted file mode 100644 index 975d036..0000000 --- a/home/templates/account/trans_history.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% include 'partials/trans_history.html' %} - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} \ No newline at end of file diff --git a/home/templates/account/transfer_cancel.html b/home/templates/account/transfer_cancel.html deleted file mode 100644 index c1fb1d6..0000000 --- a/home/templates/account/transfer_cancel.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% include 'partials/transfer_cancel.html' %} - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} \ No newline at end of file diff --git a/home/templates/account/transfer_character.html b/home/templates/account/transfer_character.html deleted file mode 100644 index fb6577e..0000000 --- a/home/templates/account/transfer_character.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% include 'partials/transfer_character.html' %} - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} \ No newline at end of file diff --git a/home/templates/account/transfer_d_points.html b/home/templates/account/transfer_d_points.html deleted file mode 100644 index 8cebf43..0000000 --- a/home/templates/account/transfer_d_points.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% include 'partials/transfer_d_points.html' %} - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} \ No newline at end of file diff --git a/home/templates/account/transfer_success.html b/home/templates/account/transfer_success.html deleted file mode 100644 index a7c10a1..0000000 --- a/home/templates/account/transfer_success.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% include 'partials/transfer_success.html' %} - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} \ No newline at end of file diff --git a/home/templates/account/unstuck_character.html b/home/templates/account/unstuck_character.html deleted file mode 100644 index 4fa7a83..0000000 --- a/home/templates/account/unstuck_character.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% include 'partials/unstuck_character.html' %} - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} \ No newline at end of file diff --git a/home/templates/account/vote_points.html b/home/templates/account/vote_points.html deleted file mode 100644 index fef07f8..0000000 --- a/home/templates/account/vote_points.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% include 'partials/vote_points.html' %} - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} \ No newline at end of file diff --git a/home/templates/admin/server_selection.html b/home/templates/admin/server_selection.html deleted file mode 100644 index d9ed680..0000000 --- a/home/templates/admin/server_selection.html +++ /dev/null @@ -1,44 +0,0 @@ -{% extends "admin/change_list.html" %} -{% load i18n %} - -{% block content %} -
-

{% trans "Seleccionar Servidor" %}

-
- {% csrf_token %} -
- - -
- {% trans "Debe seleccionar un servidor antes de continuar." %} -
-
-
- -
-
-
- -{% endblock %} diff --git a/home/templates/auth/activation_invalid.html b/home/templates/auth/activation_invalid.html deleted file mode 100644 index fb63f1f..0000000 --- a/home/templates/auth/activation_invalid.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% include 'partials/activation_invalid.html' %} - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} \ No newline at end of file diff --git a/home/templates/auth/activation_success.html b/home/templates/auth/activation_success.html deleted file mode 100644 index b76b55d..0000000 --- a/home/templates/auth/activation_success.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% include 'partials/activation_success.html' %} - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} \ No newline at end of file diff --git a/home/templates/auth/login.html b/home/templates/auth/login.html deleted file mode 100644 index 8008f09..0000000 --- a/home/templates/auth/login.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% include 'partials/login.html' %} - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} \ No newline at end of file diff --git a/home/templates/auth/recover_account.html b/home/templates/auth/recover_account.html deleted file mode 100644 index e310bf8..0000000 --- a/home/templates/auth/recover_account.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% include 'partials/recover_account.html' %} - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} \ No newline at end of file diff --git a/home/templates/auth/register.html b/home/templates/auth/register.html deleted file mode 100644 index b4fbea9..0000000 --- a/home/templates/auth/register.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% include 'partials/register.html' %} - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} \ No newline at end of file diff --git a/home/templates/auth/reset_password.html b/home/templates/auth/reset_password.html deleted file mode 100644 index b0fcd47..0000000 --- a/home/templates/auth/reset_password.html +++ /dev/null @@ -1,29 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% load django_vite %} -
-
-
-
-

Restablecer contraseña

-
-
- -
- {% vite_asset 'src/entries/reset_password.tsx' %} -
-
-
-
- {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} diff --git a/home/templates/auth/select_account.html b/home/templates/auth/select_account.html deleted file mode 100644 index c36b647..0000000 --- a/home/templates/auth/select_account.html +++ /dev/null @@ -1,45 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - -
-
-
-
-

Selecciona tu cuenta de juego

-
-
-
- {% if bnet_email %} -

Conectado como {{ bnet_email }}

-
- {% endif %} - - {% if game_accounts %} -

Tu cuenta Battle.net tiene varias cuentas de juego. Elige con cuál quieres continuar:

-
- {% load django_vite %} - -
- {{ game_accounts|json_script:"select-account-data" }} - {% vite_asset 'src/entries/select_account.tsx' %} - {% else %} -

No se han encontrado cuentas de juego asociadas a esta cuenta Battle.net.

-
-

Contacta con el soporte si crees que es un error.

- {% endif %} - -
-

Cerrar sesión

-
-
-
-
-
- - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} diff --git a/home/templates/base/base.html b/home/templates/base/base.html deleted file mode 100644 index 8d2c5af..0000000 --- a/home/templates/base/base.html +++ /dev/null @@ -1,10 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/start_time.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% include 'partials/noticias.html' %} - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} \ No newline at end of file diff --git a/home/templates/community/content_creators.html b/home/templates/community/content_creators.html deleted file mode 100644 index 80348ad..0000000 --- a/home/templates/community/content_creators.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% include 'partials/videos.html' %} - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} \ No newline at end of file diff --git a/home/templates/community/help.html b/home/templates/community/help.html deleted file mode 100644 index 2cd8b66..0000000 --- a/home/templates/community/help.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% include 'partials/help.html' %} - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} \ No newline at end of file diff --git a/home/templates/contact/contact_us.html b/home/templates/contact/contact_us.html deleted file mode 100644 index e0c88a6..0000000 --- a/home/templates/contact/contact_us.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% include 'partials/contact_us.html' %} - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} \ No newline at end of file diff --git a/home/templates/download/download_client.html b/home/templates/download/download_client.html deleted file mode 100644 index 1794ae0..0000000 --- a/home/templates/download/download_client.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% include 'partials/descargas.html' %} - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} \ No newline at end of file diff --git a/home/templates/emails/account_names.html b/home/templates/emails/account_names.html deleted file mode 100644 index d734682..0000000 --- a/home/templates/emails/account_names.html +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - -
- - - - -
-

{{ NOMBRE_SERVIDOR }}

-

Tus cuentas de juego

-

Hola,

-

Estas son las cuentas de juego asociadas a tu cuenta Battle.net ({{ email }}). Recuerda: para iniciar sesión en la web usas tu correo electrónico.

- - {% for a in accounts %} - - - - {% empty %} - - - - {% endfor %} -
{{ a.username }}
No hay cuentas de juego asociadas todavía.
-

Si no has solicitado esta información, ignora este correo.

-
-
- - diff --git a/home/templates/emails/activation.html b/home/templates/emails/activation.html deleted file mode 100644 index 48f8fb2..0000000 --- a/home/templates/emails/activation.html +++ /dev/null @@ -1,145 +0,0 @@ - - - - - -
- - - - -
- - - - - -
- - - - -

ACTIVACIÓN DE CUENTA

- - - - - -
- - - - -
- - - - -
-
- - - - -
- - - - - - - -
- - - - -
- - - - -
- - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Bienvenido, {{ username }}

Gracias por crear una cuenta en {{ NOMBRE_SERVIDOR }}.
Para poder empezar a usar tu cuenta, deberás activarla haciendo clic en el botón ACTIVAR LA CUENTA.

Tus datos de cuenta son:

Cuenta: {{ username }}
Contraseña: {{ password }}

Con tu usuario y contraseña podrás entrar a nuestro sitio web y al servidor.

- - - - -

- Al servidor se debe entrar con nombre de usuario y contraseña. NO con el correo.
- Puedes usar el mismo correo para registrar hasta 10 cuentas en el servidor.
- El usuario del foro es independiente del usuario que acabas de crear. Puedes crear una cuenta para el foro aquí.

ACTIVAR LA CUENTA

Si no puedes activar la cuenta usando el botón, copia y pega en tu navegador siguiente enlace:
{{ activation_link }}



Atentamente, el Equipo de {{ NOMBRE_SERVIDOR }}.

- - - - -
- - - - - - - -
- - - - -
- - - - - - - -

SÍGUENOS EN

- - - - - - - -
FbIgTwYt
- - - - -
- - - - -

Términos y Condiciones | Política de Reembolso | Política de Privacidad
{{ NOMBRE_SERVIDOR }} © {{ ANIO_ACTUAL }}

diff --git a/home/templates/emails/confirm_new_email.html b/home/templates/emails/confirm_new_email.html deleted file mode 100644 index ed97fd0..0000000 --- a/home/templates/emails/confirm_new_email.html +++ /dev/null @@ -1,28 +0,0 @@ - - - - - Activación del Nuevo Correo - - - - - - - - - -
-

Activación de Nuevo Correo

-
-

Hola {{ username }},

-

Tu solicitud para cambiar el correo electrónico en {{ NOMBRE_SERVIDOR }} ha sido recibida.

-

Por favor, haz clic en el siguiente enlace para confirmar tu nuevo correo:

-

- Activar nuevo correo -

-

Si no realizaste esta solicitud, por favor contacta con soporte.

-

Atentamente,
El equipo de {{ NOMBRE_SERVIDOR }}

-
- - diff --git a/home/templates/emails/confirm_old_email.html b/home/templates/emails/confirm_old_email.html deleted file mode 100644 index 1c7c0f4..0000000 --- a/home/templates/emails/confirm_old_email.html +++ /dev/null @@ -1,28 +0,0 @@ - - - - - Confirmación de Cambio de Correo - - - - - - - - - -
-

Confirmación de Cambio de Correo

-
-

Hola {{ username }},

-

Has solicitado cambiar tu correo electrónico registrado en {{ NOMBRE_SERVIDOR }}.

-

Para confirmar este cambio, haz clic en el siguiente enlace:

-

- Confirmar cambio -

-

Si no solicitaste este cambio, por favor ignora este correo.

-

Atentamente,
El equipo de {{ NOMBRE_SERVIDOR }}

-
- - diff --git a/home/templates/emails/old_email_notification.html b/home/templates/emails/old_email_notification.html deleted file mode 100644 index 6afbed1..0000000 --- a/home/templates/emails/old_email_notification.html +++ /dev/null @@ -1,27 +0,0 @@ - - - - - Notificación de Cambio de Correo - - - - - - - - - -
-

Cambio de Correo Exitoso

-
-

Hola {{ username }},

-

Te notificamos que tu correo electrónico en {{ NOMBRE_SERVIDOR }} ha sido cambiado exitosamente al siguiente correo:

-

- {{ new_email }} -

-

Si no realizaste este cambio, por favor contacta inmediatamente con soporte.

-

Atentamente,
El equipo de {{ NOMBRE_SERVIDOR }}

-
- - diff --git a/home/templates/emails/password_reset.html b/home/templates/emails/password_reset.html deleted file mode 100644 index f81519f..0000000 --- a/home/templates/emails/password_reset.html +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - -
- - - - -
-

{{ NOMBRE_SERVIDOR }}

-

Restablecer contraseña

-

Hola,

-

Hemos recibido una solicitud para restablecer la contraseña de la cuenta Battle.net asociada a {{ email }}.

-

Pulsa el botón para elegir una nueva contraseña. El enlace caduca en 1 hora.

-

- Restablecer contraseña -

-

Si el botón no funciona, copia esta dirección en tu navegador:

-

{{ reset_link }}

-

Si no has solicitado este cambio, ignora este correo: tu contraseña no se modificará.

-
-
- - diff --git a/home/templates/emails/security_token.html b/home/templates/emails/security_token.html deleted file mode 100644 index 78fc381..0000000 --- a/home/templates/emails/security_token.html +++ /dev/null @@ -1,163 +0,0 @@ -
- - - - - - - - - - - - - -
-
- - - - - -
- - - - -
- - - - -
- - - - - -
- - - - -

TOKEN DE SEGURIDAD

- - - - - -
- - - - -
- - - - -
-
- - - - -
- - - - - - - -
- - - - -
- - - - -
- - - - -
- - - - - - - - - - - - - - - - - - - -

Token de Seguridad

Alguien desde la IP {{ ip_address }} ha solicitado el Token de seguridad para la cuenta {{ username }}.
Si no has sido tú, te recomendamos cambiar la contraseña de tu cuenta.

Tus datos de cuenta son:

Cuenta: {{ username }}
Token de Seguridad: {{ token }}

Recuerda que este Token es sensible a mayúsculas y minúsculas.
Al usarlo, escríbelo exactamente de la misma forma que aparece aquí.

- - - - -

¡No compartas el Token de Seguridad con nadie!


Atentamente, el Equipo de {{ NOMBRE_SERVIDOR }}.

- - - - -
- - - - - - - -
- - - - -
- - - - - - - -

SÍGUENOS EN

- - - - - - - -
FbIgTwYt
- - - - -
- - - - -

Términos y Condiciones | Política de Reembolso | Política de Privacidad
{{ NOMBRE_SERVIDOR }} © {{ ANIO_ACTUAL }}

-
-
- -
- -
\ No newline at end of file diff --git a/home/templates/home/home.html b/home/templates/home/home.html deleted file mode 100644 index 758ece4..0000000 --- a/home/templates/home/home.html +++ /dev/null @@ -1 +0,0 @@ -{% extends 'base/base.html' %} \ No newline at end of file diff --git a/home/templates/legal/cookies.html b/home/templates/legal/cookies.html deleted file mode 100644 index 1759685..0000000 --- a/home/templates/legal/cookies.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% include 'partials/cookies.html' %} - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} \ No newline at end of file diff --git a/home/templates/legal/legal_notice.html b/home/templates/legal/legal_notice.html deleted file mode 100644 index a46bcc6..0000000 --- a/home/templates/legal/legal_notice.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% include 'partials/legal_notice.html' %} - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} \ No newline at end of file diff --git a/home/templates/legal/privacy_policy.html b/home/templates/legal/privacy_policy.html deleted file mode 100644 index 3698e55..0000000 --- a/home/templates/legal/privacy_policy.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% include 'partials/privacy_policy.html' %} - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} \ No newline at end of file diff --git a/home/templates/legal/refund_policy.html b/home/templates/legal/refund_policy.html deleted file mode 100644 index f59a073..0000000 --- a/home/templates/legal/refund_policy.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% include 'partials/refund_policy.html' %} - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} \ No newline at end of file diff --git a/home/templates/legal/terms_and_conditions.html b/home/templates/legal/terms_and_conditions.html deleted file mode 100644 index 96991cd..0000000 --- a/home/templates/legal/terms_and_conditions.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% include 'partials/terms_and_conditions.html' %} - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} \ No newline at end of file diff --git a/home/templates/maintenance/maintenance.html b/home/templates/maintenance/maintenance.html deleted file mode 100644 index bed3b25..0000000 --- a/home/templates/maintenance/maintenance.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% include 'partials/maintenance.html' %} - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} \ No newline at end of file diff --git a/home/templates/my-account/my-account.html b/home/templates/my-account/my-account.html deleted file mode 100644 index 50d1147..0000000 --- a/home/templates/my-account/my-account.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% include 'partials/my-account.html' %} - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} \ No newline at end of file diff --git a/home/templates/partials/activation_invalid.html b/home/templates/partials/activation_invalid.html deleted file mode 100644 index ecc08b2..0000000 --- a/home/templates/partials/activation_invalid.html +++ /dev/null @@ -1,14 +0,0 @@ -
-
-
-

Activación de cuenta

-
-
-

El enlace de activación es inválido

-
-

Si necesitas ayuda para crear una cuenta, puedes contactar con el equipo de {{ NOMBRE_SERVIDOR }}.

-
-
-
-
-
\ No newline at end of file diff --git a/home/templates/partials/activation_success.html b/home/templates/partials/activation_success.html deleted file mode 100644 index 4019d2e..0000000 --- a/home/templates/partials/activation_success.html +++ /dev/null @@ -1,17 +0,0 @@ -
-
-
-

Activación de cuenta

-
-
-
-

¡Bienvenido a {{ NOMBRE_SERVIDOR }}!

-

La cuenta {{ username }} ha sido activada exitosamente.

-
-

Ya puedes conectar a la página web o empezar a disfrutar del servidor.

-
-
-
-
-
-
\ No newline at end of file diff --git a/home/templates/partials/ban_history.html b/home/templates/partials/ban_history.html deleted file mode 100644 index fc7d026..0000000 --- a/home/templates/partials/ban_history.html +++ /dev/null @@ -1,36 +0,0 @@ -
-
-
-
-

Historial de sanciones

-
-
-
-

Información

- - -
-
-
-

Aquí podrás ver todas las sanciones que ha tenido la cuenta.

-

Si tienes una sanción vigente, verás que su estado indica Activo.

-

Para evitar que tu cuenta sea sancionada, recuerda respetar las normas de nuestro servidor.

-
-
-
- - -
-
-

Historial de baneos

-
-
- {% load django_vite %} - -
- {{ ban_rows|json_script:"history-table-data" }} - {% vite_asset 'src/entries/history_table.tsx' %} -
-
\ No newline at end of file diff --git a/home/templates/partials/change_email.html b/home/templates/partials/change_email.html deleted file mode 100644 index 7208db9..0000000 --- a/home/templates/partials/change_email.html +++ /dev/null @@ -1,36 +0,0 @@ -
-
-
-

Cambiar correo

-
- -
-
-

El correo actual debe escribirse tal cual figura en información de cuenta, respetando mayúsculas y minúsculas.

-

El correo nuevo debe ser funcional y pertenecer al dominio gmail.

-

Escoje un correo al que nadie más tenga acceso.

-
-

Si aún no tienes Token de seguridad, puedes solicitarlo aquí.

-
-
-
- -{% load django_vite %} - -
-{% vite_asset 'src/entries/change_email.tsx' %} - - - - - -
-
-
-
-
-
-
\ No newline at end of file diff --git a/home/templates/partials/change_faction.html b/home/templates/partials/change_faction.html deleted file mode 100644 index ce7da58..0000000 --- a/home/templates/partials/change_faction.html +++ /dev/null @@ -1,74 +0,0 @@ -
-
-
-

Cambiar facción del personaje

-
- -
-
-

La herramienta Cambiar facción del personaje te permite cambiar de la Alianza a la Horda y viceversa.

-

Si también deseas cambiar el nombre del personaje, debes usar la herramienta Renombrar personaje.

-
-

Al hacer un cambio de Facción:

-

- Los items, hechizos, títulos, reputaciones, monturas y logros se cambiarán a la nueva facción

-

- Las misiones activas en el Registro de misiones serán abandonadas

-

- Los teams de arena seán borrados

-

- Los amigos en la Lista de amigos se borrarán

-
-

Restricciones del personaje:

-

- No debe tener subastas activas

-

- No debe tener correos en el buzón

-

- No debe capitán de un team de arenas

-

- No debe ser miembro de una hermandad

-

- No debe tener demasiado oro

-
- - - - - - - - - - - - - - - - - - - - - -
NivelMax. Oro
10-30300
31-501000
51-705000
71-8020000
-
-

Al usar el botón CAMBIAR FACCIÓN del personaje que hayas escogido, se enviará una petición de cambio de facción de dicho personaje.

-

Cuando ingreses a la lista de personajes de tu cuenta, verás un ícono a la izquierda de tu personaje escogido.

-

Al darle click a dicho ícono ingresarás al menú de edición de personaje que te permitirá pasar a la facción contraria.

-
-
- NOTA -
-

Por favor, asegúrate de haber escogido al personaje correcto ya que esta acción no es reversible una vez realizada.

-
-
- {% load django_vite %} - -
- {{ characters|json_script:"character-purchase-data" }} - {% vite_asset 'src/entries/character_purchase.tsx' %} -
-
-
-
-
-
- \ No newline at end of file diff --git a/home/templates/partials/change_faction_cancel.html b/home/templates/partials/change_faction_cancel.html deleted file mode 100644 index e69de29..0000000 diff --git a/home/templates/partials/change_faction_success.html b/home/templates/partials/change_faction_success.html deleted file mode 100644 index e69de29..0000000 diff --git a/home/templates/partials/change_password.html b/home/templates/partials/change_password.html deleted file mode 100644 index a6fe3c8..0000000 --- a/home/templates/partials/change_password.html +++ /dev/null @@ -1,30 +0,0 @@ -
-
-
-

Cambiar contraseña

-
- -
-
-

La contraseña nueva debe ser alfanumérica y con una longitud de hasta 16 caracteres.

-

Escoje una contraseña que sea difícil de adivinar por los demás y guárdala en un lugar seguro.

-
-

Si aún no tienes Token de seguridad, puedes solicitarlo aquí.

-
-
-
- {% load django_vite %} - -
- {% vite_asset 'src/entries/change_password.tsx' %} -
-
-
-
-
-
-
\ No newline at end of file diff --git a/home/templates/partials/change_race.html b/home/templates/partials/change_race.html deleted file mode 100644 index 1d21bb2..0000000 --- a/home/templates/partials/change_race.html +++ /dev/null @@ -1,38 +0,0 @@ -
-
-
-

Cambiar raza del personaje

-
- -
-
-

La herramienta Cambiar raza del personaje te permite cambiar la raza del personaje entre otras de la misma facción.

-

Si también deseas cambiar el nombre del personaje, debes usar la herramienta Renombrar personaje.

-
-

Al usar el botón CAMBIAR RAZA del personaje que hayas escogido, se enviará una petición de cambio de raza de dicho personaje.

-

Cuando ingreses a la lista de personajes de tu cuenta, verás un ícono a la izquierda de tu personaje escogido.

-

Al darle click a dicho ícono ingresarás al menú de edición de personaje que te permitirá escoger entre las razas de la misma facción.

-
-
- NOTA -
-

Por favor, asegúrate de haber escogido al personaje correcto ya que esta acción no es reversible una vez realizada.

-
-
- {% load django_vite %} - -
- {{ characters|json_script:"character-purchase-data" }} - {% vite_asset 'src/entries/character_purchase.tsx' %} -
-
-
-
-
-
-
\ No newline at end of file diff --git a/home/templates/partials/change_race_cancel.html b/home/templates/partials/change_race_cancel.html deleted file mode 100644 index e69de29..0000000 diff --git a/home/templates/partials/change_race_success.html b/home/templates/partials/change_race_success.html deleted file mode 100644 index 8b13789..0000000 --- a/home/templates/partials/change_race_success.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/home/templates/partials/confirm_new_email_success.html b/home/templates/partials/confirm_new_email_success.html deleted file mode 100644 index 918a284..0000000 --- a/home/templates/partials/confirm_new_email_success.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - - Confirmación de Nuevo Correo - - - -
-

Cambio de Correo Completado

-

El cambio de correo ha sido confirmado y completado con éxito.

- Regresar a Mi Cuenta -
- - diff --git a/home/templates/partials/confirm_old_email_success.html b/home/templates/partials/confirm_old_email_success.html deleted file mode 100644 index b96b992..0000000 --- a/home/templates/partials/confirm_old_email_success.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - - Confirmación de Correo Anterior - - - -
-

Confirmación de Correo Anterior Exitosa

-

Has confirmado tu correo anterior. Se ha enviado un enlace al nuevo correo para completar el cambio.

- Regresar a Mi Cuenta -
- - diff --git a/home/templates/partials/contact_us.html b/home/templates/partials/contact_us.html deleted file mode 100644 index b33cbc9..0000000 --- a/home/templates/partials/contact_us.html +++ /dev/null @@ -1,23 +0,0 @@ -
-
-
-

Contáctanos

-
-
-

¡Gracias por tu interés en contactar con nosotros!

-

Si tienes alguna pregunta, consulta o comentario, estaremos encantados de ayudarte. Por favor, envía un correo electrónico a la siguiente dirección:

-

contacto@novawow.com

-
-

Error 404 es la empresa que representa el servicio de {{ NOMBRE_SERVIDOR }}.

-

Nos esforzamos por responder a todos los mensajes en un plazo de 48 horas hábiles.

-

Asegúrate de incluir la información relevante en tu correo para que podamos brindarte una respuesta precisa y oportuna.

-
-

En Error 404, valoramos tus comentarios y opiniones. Tu satisfacción es nuestra prioridad y nos esforzamos por brindarte la mejor experiencia posible en {{ NOMBRE_SERVIDOR }}.

-

¡Esperamos tener noticias tuyas pronto!

-
-

Atentamente, el equipo de Error 404.

-
-
-
-
-
\ No newline at end of file diff --git a/home/templates/partials/cookies.html b/home/templates/partials/cookies.html deleted file mode 100644 index 834b98e..0000000 --- a/home/templates/partials/cookies.html +++ /dev/null @@ -1,12 +0,0 @@ -
-
-
-

Declaración de Cookies

-
-
-

Esta página web usa cookies. Las cookies de este sitio web se usan para personalizar el contenido y los anuncios, ofrecer funciones de redes sociales y analizar el tráfico. Además, compartimos información sobre el uso que haga del sitio web con nuestros partners de redes sociales, publicidad y análisis web, quienes pueden combinarla con otra información que les haya proporcionado o que hayan recopilado a partir del uso que haya hecho de sus servicios.

Las cookies son pequeños archivos de texto que las páginas web pueden utilizar para hacer más eficiente la experiencia del usuario.

La ley afirma que podemos almacenar cookies en su dispositivo si son estrictamente necesarias para el funcionamiento de esta página. Para todos los demás tipos de cookies necesitamos su permiso.

Esta página utiliza tipos diferentes de cookies. Algunas cookies son colocadas por servicios de terceros que aparecen en nuestras páginas.

En cualquier momento puede cambiar o retirar su consentimiento desde la Declaración de cookies en nuestro sitio web.

Obtenga más información sobre quiénes somos, cómo puede contactarnos y cómo procesamos los datos personales en nuestra Política de privacidad.

Al contactarnos respecto a su consentimiento, por favor, indique el ID y la fecha de su consentimiento.

Su consentimiento se aplica a los siguientes dominios: foro.novawow.com, wotlk.novawow.com, novawow.com

Tu estado actual: Permitir todas. 

ID de tu consentimiento: dWfhmXtShMdSvyB/zqqHOTipyrI+25AS0bn6304RDF5yHDSRbf0YUA==Fecha del consentimiento: viernes, 8 de noviembre de 2024, 19:34:03 CET


Declaración de cookies actualizada por última vez el 10/9/24 por Cookiebot:

Necesario (24)

Las cookies necesarias ayudan a hacer una página web utilizable activando funciones básicas como la navegación en la página y el acceso a áreas seguras de la página web. La página web no puede funcionar adecuadamente sin estas cookies.

Nombre Proveedor Propósito Duración máxima de almacenamiento Tipo
__cf_bm [x3] artstation.com
discordapp.com
vimeo.com
Esta cookie se utiliza para distinguir entre humanos y bots. Esto es beneficioso para la web con el objeto de elaborar informes válidos sobre el uso de su web. 1 día Cookie HTTP
__cfruid discordapp.net Esta cookie es una parte de los servicios provistos por Cloudflare – Incluyendo el equilibrio de carga, entrega del contenido de la web y conexión al servidor DNS para operadores de la web. Sesión Cookie HTTP
_cfuvid [x3] discordapp.com
discordapp.net
vimeo.com
Esta cookie es una parte de los servicios provistos por Cloudflare – Incluyendo el equilibrio de carga, entrega del contenido de la web y conexión al servidor DNS para operadores de la web. Sesión Cookie HTTP
1.gif imgsct.cookiebot.com Se utiliza para contar el número de sesiones del sitio web, necesario para optimizar la entrega de productos CMP. Sesión Píxel de Seguimiento
cf_clearance novawow.com Esta cookie se utiliza para distinguir entre humanos y bots. 1 año Cookie HTTP
CookieConsent [x3] foro.novawow.com
novawow.com
wotlk.novawow.com
Almacena el estado de consentimiento de cookies del usuario para el dominio actual 1 año Cookie HTTP
cookietest novawow.com Esta cookie se utiliza para determinar si el visitante ha aceptado la casilla de autorización de uso de cookies. Sesión Cookie HTTP
csrf_token reddit.com Asegura la navegación segura del visitante previniendo la falsificación de petición en sitios cruzados (CSRF). Esta cookie es esencial para la seguridad de la web y del visitante. Sesión Cookie HTTP
GCLB gyazo.com Esta cookie se utiliza en relación al equilibrio de carga - Esto optimiza el índice de respuesta entre el visitante y la web, distribuyendo la carga de tráfico entre varios enlaces de red o servidores. Sesión Cookie HTTP
GPS youtube.com Empleado para determinar si un usuario ha iniciado sesión en una cuenta de YouTube, cuando visualiza vídeos insertados. 1 día Cookie HTTP
PHPSESSID [x4] files.fm
imgbb.com
novawow.com
wotlk.novawow.com
Conserva los estados de los usuarios en todas las peticiones de la página. 7 días Cookie HTTP
rc::a gstatic.com Esta cookie se utiliza para distinguir entre humanos y bots. Esto es beneficioso para la web con el objeto de elaborar informes válidos sobre el uso de su web. Persistente Almacenamiento Local HTML
rc::c gstatic.com Esta cookie se utiliza para distinguir entre humanos y bots. Sesión Almacenamiento Local HTML
SESS# reddit.com Conserva los estados de los usuarios en todas las peticiones de la página. Sesión Cookie HTTP
srv lunapic.com Registra que grupo de servidores está sirviendo al visitante. Esto se utiliza en relación con el equilibrio de carga para optimizar la experiencia del usuario. Sesión Cookie HTTP

Preferencias (1)

Las cookies de preferencias permiten a la página web recordar información que cambia la forma en que la página se comporta o el aspecto que tiene, como su idioma preferido o la región en la que usted se encuentra.

Nombre Proveedor Propósito Duración máxima de almacenamiento Tipo
Gyazo_cfwoker i.gyazo.com Permite al visitante compartir contenido de la web en plataformas de redes sociales o webs. 400 días Cookie HTTP

Estadística (1)

Las cookies estadísticas ayudan a los propietarios de páginas web a comprender cómo interactúan los visitantes con las páginas web reuniendo y proporcionando información de forma anónima.

Nombre Proveedor Propósito Duración máxima de almacenamiento Tipo
serverTimestamps www.redditstatic.com Fija la fecha y hora de la última solicitud realizada con éxito al servidor. Sesión Almacenamiento Local HTML

Marketing (45)

Las cookies de marketing se utilizan para rastrear a los visitantes en las páginas web. La intención es mostrar anuncios relevantes y atractivos para el usuario individual, y por lo tanto, más valiosos para los editores y terceros anunciantes.

Nombre Proveedor Propósito Duración máxima de almacenamiento Tipo
#-# [x2] www.youtube-nocookie.com
youtube.com
Se usa para rastrear la interacción del usuario con el contenido integrado. Sesión Almacenamiento Local HTML
__Host-GAPS accounts.google.com Recopila datos relacionados con las visitas del usuario al sitio web, como el número de visitas, el tiempo medio pasado en el sitio web y qué páginas han sido cargadas, con el propósito de generar informes para optimizar el contenido del sitio web. 2 años Cookie HTTP
_fbp novawow.com Utilizada por Facebook para proporcionar una serie de productos publicitarios como pujas en tiempo real de terceros anunciantes. 3 meses Cookie HTTP
_ga novawow.com Se utiliza para enviar datos a Google Analytics sobre el dispositivo del visitante y su comportamiento. Rastrea al visitante a través de dispositivos y canales de marketing. 2 años Cookie HTTP
_ga_# novawow.com Se utiliza para enviar datos a Google Analytics sobre el dispositivo del visitante y su comportamiento. Rastrea al visitante a través de dispositivos y canales de marketing. 2 años Cookie HTTP
_gat novawow.com Se utiliza para enviar datos a Google Analytics sobre el dispositivo del visitante y su comportamiento. Rastrea al visitante a través de dispositivos y canales de marketing. 1 día Cookie HTTP
_gid novawow.com Se utiliza para enviar datos a Google Analytics sobre el dispositivo del visitante y su comportamiento. Rastrea al visitante a través de dispositivos y canales de marketing. 1 día Cookie HTTP
iU5q-!O9@$ [x2] www.youtube-nocookie.com
youtube.com
Registra una identificación única para mantener estadísticas de qué vídeos de YouTube ha visto el usuario. Sesión Almacenamiento Local HTML
LAST_RESULT_ENTRY_KEY [x2] www.youtube-nocookie.com
youtube.com
Se usa para rastrear la interacción del usuario con el contenido integrado. Sesión Cookie HTTP
lastExternalReferrer [x2] connect.facebook.net
foro.novawow.com
Detecta cómo el usuario encontró la página web al registrar su última dirección URL. Persistente Almacenamiento Local HTML
lastExternalReferrerTime [x2] connect.facebook.net
foro.novawow.com
Detecta cómo el usuario encontró la página web al registrar su última dirección URL. Persistente Almacenamiento Local HTML
LogsDatabaseV2:V#||LogsRequestsStore youtube.com Se usa para rastrear la interacción del usuario con el contenido integrado. Persistente IndexedDB
loid reddit.com Utilizada para rastrear a los visitantes en las múltiples webs para presentar publicidad relevante basada en las preferencias del visitante. 400 días Cookie HTTP
nextId www.youtube-nocookie.com Se usa para rastrear la interacción del usuario con el contenido integrado. Sesión Cookie HTTP
NID google.com Registra una identificación única que identifica el dispositivo de un usuario que vuelve. La identificación se utiliza para los anuncios específicos. 6 meses Cookie HTTP
remote_sid youtube.com Necesaria para la implementación y funcionabilidad del contenido de video de YouTube en la web. Sesión Cookie HTTP
requests www.youtube-nocookie.com Se usa para rastrear la interacción del usuario con el contenido integrado. Sesión Cookie HTTP
TESTCOOKIESENABLED youtube.com Se usa para rastrear la interacción del usuario con el contenido integrado. 1 día Cookie HTTP
VISITOR_INFO1_LIVE youtube.com Intenta calcular el ancho de banda del usuario en páginas con vídeos de YouTube integrados. 180 días Cookie HTTP
YSC youtube.com Registra una identificación única para mantener estadísticas de qué vídeos de YouTube ha visto el usuario. Sesión Cookie HTTP
yt.innertube::nextId www.youtube-nocookie.com Registra una identificación única para mantener estadísticas de qué vídeos de YouTube ha visto el usuario. Persistente Almacenamiento Local HTML
yt.innertube::requests www.youtube-nocookie.com Registra una identificación única para mantener estadísticas de qué vídeos de YouTube ha visto el usuario. Persistente Almacenamiento Local HTML
ytidb::LAST_RESULT_ENTRY_KEY [x2] www.youtube-nocookie.com
youtube.com
Se usa para rastrear la interacción del usuario con el contenido integrado. Persistente Almacenamiento Local HTML
YtIdbMeta#databases [x2] www.youtube-nocookie.com
youtube.com
Se usa para rastrear la interacción del usuario con el contenido integrado. Persistente IndexedDB
yt-remote-cast-available [x2] www.youtube-nocookie.com
youtube.com
Registra las preferencias del reproductor de vídeo del usuario al ver vídeos incrustados de YouTube Sesión Almacenamiento Local HTML
yt-remote-cast-installed [x2] www.youtube-nocookie.com
youtube.com
Registra las preferencias del reproductor de vídeo del usuario al ver vídeos incrustados de YouTube Sesión Almacenamiento Local HTML
yt-remote-connected-devices [x2] www.youtube-nocookie.com
youtube.com
Registra las preferencias del reproductor de vídeo del usuario al ver vídeos incrustados de YouTube Persistente Almacenamiento Local HTML
yt-remote-device-id [x2] www.youtube-nocookie.com
youtube.com
Registra las preferencias del reproductor de vídeo del usuario al ver vídeos incrustados de YouTube Persistente Almacenamiento Local HTML
yt-remote-fast-check-period [x2] www.youtube-nocookie.com
youtube.com
Registra las preferencias del reproductor de vídeo del usuario al ver vídeos incrustados de YouTube Sesión Almacenamiento Local HTML
yt-remote-session-app [x2] www.youtube-nocookie.com
youtube.com
Registra las preferencias del reproductor de vídeo del usuario al ver vídeos incrustados de YouTube Sesión Almacenamiento Local HTML
yt-remote-session-name [x2] www.youtube-nocookie.com
youtube.com
Registra las preferencias del reproductor de vídeo del usuario al ver vídeos incrustados de YouTube Sesión Almacenamiento Local HTML

No clasificados (12)

Las cookies no clasificadas son cookies para las que todavía estamos en proceso de clasificar, junto con los proveedores de cookies individuales.

Nombre Proveedor Propósito Duración máxima de almacenamiento Tipo
acolor lunapic.com Pendiente Sesión Cookie HTTP
comments_sort wotlk.novawow.com Pendiente 1000 días Cookie HTTP
Gyazo_session gyazo.com Pendiente 400 días Cookie HTTP
icon_id lunapic.com Pendiente Sesión Cookie HTTP
ips4_hasJS foro.novawow.com Pendiente 1 día Cookie HTTP
ips4_IPSSessionFront foro.novawow.com Pendiente Sesión Cookie HTTP
ips4_ipsTimezone foro.novawow.com Pendiente Sesión Cookie HTTP
ips4_lastSearch foro.novawow.com Pendiente Sesión Cookie HTTP
ips4_noCache foro.novawow.com Pendiente Sesión Cookie HTTP
lvBrowse wotlk.novawow.com Pendiente Persistente Almacenamiento Local HTML
postpagebeta imgur.com Pendiente 66 años Cookie HTTP
token_v2 reddit.com Pendiente 1 día Cookie HTTP
-
-
-
-
-
\ No newline at end of file diff --git a/home/templates/partials/customize_character.html b/home/templates/partials/customize_character.html deleted file mode 100644 index 7916779..0000000 --- a/home/templates/partials/customize_character.html +++ /dev/null @@ -1,43 +0,0 @@ -
-
-
-

Personalizar personaje

-
- -
-
-

La herramienta Personalizar personaje te permite cambiar los siguientes aspectos un personaje:

-

- Color de la piel

-

- Rostro

-

- Pelo

-

- Sexo

-

- Características correspondientes a cada raza como cuernos, pendientes, marcas, vello facial

-

Si también deseas cambiar el nombre del personaje, debes usar la herramienta Renombrar personaje.

-
-

Al usar el botón PERSONALIZAR del personaje que hayas escogido, se enviará una petición de personalización de dicho personaje.

-

Cuando ingreses a la lista de personajes de tu cuenta, verás un ícono a la izquierda de tu personaje escogido.

-

Al darle click a dicho ícono ingresarás al menú de edición de personaje que te permitirá escoger diferentes características.

-
-
- NOTA -
-

Por favor, asegúrate de haber escogido al personaje correcto ya que esta acción no es reversible una vez realizada.

-
-
- {% load django_vite %} - -
- {{ characters|json_script:"character-purchase-data" }} - {% vite_asset 'src/entries/character_purchase.tsx' %} -
-
-
-
-
-
-
\ No newline at end of file diff --git a/home/templates/partials/customize_success.html b/home/templates/partials/customize_success.html deleted file mode 100644 index e550bd4..0000000 --- a/home/templates/partials/customize_success.html +++ /dev/null @@ -1,44 +0,0 @@ -
-
-
-

Personalizar personaje

-
- -
-
-

La herramienta Personalizar personaje te permite cambiar los siguientes aspectos un personaje:

-

- Color de la piel

-

- Rostro

-

- Pelo

-

- Sexo

-

- Características correspondientes a cada raza como cuernos, pendientes, marcas, vello facial

-

Si también deseas cambiar el nombre del personaje, debes usar la herramienta Renombrar personaje.

-
-

Al usar el botón PERSONALIZAR del personaje que hayas escogido, se enviará una petición de personalización de dicho personaje.

-

Cuando ingreses a la lista de personajes de tu cuenta, verás un ícono a la izquierda de tu personaje escogido.

-

Al darle click a dicho ícono ingresarás al menú de edición de personaje que te permitirá escoger diferentes características.

-
-
- NOTA -
-

Por favor, asegúrate de haber escogido al personaje correcto ya que esta acción no es reversible una vez realizada.

-
-
-
-

Selecciona el personaje que deseas personalizar:

-
- - -
-
-
-
-
-
-
-
\ No newline at end of file diff --git a/home/templates/partials/d_points.html b/home/templates/partials/d_points.html deleted file mode 100644 index 1eabaf5..0000000 --- a/home/templates/partials/d_points.html +++ /dev/null @@ -1,934 +0,0 @@ -
-
-
-

Adquirir PD

-
-
- - - - - - - - - - -
-
-
-
-
-
- ¿Qué son los PD? -

Ofrecemos a nuestros usuarios la oportunidad de obtener niveles que les brindan beneficios exclusivos dentro de nuestra comunidad en línea. Estos beneficios incluyen acceso a herramientas especiales, contenido exclusivo y mejoras significativas para su experiencia en nuestro sitio web. Al realizar una compra, nuestros usuarios reciben PD, una unidad de reconocimiento interna que refleja su compromiso y progreso en nuestra plataforma. Los PD les permiten alcanzar nuevos niveles y desbloquear recompensas exclusivas. Cabe destacar que estos PD son una forma única de valorar y premiar la participación activa de nuestros usuarios en nuestra comunidad.

-
-
-
- ¿Cuáles son los niveles disponibles? -

Mostrar

-

Ocultar

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Nivel 1Nivel 1Desde 0 PD
Nivel 2Nivel 2Hasta 100 PD
Nivel 3Nivel 3Hasta 200 PD
Nivel 4Nivel 4Hasta 300 PD
Nivel 5Nivel 5Hasta 400 PD
Nivel 6Nivel 6Hasta 500 PD
Nivel 7Nivel 7Hasta 600 PD
Nivel 8Nivel 8Hasta 700 PD
Nivel 9Nivel 9Hasta 800 PD
Nivel 10Nivel 10Hasta 900 PD
Nivel 11Nivel 11Hasta 1000 PD
Nivel 12Nivel 12Hasta 5000 PD
Nivel 13Nivel 13Hasta 10000 PD
Nivel 14Nivel 14Más de 10000 PD
-
- -
-
-
- ¿Cuáles son los métodos de compra de PD? -

Los métodos de compra disponibles actualmente son:

-

- PayPal (Para todos los países)

-

- Skrill (Para todos los países)

-

- dLocal Go para Argentina Argentina, Bolivia Bolivia, Chile Chile, Colombia Colombia, Costa Rica Costa Rica, Ecuador Ecuador, Guatemala Guatemala, México México, Panamá Panamá, Perú Perú, Paraguay Paraguay, Uruguay Uruguay.

-

- Métodos alternativos sólo para Argentina (Argentina ARG)

- -

- Métodos alternativos sólo para Chile ( Chile CHI)

-

- Métodos alternativos sólo para Colombia (Colombia COL)

-

- Métodos alternativos sólo para Perú (Perú PER)

-
-

Puedes hacer una sugerencia sobre nuevos métodos en nuestro foro de sugerencias aquí.

-
-
-
- ¿Cuántos PD se reciben? -

Cada método indicará cuántos PD se reciben.

-
-
-
- ¿Tienes dudas o problemas con una compra? -

No realices una compra hasta estar completamente seguro de que has leído esta información y sabes cómo es el proceso que vayas a escoger.

-

Si a pesar de leer este instructivo o el de alguna de nuestras herramientas, aún tienes dudas, el equipo de {{ NOMBRE_SERVIDOR }} podrá asesorarte en cualquier momento. Puedes colocar un ticket en el servidor, o contactarnos mediante nuestra plataforma de Discord.

-
-
- - - - - - - - - - - - - - - - - - - -
- -
-
-
\ No newline at end of file diff --git a/home/templates/partials/descargas.html b/home/templates/partials/descargas.html deleted file mode 100644 index a5b63eb..0000000 --- a/home/templates/partials/descargas.html +++ /dev/null @@ -1,119 +0,0 @@ - -
-
-
-
-

{{ download_content.title }}

-
- -
-

-
- {{ download_content.content|safe }} -
-
-

Idioma

-
- -

- - {% for categoria in categorias %} - - {% endfor %} - -
- -
- -
-
-
-
-
-
- - -
-
-
\ No newline at end of file diff --git a/home/templates/partials/expired_link.html b/home/templates/partials/expired_link.html deleted file mode 100644 index 286eab0..0000000 --- a/home/templates/partials/expired_link.html +++ /dev/null @@ -1,27 +0,0 @@ -
-
-
-
-

Enlace Expirado

-
-
-
-

Enlace Expirado

- - -
-
-
-
-
- Lo sentimos, el enlace que ha utilizado ha expirado o no es válido. -
-
- No hay códigos de promoción disponibles - -
-
-
-
-
-
diff --git a/home/templates/partials/final.html b/home/templates/partials/final.html deleted file mode 100644 index 308b1d0..0000000 --- a/home/templates/partials/final.html +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/home/templates/partials/footer.html b/home/templates/partials/footer.html deleted file mode 100644 index fb25426..0000000 --- a/home/templates/partials/footer.html +++ /dev/null @@ -1,11 +0,0 @@ - \ No newline at end of file diff --git a/home/templates/partials/gold_cancel.html b/home/templates/partials/gold_cancel.html deleted file mode 100644 index e69de29..0000000 diff --git a/home/templates/partials/gold_character.html b/home/templates/partials/gold_character.html deleted file mode 100644 index 19b9d2a..0000000 --- a/home/templates/partials/gold_character.html +++ /dev/null @@ -1,24 +0,0 @@ -
-
-
-

Adquirir oro

-
-
-

Información

- -
-
-

Selecciona un personaje y la cantidad de oro:

- {% load django_vite %} - -
- {{ character_names|json_script:"gold-characters-data" }} - {{ gold_options|json_script:"gold-options-data" }} - {% vite_asset 'src/entries/gold_character.tsx' %} -
-
-
-
-
diff --git a/home/templates/partials/gold_success.html b/home/templates/partials/gold_success.html deleted file mode 100644 index e69de29..0000000 diff --git a/home/templates/partials/guild_rename_cancel.html b/home/templates/partials/guild_rename_cancel.html deleted file mode 100644 index e69de29..0000000 diff --git a/home/templates/partials/guild_rename_success.html b/home/templates/partials/guild_rename_success.html deleted file mode 100644 index e69de29..0000000 diff --git a/home/templates/partials/head.html b/home/templates/partials/head.html deleted file mode 100644 index 5c00e7c..0000000 --- a/home/templates/partials/head.html +++ /dev/null @@ -1,125 +0,0 @@ - - - - - - - {{ NOMBRE_SERVIDOR }} Server de WoW WotLK 3.4.3 Latino y Español 💎 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/home/templates/partials/header.html b/home/templates/partials/header.html deleted file mode 100644 index 2ab3f58..0000000 --- a/home/templates/partials/header.html +++ /dev/null @@ -1,66 +0,0 @@ -
- - - - - - - -
\ No newline at end of file diff --git a/home/templates/partials/help.html b/home/templates/partials/help.html deleted file mode 100644 index 564cbd7..0000000 --- a/home/templates/partials/help.html +++ /dev/null @@ -1,82 +0,0 @@ -
-
-
-

Ayuda

-
-

Guía de asistencia

-
-

En esta guía podrás saber qué pasos seguir para obtener asistencia adecuada dependiendo del tipo de problema que hayas encontrado en nuestro servidor.

-

Ofrecemos un conjunto de problemas comunes, qué medios utilizar para cada caso y una explicación de cada medio de contacto.

-

Los diferentes medios para cada problema se listan desde el más ideal hasta la última opción.

-
- Con el cliente -

Problemas que impiden arrancar el cliente, conectar al servidor o errores que provocan el cierre abrupto del cliente.

-

Los medios son:

-

- Foros

-

- Discord

-
- Con errores del servidor -

Problemas relacionados a errores que deben ser reparados por nuestro equipo de reparación para mejorar el servidor.

-

Los medios son:

-

- Foros

-
- Con el personaje -

Problemas relacionados a errores que puedan afectar al personaje, habilidades, hechizos, misiones, etc.

-

Los medios son:

-

- Ticket

-

- Discord

-
- Con otro jugador -

Problemas relacionados a otros usuarios cometiendo faltas a las normas del servidor.

-

Este tipo de problemas requieren de una denuncia con las pruebas correspondientes de lo que haya sucedido.

-

Los medios son:

-

- Foros

-
- Con bloqueos de cuenta -

Problemas relacionados a bloqueos de cuenta realizados por el equipo de {{ NOMBRE_SERVIDOR }}.

-

Los medios son:

-

- Foros

-
- Con la página web -

Problemas relacionados a errores, fallas o sugerencias respecto de este sitio web.

-

Los medios son:

-

- Correo a ryuzaki@novawow.com

-
- Con PD -

Problemas relacionados a PD o PV.

-

Los medios son:

-

- Discord (enviando un mensaje a Ryuzaki o Torete)

-

- Correo a contacto@novawow.com

-
-
- Medios de contacto -
- Ticket -

Para colocar un ticket, debes hacer click al signo de interrogación rojo en la parte inferior derecha de la interfaz del cliente (a la izquierda del llavero del personaje).

-

Al hacerle click, aparecerá una ventana en el medio de la pantalla.

-

En dicha ventana, hacer click al primer botón en la esquina inferior izquierda llamado 'Hablar con un MJ'

-

Aparecerá una ventana con un texto informativo y debajo en medio, un botón llamado 'Abrir una consulta'.

-

Al darle click al botón, aparecerá un campo de texto en el que puedes describir tu problema

-

Cuando hayas finalizado haz click en el botón en el primer botón en la esquina inferior derecha llamado 'Enviar'.

-

El ticket ya ha sido creado y está pendiente de respuesta de un miembro del equipo.

-
- Discord -

Discord es una plataforma de chat y comunicación por voz que puede descargarse aquí.

-

Para entrar en el servidor de Discord de nuestro servidor, haz click aquí.

-

Una vez que seas miembro de nuestro Discord podrás pedir ayuda en el canal 'Asistencia' o enviar un mensaje directo a cualquier miembro del equipo solicitando ayuda.

-
- Foros -

Los foros son una gran herramienta en donde no sólo puede ayudarte el equipo de {{ NOMBRE_SERVIDOR }} sino que además otros jugadores pueden asistirte si conocen cómo resolver el problema que tengas.

-

Para solicitar ayuda por este medio, debes tener un usuario con el cual poder conectar y abrir un nuevo tema en el foro adecuado.

-

Puedes visitar nuestros foros aquí.

-
- Correo electrónico -

El servidor tiene diferentes direcciones de correo dedicados a diferentes temas del servidor.

-

Depende cual sea tu problema, usa el adecuado para enviarnos un mensaje de correo.

-
-
-
-
-
-
-
\ No newline at end of file diff --git a/home/templates/partials/legal_notice.html b/home/templates/partials/legal_notice.html deleted file mode 100644 index 90afe9e..0000000 --- a/home/templates/partials/legal_notice.html +++ /dev/null @@ -1,24 +0,0 @@ -
-
-
-

Aviso legal

-
-
-

El sitio web "NovaWoW.com" y sus subdominios (en adelante, "el sitio web") se ofrecen únicamente con fines informativos y de entretenimiento. {{ NOMBRE_SERVIDOR }} no se hace responsable del uso que los usuarios hagan del sitio web. Aunque se procura que se dé un uso correcto, {{ NOMBRE_SERVIDOR }} no asume responsabilidad por las acciones, decisiones o consecuencias derivadas del uso del sitio web. Los usuarios son responsables de verificar la precisión, integridad y utilidad de la información proporcionada, así como de cumplir con las leyes y regulaciones aplicables al utilizar el contenido del sitio web.

-
-

Enlaces a terceros: El sitio web puede contener enlaces a sitios web de terceros. Estos enlaces se proporcionan únicamente para tu conveniencia y no implican el respaldo o afiliación de {{ NOMBRE_SERVIDOR }} con dichos sitios web. {{ NOMBRE_SERVIDOR }} no se hace responsable del contenido o las prácticas de privacidad de estos sitios web de terceros.

-
-

Privacidad y Cookies: La recopilación y uso de tus datos personales se rigen por nuestra Política de Privacidad y el uso de cookies se rige por nuestra Declaración de Cookies. Al utilizar el sitio web, aceptas nuestras prácticas descritas en dichas políticas.

-
-

Derechos de autor: Los usuarios del sitio web deben respetar los derechos de autor y no publicar contenido que infrinja los derechos de propiedad intelectual de terceros. {{ NOMBRE_SERVIDOR }} se compromete a proteger los derechos de autor y cumplir con las leyes aplicables. Si encuentras contenido infractor, contáctanos utilizando el mecanismo de reclamación de infracción descrito a continuación.

-
-

Mecanismo de reclamación de infracción: Si crees que tu trabajo ha sido utilizado de manera no autorizada en el sitio web, puedes presentar una reclamación por infracción de derechos de autor proporcionando la información requerida. Contáctanos utilizando los datos de contacto a continuación.

-
-

Contacto

-

novawow.com

-

info@novawow.com

-
-
-
-
-
\ No newline at end of file diff --git a/home/templates/partials/level_up.html b/home/templates/partials/level_up.html deleted file mode 100644 index 30471d7..0000000 --- a/home/templates/partials/level_up.html +++ /dev/null @@ -1,39 +0,0 @@ -
-
-
-

Subir a nivel 80

-
- -
-
-

La herramienta Subir a nivel 80 te permite subir cualquier personaje al nivel 80.

-

Sólo subirá de nivel al personaje. Esta opción no incluye ítems/oro/etc.

-
-

Al usar el botón SUBIR A NIVEL 80 del personaje que hayas escogido, se enviará el nivel 80 al personaje.

-

Cuando ingreses a la lista de personajes de tu cuenta, verás que tu personaje escogido ya es nivel 80.

-
-
- NOTA -
-

Se requiere que el personaje esté desconectado.

-

Por favor, asegúrate de haber escogido al personaje correcto ya que esta acción no es reversible una vez realizada.

-
-
-

Selecciona el personaje que deseas subir al nivel 80:

- {% load django_vite %} - -
- {{ characters|json_script:"character-purchase-data" }} - {% vite_asset 'src/entries/character_purchase.tsx' %} -
-
-
-
-
-
- \ No newline at end of file diff --git a/home/templates/partials/level_up_cancel.html b/home/templates/partials/level_up_cancel.html deleted file mode 100644 index e69de29..0000000 diff --git a/home/templates/partials/level_up_success.html b/home/templates/partials/level_up_success.html deleted file mode 100644 index e69de29..0000000 diff --git a/home/templates/partials/login.html b/home/templates/partials/login.html deleted file mode 100644 index 708f5a0..0000000 --- a/home/templates/partials/login.html +++ /dev/null @@ -1,46 +0,0 @@ -{% if request.session.username %} -
-
-
-

Conectar a {{ NOMBRE_SERVIDOR }}

-
-
-

¡Ya estás conectado!

-
-

Si quieres conectar otra cuenta, primero desconecta.

-
-
-
-
-
- {% else %} -
-
-
-
-

Conectar a {{ NOMBRE_SERVIDOR }}

-
-
-
- {% load django_vite %} - -
- {% vite_asset 'src/entries/login.tsx' %} -
-

He olvidado mi contraseña/usuario

-

No me ha llegado el enlace de activación

-
-

¿Nuevo en la comunidad? Puedes crear una cuenta aquí

-
-

* Si conectas desde un lugar público recuerda desconectar cuando no estés usando la página web o el foro.

-
-
-
-
-
-{% endif %} \ No newline at end of file diff --git a/home/templates/partials/maintenance.html b/home/templates/partials/maintenance.html deleted file mode 100644 index 853f569..0000000 --- a/home/templates/partials/maintenance.html +++ /dev/null @@ -1,16 +0,0 @@ -
-
-
-

Mantenimiento

-
-
-

Nuestro sitio web está actualmente en mantenimiento para mejorar la experiencia de nuestros usuarios.

-

Agradecemos su paciencia y comprensión.

-
-

Para mantenerse informados sobre el estado del mantenimiento y recibir actualizaciones en tiempo real, únanse a nuestro servidor de Discord.

- -
-
-
-
-
\ No newline at end of file diff --git a/home/templates/partials/my-account.html b/home/templates/partials/my-account.html deleted file mode 100644 index e01d628..0000000 --- a/home/templates/partials/my-account.html +++ /dev/null @@ -1,410 +0,0 @@ -{% load django_vite %} -
-
-
-

Mi cuenta

-
-

Información

-
- -
- {% vite_asset 'src/entries/my_account.tsx' %} -
-
- - - - -
-

Utilidades y Herramientas

- -
-
-
-
- - diff --git a/home/templates/partials/not_found.html b/home/templates/partials/not_found.html deleted file mode 100644 index 57ff5de..0000000 --- a/home/templates/partials/not_found.html +++ /dev/null @@ -1,12 +0,0 @@ -
-
-
-

Página no encontrada

-
-
-

Parece que la página que estás buscando no existe o no se encuentra disponible.

-
-
-
-
-
\ No newline at end of file diff --git a/home/templates/partials/noticias.html b/home/templates/partials/noticias.html deleted file mode 100644 index 20f47c1..0000000 --- a/home/templates/partials/noticias.html +++ /dev/null @@ -1,53 +0,0 @@ -{% load django_vite %} - -
-
-
-

{{ NOMBRE_SERVIDOR }} WOTLK 3.4.3

-
- -
-
-

{{ NOMBRE_SERVIDOR }} es una comunidad enfocada en usuarios de habla hispana, especialmente dirigida al público español y latino.

-

Atraemos a usuarios apasionados tanto por el PvE como por el PvP, y ofrecemos una amplia variedad de contenido.

-
-

Nuestro objetivo principal es mantener una comunidad de alto nivel, brindando un ambiente agradable para que todos los miembros, nuevos y veteranos, encuentren aquí un lugar donde puedan vivir momentos inigualables.

-

¡Únete y disfruta de una experiencia única con miles de usuarios!

-
-
-
-
- -
-

NOTICIAS

-
-
-
- -
- {% vite_asset 'src/entries/home.tsx' %} -
-
- - - - diff --git a/home/templates/partials/novawow-realm.html b/home/templates/partials/novawow-realm.html deleted file mode 100644 index fb31eb8..0000000 --- a/home/templates/partials/novawow-realm.html +++ /dev/null @@ -1,31 +0,0 @@ -
-
-
-

Reino - {{ server.name }}

-
-
-

Rates

-

Experiencia: x8 modificable con .xp (Con Recluta a un Amigo x16)

-

Drop de objetos verdes y azules: x4

-

Drop de oro en NPCs: x2

-

Drop de oro en misiones: x1

-

Habilidades: x3

-

Profesiones: x3

-

Reputaciones: x3 (Bonus con Recluta a un amigo)

-

Honor: x3

-
-
-

Horarios

-

Los siguientes horarios son basados en la hora del reino.

-
-

Misiones diarias: 7 am -

Reinicio de mazmorras diarias: 9 am

-

Reinicio de bandas: 9 am (Miércoles)

-

Campos de Batalla: 10 am

-

Reparto de Puntos de Arena: 11 am (Miércoles)

-
-
-
-
-
-
\ No newline at end of file diff --git a/home/templates/partials/novawow_players.html b/home/templates/partials/novawow_players.html deleted file mode 100644 index 009c840..0000000 --- a/home/templates/partials/novawow_players.html +++ /dev/null @@ -1,159 +0,0 @@ -
-
-
-

Personajes - {{ server.name }}

-
-
-

La información de esta página se actualiza cada 5 minutos automáticamente

-
-
-
-

Personajes creados por clase

-
- {% for data in classes_data %} -
- - - - - - - - - - - - - - -
Total: {{ data.total }}

- - Alianzas: {{ data.alliance }} -
- - Hordas: {{ data.horde }} -
-
- {% endfor %} -
-
- -
-
-
-
-

Primeros del Reino - Nivel 80

-
- - - - - - - - {% for achievement in first_realm_data %} - - - - - - {% endfor %} - -
LogroPersonajeFecha
- - {{ achievement.achievement_name }} - {{ achievement.name }}{{ achievement.date }}
-
-
- -
-
-
-
-

Top de logros

-
-

Los 10 jugadores con más logros

- - - - - - - - {% for player in players_with_achievements %} - - - - - - - {% endfor %} -
NombreRazaClasePuntos de logros
{{ player.name }}{{ player.achievement_points }}
-
-
- -
-
-
-
-

Top de muertes con honor

-
-

Los 10 jugadores con más muertes con honor

- - - - - - - - - {% for player in players_with_kills %} - - - - - - - - {% endfor %} -
NombreRazaClaseTotal de muertesTotal de muertes hoy
{{ player.name }} - {{ player.total_kills }}{{ player.today_kills }}
-
-
-
-
-
-
-

Top de hermandades

-
-

Las 5 hermandades con más miembros

- - - - - - - - - - - - {% for guild in guilds %} - - - - - - - - {% endfor %} - -
NombreLíderFacciónMiembrosFecha de creación
<{{ guild.0 }}>{{ guild.1 }} - - {{ guild.3 }}{{ guild.4 }}
-
-
-
-
-
\ No newline at end of file diff --git a/home/templates/partials/pago_sumup.html b/home/templates/partials/pago_sumup.html deleted file mode 100644 index ad66f8d..0000000 --- a/home/templates/partials/pago_sumup.html +++ /dev/null @@ -1,38 +0,0 @@ - - - - - Pago con SumUp - - - -

Pago con SumUp

- - {% if error %} -

Error: {{ error }}

-

Volver a mis compras

- {% else %} - {% if product_name %} -

Producto: {{ product_name }}

-

Importe: {{ price_eur }} €

- {% endif %} -
- - -

Cancelar y volver

- {% endif %} - - diff --git a/home/templates/partials/points_history.html b/home/templates/partials/points_history.html deleted file mode 100644 index 15128d1..0000000 --- a/home/templates/partials/points_history.html +++ /dev/null @@ -1,27 +0,0 @@ -
-
-
-

Historial de PD y PV

-
- -
-
-

Aquí verás los consumos de PD y PV más recientes que ha tenido la cuenta.

-

Con esta información podrás tener control de todo lo que has adquirido así como también chequear que toda la activdad sea normal.

-

Si observas algún movimiento sospechoso, puedes contactar con el equipo de {{ NOMBRE_SERVIDOR }}.

-
-
-
- -

Historial de PD y PV

-
- - - - -
No hay movimientos
-
-
-
-
-
\ No newline at end of file diff --git a/home/templates/partials/privacy_policy.html b/home/templates/partials/privacy_policy.html deleted file mode 100644 index 91cf166..0000000 --- a/home/templates/partials/privacy_policy.html +++ /dev/null @@ -1,68 +0,0 @@ -
-
-
-

Política de privacidad

-
-
-

Esta Política de Privacidad describe cómo recopilamos, utilizamos, almacenamos y protegemos la información personal de los usuarios ("usted" o "usuario") de nuestro sitio web. Al utilizar nuestro sitio web, usted acepta las prácticas descritas en esta política.

-
-
-
-

Recopilación de Información Personal

-

Recopilamos la siguiente información personal cuando usted utiliza nuestro sitio web:

-

1. Cookies: Utilizamos cookies para recopilar información sobre su actividad en nuestro sitio web. Las cookies son pequeños archivos de texto que se almacenan en su dispositivo y nos permiten reconocer su navegador o dispositivo y recordar cierta información. Estas cookies pueden ser cookies de sesión o cookies persistentes. Puede ajustar la configuración de su navegador para rechazar todas las cookies o para indicar cuándo se envía una cookie. Sin embargo, si desactiva las cookies, es posible que algunas funciones de nuestro sitio web no funcionen correctamente.

-

2. Direcciones IP: Recopilamos direcciones IP de los usuarios que visitan nuestro sitio web. Las direcciones IP se utilizan para analizar tendencias, administrar el sitio web, rastrear el movimiento del usuario y recopilar información demográfica. No vinculamos las direcciones IP con ninguna información de identificación personal.

-

3. Direcciones de correo electrónico: Recopilamos las direcciones de correo electrónico que usted proporciona al registrarse en nuestro sitio web o al actualizar su información de registro. Utilizamos estas direcciones de correo electrónico para enviarle información relacionada con su cuenta, como confirmaciones de registro, notificaciones de cambios en los términos o políticas, y actualizaciones de seguridad.

-

4. Nombres de usuario y contraseñas: Recopilamos los nombres de usuario y las contraseñas que usted elige al registrar una cuenta en nuestro sitio web. Estas credenciales se utilizan para autenticar su acceso a su cuenta y para proteger su información personal.

-
-
-
-

Uso de la Información Personal

-

Utilizamos la información personal que recopilamos para los siguientes fines:

-

1. Brindar y mejorar nuestros servicios: Utilizamos su información personal para brindarle los servicios solicitados y mejorar su experiencia en nuestro sitio web. Esto incluye personalizar el contenido y las recomendaciones, proporcionar soporte técnico, y analizar el rendimiento y la eficacia de nuestro sitio web.

-

2. Comunicaciones: Podemos utilizar su dirección de correo electrónico para enviarle comunicaciones relacionadas con su cuenta, como confirmaciones de registro, notificaciones de cambios en los términos o políticas, actualizaciones de seguridad y otras actualizaciones importantes.

-
-
-
-

Protección de la Información Personal

-

Tomamos medidas razonables para proteger la información personal que recopilamos y almacenamos. Utilizamos medidas de seguridad técnicas, administrativas y físicas para proteger su información contra el acceso no autorizado, la divulgación, la alteración o la destrucción.

-
-
-
-

Divulgación de la Información Personal a Terceros

-

No vendemos, intercambiamos ni transferimos su información personal a terceros sin su consentimiento, excepto en los siguientes casos:

-

- Proveedores de servicios: Podemos compartir su información personal con proveedores de servicios que realizan funciones en nuestro nombre, como el alojamiento del sitio web, el procesamiento de pagos y el análisis de datos. Estos proveedores de servicios tienen acceso a su información personal solo en la medida necesaria para realizar sus funciones y están obligados a no divulgar ni utilizar la información con ningún otro fin.

-

- Cumplimiento legal: Podemos divulgar su información personal si creemos de buena fe que dicha divulgación es necesaria para cumplir con una obligación legal, proteger sus derechos o seguridad, investigar fraudes o responder a una solicitud legal.

-
-
-
-

Sus Derechos y Opciones

-

Usted tiene ciertos derechos y opciones con respecto a su información personal:

-

- Acceso y actualización: Usted puede acceder y actualizar cierta información personal proporcionada a través de su cuenta en nuestro sitio web.

-

- Eliminación de datos: Usted puede solicitar la eliminación de su información personal de nuestros registros, sujeto a cualquier requisito legal o contractual que podamos tener.

-

- Cookies: Puede ajustar la configuración de su navegador para rechazar cookies o para indicar cuándo se envía una cookie. Sin embargo, tenga en cuenta que deshabilitar las cookies puede afectar el funcionamiento de nuestro sitio web.

-
-
-
-

Uso incorrecto de plataformas de terceros con el fin de violar nuestra política de devolución

-

Este tipo de acciones conllevará a la expulsión del usuario de nuestro sitio sin ningún tipo de devolución.

-
-
-
-

Cambios a esta Política de Privacidad

-

Nos reservamos el derecho de actualizar esta Política de Privacidad en cualquier momento. Le recomendamos que revise periódicamente esta página para conocer los cambios. La fecha de entrada en vigencia de la política se indicará en la parte superior de la página.

-
-
-
-

Contáctanos

-

Si tiene alguna pregunta o inquietud sobre esta Política de Privacidad, puede contactarnos a través de la siguiente información de contacto:

-

Error 404

-

novawow.com

-

info@novawow.com

-
-

Fecha de actualización: 23-05-2023

-
-
-
-
-
\ No newline at end of file diff --git a/home/templates/partials/promo_code.html b/home/templates/partials/promo_code.html deleted file mode 100644 index cfa9da6..0000000 --- a/home/templates/partials/promo_code.html +++ /dev/null @@ -1,29 +0,0 @@ -
-
-
-

Código de promoción

-
- - -
-
-

Los Códigos de promoción son palabras con letras y números que se canjean en el formulario debajo por PD o PV y tienen usos limitados. Cada uno puede dar diferente cantidad de PD/PV.

-

Estos códigos son generados por el Equipo de {{ NOMBRE_SERVIDOR }} y pueden aparecer en la página web, Discord, las redes sociales o el servidor.

-

Aconsejamos poner atención a las redes sociales y los anuncios del servidor para saber cuándo hay un código disponible.

-
-
- NOTA -
-

Los códigos son sensibles a mayúsculas y minúsculas por lo que siempre debes escribirlos igual a como aparecen publicados.

-
-
-
-
-
- No hay códigos de promoción disponibles -
-
-
-
-
-
\ No newline at end of file diff --git a/home/templates/partials/quest_character.html b/home/templates/partials/quest_character.html deleted file mode 100644 index 7f0aae0..0000000 --- a/home/templates/partials/quest_character.html +++ /dev/null @@ -1,90 +0,0 @@ -
-
-
-

Rastreador de misiones

-
- -
-
-

La herramienta Rastreador de misiones te permite ver el estado de misiones o cadenas de misiones importantes en tus personajes.

-
-

Opciones disponibles:

-

Brujo

- -
-

Cazador

-
    -
  • Habilidades de mascota a partir del nivel 10
  • -
-
-

Chamán

- -
-

Druida

- -
-

Guerrero

- -
-

Paladín

- -
-

Por reputación

- -
-

Por misión específica

-
    -
  • A partir del nivel 10
  • -
-
-

Cómo buscar por misión específica:

-

1) Visita nuestra base de datos.

-

2) Busca allí el nombre de la misión.

-

3) Una vez que la hayas encontrado, su enlace se verá cómo "https://wotlk.novawow.com/?quest=12843"

-

4) Usa el número final del enlace en el campo ID de misión (Ejemplo del enlace anterior: 12843).

-
-

Al usar el botón BUSCAR MISIONES, podrás ver el estado de las misiones del personaje.

-
-
- NOTA -
-

Se requiere que el personaje esté desconectado y cumpla con el mínimo de nivel de cada opción.

-

Se puede repetir búsqueda de cadenas de misiones en un mismo personaje cada 1 hora.

-

Se puede repetir búsqueda de cadenas de misiones de clase en un mismo personaje cada 30 minutos.

-

Se puede repetir búsqueda de misión específica en un mismo personaje cada 10 minutos.

-
-
-
-
-
- No hay personajes disponibles para esta herramienta -
-
-
-
-
-
-
\ No newline at end of file diff --git a/home/templates/partials/recover_account.html b/home/templates/partials/recover_account.html deleted file mode 100644 index bdc2f6e..0000000 --- a/home/templates/partials/recover_account.html +++ /dev/null @@ -1,41 +0,0 @@ -{% load django_vite %} -
-
-
-

Recuperar información de cuenta

-
-

Información

-
- Si has olvidado la contraseña de la cuenta: -

- Selecciona la opción 'Contraseña'.

-

- Escribe el correo electrónico de la cuenta.

-

- Un correo será enviado con un enlace para generar una contraseña nueva.

-
- Si has olvidado con qué cuentas de juego cuentas: -

- Selecciona la opción 'Nombre de cuenta'.

-

- Escribe el correo electrónico.

-

- Un correo será enviado con todas las cuentas ligadas al correo.

-
- Si no has recibido el enlace de activación: -

- Selecciona la opción 'Enlace de activación'.

-

- Escribe el correo electrónico.

-

- Un correo será enviado con el enlace de activación.

-
-
- -
-

Solicitud de información

-
- -
- {% vite_asset 'src/entries/recover.tsx' %} -
-
-
-
-
-
diff --git a/home/templates/partials/recruit_a_friend.html b/home/templates/partials/recruit_a_friend.html deleted file mode 100644 index 1c2dde8..0000000 --- a/home/templates/partials/recruit_a_friend.html +++ /dev/null @@ -1,190 +0,0 @@ -
-
-
- -

Recluta a un amigo

- - -
- {% if request.session.username %} - - {% endif %} -

Información

-
-
- ¿Qué es Recluta a un amigo? -

Recluta a un amigo (También conocido como RAF) es un sistema en el cual un usuario de Nova WoW puede registrarse como referido/reclutado de otro usuario y así disfrutar de beneficios y varios premios para el usuario que ha referido/reclutado a otros.

-

Sólo funciona para usuarios nuevos en el servidor y no tiene límites de hasta cuántas cuentas pueden reclutarse.

-

Los beneficios funcionan para todos los personajes de cada cuenta y permanece para siempre.

-
-

- Quien se crea una cuenta completando el campo Personaje reclutante en el registro, es el referido/reclutante.

-

- El personaje que se haya utilizado para crear la cuenta, es el referente/reclutante.

-
-
-
- ¿Cómo reclutar a un amigo? -

Cuando tu amigo cree una cuenta, dile que en el campo 'Personaje reclutante' escriba el nombre de alguno de los personajes en tu cuenta.

-

Una vez registrado como referido, ya podrán disfrutar de todos los beneficios.

-

Asegúrate de elegir bien a tu referente y/o referido ya que la acción no es reversible.

-
-
-
- ¿Cómo obtener los beneficios? -

- Ambos personajes tienen que estar leveando en grupo. Recuerda que deben estar ambos en lista de amigos para darse grupo.

-

- Deben estar en una distancia relativamente cercana.

-

- La diferencia máxima entre ambos jugadores debe ser de 4 niveles.

-
-

Si no se cumple alguna de las condiciones mencionadas, no se recibirá el bonus de experiencia.

-
-
-
- ¿Cómo ver la experiencia obtenida? -

Podrás ver el bonus de experiencia recibida activando los mensajes de experiencia en el chat:

-

1) Clic derecho en la pestaña de chat General.

-

2) Clic izquierdo en Configuración.

-

3) Clic izquierdo en Otros.

-

4) Marca la casilla Experiencia.

-

5) Clic izquierdo en Aceptar.

-
-

Al ganar experiencia, verás un mensaje detallado que muestra la cantidad obtenida y cuánto de ello proviene del bonus de Recluta a un amigo.

-
-
-
- ¿Puedo desactivar el bonus de experiencia? -

El bonus será otorgado siempre y cuando se cumplan los requisitos.

-

Si deseas dejar de obtenerlo, asegúrate de no cumplir alguna de las condiciones mencionadas en span>¿Cómo obtener los beneficios?.

-
-
-
- ¿Hay recompensas por reclutar amigos? -

Existen varias recompensas por reclutar amigos.

-

En el recuadro debajo podrás ver cuáles son los objetivos de cada recompensa. (Debes estar conectado para ver el recuadro).

-
-
-
- ¿Cuáles son los beneficios? -

Cuando un personaje del referente y un personaje del referido levean juntos, serán beneficiados por:

-

Hasta nivel 60:

-

- La habilidad de 'Invocar amigo'.

-

- El personaje referido podrá dar niveles al personaje referente.

-
-

Hasta nivel 80:

-

- Recibirán experiencia x2.

-

- Un bonus de reputación.

-
-
-
- Invocar amigo -

Esta habilidad aparece en 2 lugares:

-

- Como opción al hacer click derecho en el avatar del personaje referente/referido.

-

- Si tienes al personaje referente/referido en la lista de amigos, aparece un ícono especial a la derecha de su nombre.

-
-

Al usar esta habilidad se debe tener en target al personaje referente/referido y no debe ser modificado hasta finalizar su casteo.

-

Su tiempo de reutilización es de 1 hora.

-

-
-
-
- Otorgar niveles -

Esta habilidad aparece al hacer click derecho en el avatar del personaje referente y puede utilizarla solamente el personaje referido.

-

Cuando el personaje referido tenga al menos 1 nivel más que el personaje referente, puede otorgar un nivel que deberá ser aceptado por el personaje referente.

-

Para volver a otorgar un nivel, el personaje referido tendrá que subir 2 niveles más.

-
-
-
- ¿Tienes dudas o problemas para reclutar amigos? -

Si a pesar de leer este instructivo aún tienes dudas, el equipo de Nova WoW podrá asesorarte en cualquier momento. Puedes colocar un ticket en el servidor solicitando ayuda para Reclutar a un amigo.

-
-
- - {% if request.session.username %} - -
-

Panel de Recluta a un amigo

-
- {% if account_status.is_recruited %} -

Cuenta reclutada:

- {% else %} -

Cuenta reclutada: No

- {% endif %} -

Amigos reclutados: {{ account_status.recruited_count }}

-

Amigos reclutados al nivel 80: {{ account_status.recruited_level_80_count }}

-

Recompensas reclamadas: {{ claimed_rewards_count }}/6

- -
- -
-

RECOMPENSAS DISPONIBLES

-
- - - - - {% for reward in all_rewards %} - - - - - - {% endfor %} -
Amigos reclutadosRecompensa
{{ reward.required_friends }} amigos al nivel 80 - - {{ reward.reward_name }} - - {{ reward.status }}
-
-

RECOMPENSAS POR RECLAMAR

-
- - - - - - - - {% for reward in available_rewards %} - - - - - - - - {% endfor %} -
Amigos reclutadosRecompensaAcción
{{ reward.required_friends }} amigos al nivel 80 - - {{ reward.reward_name }} - - {{ reward.status }} -
- {% if account_status.recruited_level_80_count >= reward.required_friends %} -
- {% csrf_token %} - - - -
- {% else %} - No disponible - {% endif %} -
- - Recluta amigos para poder reclamar las recompensas -
-
- {% else %} - - -
-
-

Conecta para ver el Panel de recompensas.

-
-
- {% endif %} -
-
-
diff --git a/home/templates/partials/refund_policy.html b/home/templates/partials/refund_policy.html deleted file mode 100644 index cca1f9d..0000000 --- a/home/templates/partials/refund_policy.html +++ /dev/null @@ -1,96 +0,0 @@ -
-
-
-

Política de reembolso

-
-
-

NUESTRO SERVICIO

-

Todos nuestros servicios son de entrega inmediata y se reciben tal cual se encuentran descritos.

-
-
-
-

NUESTRA RESPONSABILIDAD

-

Garantizamos el funcionamiento, tiempo de entrega y descripción de los servicios brindados por nuestro sitio.

-

Es importante que el usuario comprenda que lo que adquiere en el sitio son servicios que se reciben de forma inmediata.

-

No adquiere tiempo de uso de los servicios, ni beneficio alguno, ni inmunidad.

-

Todo caso avalado por nuestra política de devolución será tratado con el reinicio o corrección del servicio y posterior revisión garantizando el funcionamiento correcto del servicio reiniciado o corregido.

-
-
-
-

TIEMPO DE NOTIFICACIÓN

-

Cualquier error o problema de nuestros servicios debe ser notificado a la administración en un período máximo de 48 horas desde la entrega del servicio.

-

Pasado este período de tiempo, el usuario ya no puede solicitar ningún tipo de devolución.

-

Contacto: contacto@novawow.com.

-
-
-
-

COBERTURA

-

Esta política de devolución cubre:

-

- Adquisición del servicio incorrecto por una mala selección del usuario.

-

- Errores ocasionados por el sitio web que hayan impedido la entrega del servicio.

-

- Errores ocasionados por plataformas de terceros que hayan impedido la entrega del servicio.

-
-

Esta política de devolución NO cubre, bajo ninguna circunstancia:

-

- Arrepentimientos.

-

- Errores en el uso del servicio realizados por el usuario.

-

- Desconocimiento de nuestra política de devolución.

-

- Uso incorrecto de plataformas de terceros con el fin de violar nuestra política de devolución.

-

- Expulsión de nuestro sitio por incumplimiento de nuestros términos y normas.

-
-

Solicitud de reembolso hecha a través de la plataforma de pago.

-

Pedir un reembolso por razones avaladas en esta política de devolución procederá a un reembolso de nuestra parte con la correspondiente cancelación del servicio adquirido.

-
-

Pedir un reembolso por razones no avaladas en esta política de devolución como:

-

- La cuenta ha sido sancionada.

-

- La persona ya no usa el servicio e intenta solicitar un reembolso posterior a las 48 hs del momento de la adquisición.

-

- Abuso de herramientas disponibles alegando razones que falten a la verdad respecto del servicio entregado.

-

No aplican para devolución y puede conllevar a la expulsión permanente del sitio por falta a nuestros términos y un uso inadecuado de las plataformas de pago disponibles.

-
-
-
-

Arrepentimientos

-

Ningún tipo de arrepentimiento posterior a la entrega de un servicio aplica para devolución.

-
-
-
-

Errores en el uso del servicio realizados por el usuario

-

Errores en el uso de los servicios, ocasionados por el usuario, que no tienen relación con ningún tipo de falla en el servicio entregado no aplica para devolución.

-

Cualquier error de usuario ocasionado por no leer la descripción de un servicio no aplica para devolución.

-

Una vez que el servicio se encuentra entregado, el uso del mismo queda a responsabilidad del usuario.

-
-
-
-

Desconocimiento de nuestra política de devolución

-

El desconocimiento por parte del usuario de nuestra política de devolución no exime al usuario de su cumplimiento.

-
-
-
-

Uso incorrecto de plataformas de terceros con el fin de violar nuestra política de devolución

-

Este tipo de acciones conllevará a la expulsión del usuario de nuestro sitio sin ningún tipo de devolución.

-
-
-
-

Expulsión de nuestro sitio por incumplimiento de nuestros términos y normas

-

Ninguna adquisición de nuestros servicios otorga ningún tipo de inmunidad.

-

La expulsión de un usuario de nuestro sitio no aplica a ningún tipo de devolución.

-

El cumplimiento de nuestros términos y normas queda a exclusiva responsabilidad del usuario y cualquier expulsión que resulte del incumplimiento de los mismos no califica a solicitar devolución de los servicios ya entregados y disfrutados.

-
-
-
-

Cambios a esta Política de Reembolso

-

Nos reservamos el derecho de actualizar esta Política de Reembolso en cualquier momento. Le recomendamos que revise periódicamente esta página para conocer los cambios.

-
-
-
-

Contáctanos

-

Si tiene alguna pregunta o inquietud sobre esta Política de Reembolso, puede contactarnos a través de la siguiente información de contacto:

-

Error 404

-

novawow.com

-

info@novawow.com

-
-

Fecha de actualización: 25-05-2023

-
-
-
-
-
\ No newline at end of file diff --git a/home/templates/partials/register.html b/home/templates/partials/register.html deleted file mode 100644 index c59fddb..0000000 --- a/home/templates/partials/register.html +++ /dev/null @@ -1,71 +0,0 @@ -{% if request.session.username %} -
-
-
-

Creación de cuenta

-
-
-

Tu cuenta es de tipo Battle.net: se identifica con tu correo electrónico.

-

La contraseña debe ser alfanumérica y con una longitud de hasta 16 caracteres.

-

El correo debe pertenecer a gmail y con acceso al mismo.

-
-

Una vez que la cuenta se haya creado exitosamente, se enviará un correo con un enlace de activación.

-

De no usar el enlace de activación, la cuenta no podrá usarse para conectar al servidor.

-

Al iniciar sesión, ingresa con tu correo electrónico.

-
-

La cuenta del foro es independiente y se puede crear aquí.

-
-
- - -
-
-

¡Ya estás conectado!

-
-

Si quieres crear una cuenta, primero desconecta.

-
-
-
-
-
- {% else %} -
-
-
-

Creación de cuenta

-
-
-

Tu cuenta es de tipo Battle.net: se identifica con tu correo electrónico.

-

La contraseña debe ser alfanumérica y con una longitud de hasta 16 caracteres.

-

El correo debe pertenecer a gmail y con acceso al mismo.

-
-

Una vez que la cuenta se haya creado exitosamente, se enviará un correo con un enlace de activación.

-

De no usar el enlace de activación, la cuenta no podrá usarse para conectar al servidor.

-

Al iniciar sesión, ingresa con tu correo electrónico.

-
-

La cuenta del foro es independiente y se puede crear aquí.

-
-
- - -
-
- {% load django_vite %} - -
- {% vite_asset 'src/entries/register.tsx' %} -
-
-
-
-
-
- {% endif %} \ No newline at end of file diff --git a/home/templates/partials/rename_cancel.html b/home/templates/partials/rename_cancel.html deleted file mode 100644 index 644ca8d..0000000 --- a/home/templates/partials/rename_cancel.html +++ /dev/null @@ -1,35 +0,0 @@ -
-
-
-

Renombrar personaje

-
- -
-
-

La herramienta Renombrar personaje te permite Renombrar un personaje el nombre se cambiara en el proximo inicio de session.

-

Usa esta opción cuando quieras cambiar el nombre del personaje.

-
-
- NOTA -
-

Se requiere que el personaje esté desconectado.

-

Se puede repetir la acción las veces que quieras.

-
-
- -
-
-
-

Escoge el personaje que quieres renombrar

-
- -
-
- El pago ha sido cancelado. No se realizó ningún cambio. -
-
-
-
-
-
-
\ No newline at end of file diff --git a/home/templates/partials/rename_character.html b/home/templates/partials/rename_character.html deleted file mode 100644 index a6cc462..0000000 --- a/home/templates/partials/rename_character.html +++ /dev/null @@ -1,34 +0,0 @@ -
-
-
-

Renombrar personaje

-
- -
-
-

La herramienta Renombrar personaje te permite Renombrar un personaje el nombre se cambiara en el proximo inicio de session.

-

Usa esta opción cuando quieras cambiar el nombre del personaje.

-
-
- NOTA -
-

Se requiere que el personaje esté desconectado.

-

Se puede repetir la acción en un mismo las veces que quieras.

-
-
- {% load django_vite %} - -
- {{ characters|json_script:"character-purchase-data" }} - {% vite_asset 'src/entries/character_purchase.tsx' %} -
-
-
-
-
\ No newline at end of file diff --git a/home/templates/partials/rename_guild.html b/home/templates/partials/rename_guild.html deleted file mode 100644 index 400713c..0000000 --- a/home/templates/partials/rename_guild.html +++ /dev/null @@ -1,46 +0,0 @@ -
-
-
-

Renombrar Hermandad

-
-
-

Información

- - -
-
-
-

La herramienta Renombrar hermandad te permite cambiar de nombre a una hermandad de la cual seas Maestro de Hermandad.

-
-

A la hora de escoger un nuevo nombre, ten en cuenta:

-

- La longitud máxima es de 24 caracteres.

-

- Los caracteres permitidos son A-Za-z y el espacio.

-
-
- NOTA -
-

Por favor, asegúrate de haber elegido el nombre correcto ya que esta acción no es reversible una vez realizada.

-
-
-
-
-

Requiere {{ rename_cost }} EUR

-
- - {% if no_guild_leader_message %} - {{ no_guild_leader_message }} - {% else %} - {% load django_vite %} - -
- {{ guild_names|json_script:"rename-guild-data" }} - {% vite_asset 'src/entries/rename_guild.tsx' %} - {% endif %} -
-
-
-
-
-
diff --git a/home/templates/partials/rename_success.html b/home/templates/partials/rename_success.html deleted file mode 100644 index e69de29..0000000 diff --git a/home/templates/partials/restore_character.html b/home/templates/partials/restore_character.html deleted file mode 100644 index 73d3ba7..0000000 --- a/home/templates/partials/restore_character.html +++ /dev/null @@ -1,51 +0,0 @@ -
-
-
-

Recuperar personaje

-
-
-

Información

- - -
-
-
-

La herramienta Recuperar personaje te permite restaurar un personaje que haya sido borrado.

-
-

Sólo disponible para personajes que cumplan las siguientes dos condiciones:

-

- Nivel superior a 60.

-

- Borrado en un período menor a 30 días.

-

Para personajes Caballeros de la Muerte:

-

- No es posible recuperar un personaje Caballero de la Muerte si ya hay otro activo en la cuenta.

-
-

Cuando ingreses a la lista de personajes de tu cuenta, verás al personaje recuperado.

-
-
- NOTA -
-

Personajes borrados que no cumplan dichas condiciones ya no son recuperables.

-

Se puede repetir la acción en un mismo personaje cada 12 horas.

-
-
-
-

- {% if message %} -

{{ message }}

- {% endif %} - {% if character_list %} - {% load django_vite %} - -
- {{ character_data|json_script:"restore-character-data" }} - {% vite_asset 'src/entries/restore_character.tsx' %} - {% else %} - No hay personajes eliminados disponibles para restaurar. - {% endif %} -
-
-
-
-
-
diff --git a/home/templates/partials/restore_items.html b/home/templates/partials/restore_items.html deleted file mode 100644 index 3f8f350..0000000 --- a/home/templates/partials/restore_items.html +++ /dev/null @@ -1,32 +0,0 @@ -
-
-
-

Recuperar ítems

-
-
-

Información

- -
-
-

La herramienta Recuperar ítems te permite recuperar ítems que hayan sido borrados.

-

Sólo disponible para ítems de calidad: Raro | Épico | Legendaria | Artefacto | Reliquia.

- -
- NOTA -

Ítems que no cumplan las condiciones no son recuperables.

-

Se pueden consultar ítems borrados en un mismo personaje cada 8 horas.

-
- - {% load django_vite %} - -
- {{ character_names|json_script:"restore-items-data" }} - {% vite_asset 'src/entries/restore_items.tsx' %} -
-
-
-
-
diff --git a/home/templates/partials/revive_character.html b/home/templates/partials/revive_character.html deleted file mode 100644 index f842970..0000000 --- a/home/templates/partials/revive_character.html +++ /dev/null @@ -1,34 +0,0 @@ -
-
-
-
-

Revivir personaje

-
-
-
-

Información

- - -
-
-
- {% if characters %} -

Escoge el personaje que quieres revivir:

-
- {% load django_vite %} - -
- {{ characters|json_script:"character-purchase-data" }} - {% vite_asset 'src/entries/character_purchase.tsx' %} - {% else %} -

No tienes personajes para revivir.

- {% endif %} -
-
-
-
-
diff --git a/home/templates/partials/security_history.html b/home/templates/partials/security_history.html deleted file mode 100644 index 1fcbe34..0000000 --- a/home/templates/partials/security_history.html +++ /dev/null @@ -1,28 +0,0 @@ -
-
-
-

Historial de seguridad

-
- -
-
-

Aquí verás la actividad de la cuenta y los últimos 20 intentos de conexión de tu cuenta a la página web.

-

Si observas alguna actividad sospechosa puedes cambiar la contraseña o contactar con el equipo de {{ NOMBRE_SERVIDOR }}.

-
-
- -
-

Historial de conexiones

-
- {% load django_vite %} - -
- {{ login_attempts_data|json_script:"history-table-data" }} - {% vite_asset 'src/entries/history_table.tsx' %} -
-
-
-
-
diff --git a/home/templates/partials/security_token.html b/home/templates/partials/security_token.html deleted file mode 100644 index a154a2e..0000000 --- a/home/templates/partials/security_token.html +++ /dev/null @@ -1,47 +0,0 @@ -
-
-
-

Token de seguridad

-
- -
-
-

El Token de seguridad es un código de 6 dígitos (sensible a mayúsculas y minúsculas), que añade seguridad a ciertas opciones en tu panel de cuenta en la página web.

-

El mismo será solicitado para acciones importantes de la cuenta.

-
-

Para obtenerlo, es necesario solicitarlo haciendo click en el botón debajo.

-

Al hacer click, un mensaje será enviado al correo de la cuenta conteniendo el Token de Seguridad.

-
-

Es importante que conserves el Token de seguridad en un lugar seguro y no lo compartas con otros usuarios.

-
-
- NOTA -
-

Puedes solicitar un nuevo Token de seguridad una vez cada 7 días.

-
-
-
-
-
- {% load django_vite %} - -
- {% vite_asset 'src/entries/security_token.tsx' %} -
-

He olvidado el Token de seguridad

-
-
-
-
- -
-
-
\ No newline at end of file diff --git a/home/templates/partials/send_gift.html b/home/templates/partials/send_gift.html deleted file mode 100644 index dc074ee..0000000 --- a/home/templates/partials/send_gift.html +++ /dev/null @@ -1,81 +0,0 @@ -
-
- - -
-

Enviar regalo

-
- -
-
-

La herramienta Enviar regalo te permite enviar regalos a tus amigos.

-
-

Al elegir desde qué personaje se enviará el regalo y el personaje que lo recibirá, podrás ver la lista de objetos disponibles para enviar como regalo.

-

Los objetos disponibles son los mismos de la tienda y puedes enviar varios objetos a la vez.

-
-

Al usar el botón ENVIAR REGALO se enviará un correo al personaje destino con los ítems seleccionados, indicando quién lo ha regalado.

-
-

Si aún no tienes Token de seguridad, puedes solicitarlo aquí.

-
-
- NOTA -
-

Por favor, asegúrate de haber escrito el nombre del personaje correcto ya que esta acción no es reversible una vez realizada.

-
-
-
-
-
- No tienes PD/PV suficientes -
-
-
-
-
- - - - -
\ No newline at end of file diff --git a/home/templates/partials/social.html b/home/templates/partials/social.html deleted file mode 100644 index 4485163..0000000 --- a/home/templates/partials/social.html +++ /dev/null @@ -1,28 +0,0 @@ - \ No newline at end of file diff --git a/home/templates/partials/start_time.html b/home/templates/partials/start_time.html deleted file mode 100644 index ea2fa59..0000000 --- a/home/templates/partials/start_time.html +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/home/templates/partials/store/store_novawow.html b/home/templates/partials/store/store_novawow.html deleted file mode 100644 index 74f4105..0000000 --- a/home/templates/partials/store/store_novawow.html +++ /dev/null @@ -1,153 +0,0 @@ - -
-
-
-

Tienda de {{ NOMBRE_SERVIDOR }}

-
- -
- - - -
- - -
- - -
- - -
- - -
-

Carrito de Compras

-
-
-
    -

    Total: 0 EUR

    -

    Total con IVA (21%): 0 EUR

    - -
    -
    -
    -
    -
    - - diff --git a/home/templates/partials/store/store_novawow_cancel.html b/home/templates/partials/store/store_novawow_cancel.html deleted file mode 100644 index e69de29..0000000 diff --git a/home/templates/partials/store/store_novawow_success.html b/home/templates/partials/store/store_novawow_success.html deleted file mode 100644 index e69de29..0000000 diff --git a/home/templates/partials/terms_and_conditions.html b/home/templates/partials/terms_and_conditions.html deleted file mode 100644 index c86e570..0000000 --- a/home/templates/partials/terms_and_conditions.html +++ /dev/null @@ -1,107 +0,0 @@ -
    -
    -
    -

    Términos y condiciones

    -
    -
    - IMPORTANTE -

    Por favor, tómese un momento para leer los términos y condiciones detallados debajo. Si está de acuerdo con ellos, entonces podrá ingresar en nuestro sitio.

    -
    -
    -
    - ACEPTACIÓN DE LOS TÉRMINOS DE USO Y CONDICIONES -

    {{ NOMBRE_SERVIDOR }} es un proyecto sin ánimo de lucro desarrollado con la finalidad de simular versiones desactualizadas del servidor con fines únicamente educativos. Los Servicios ofrecidos por {{ NOMBRE_SERVIDOR }} no apoyan ni proveerán modificación de ningún tipo para con el servidor o sus ficheros, dicho esto, el usuario al hacer uso de este sitio o cualquier servicio perteneciente o derivado de la Administración, acepta hacerse responsable de seguir y cumplir con el contrato de licencia para usuarios finales (EULA) del servidor.

    -
    -

    {{ NOMBRE_SERVIDOR }} no provee ni se responsabiliza por la distribución de ningún cliente del servidor o fuente externa que lo contenga. Los enlaces de descarga para el cliente ofrecidos por este sitio o cualquier medio bajo su responsabilidad, apuntarán siempre a dichas fuentes externas, las cuales, a la fecha no pertenecen ni poseen relación alguna con esta administración. No haciéndose {{ NOMBRE_SERVIDOR }} de esta manera, y bajo ningún concepto, responsable sobre aspecto alguno sobre dicha fuente, dígase: calidad, rendimiento, garantías o cualquiera fuese o no, característica determinante a su fin.

    -
    -

    Al acceder, navegar o usar este sitio de Internet y en adelante (el “Sitio”), propiedad de la administración {{ NOMBRE_SERVIDOR }} en adelante (la “Administración”), el usuario admite haber leído y entendido los presentes términos y condiciones de uso, en adelante (“Términos y Condiciones”) y está de acuerdo en acatar los mismos y cumplir con todas las normas, leyes y reglamentos aplicables que hagan parte de la legislación vigente en su lugar de residencia, o a las que se sujete indistintamente. Además, cuando el usuario utilice cualquier servicio suministrado o referenciado en el Sitio, (siempre y cuando éste resida bajo responsabilidad o propiedad de la Administración) estará sujeto a las reglas, guías, políticas, términos y condiciones aplicables a dichos servicios.

    -
    -

    Este Sitio es controlado y desarrollado por la Administración, localizable a través de sus plataformas de Discord.

    -
    -

    La Administración no es responsable de que el material en este Sitio sea apropiado o esté disponible para su uso en otros lugares, estando prohibido su acceso desde territorios donde su contenido sea ilegal. Aquellos que decidan acceder a este Sitio desde otros lugares lo harán bajo su propia iniciativa, y es su responsabilidad el sujetarse a las leyes locales que sean aplicables.

    -
    -

    Estos Términos y Condiciones están sujetos a cambios a discreción, sin previo aviso y en cualquier momento, bajo la sola voluntad de {{ NOMBRE_SERVIDOR }}, y partiendo de la fecha de publicación de la modificación de los mismos en este Sitio, todas las operaciones celebradas entre la Administración y el usuario se regirán por el documento modificado.

    -
    -
    -
    - NUESTRO SERVICIO: USO, DISTRIBUCIÓN Y DESCARGO DE RESPONSABILIDADES -

    De conformidad con las legislaciones bajo las cuales la Administración se acate, el material contenido en este Sitio o cualquier otro que se encontrase bajo su responsabilidad o tutela y en adelante, (los “Servicios” o el “Servicio”), incluyendo sin limitación, toda imagen, links, logotipo, diseño, insignia, marca, fotografía, sonido, texto, mensaje, herramienta, software, tecnología, producto, archivo, información, dato, demostración, muestra, material promocional, obra audiovisual u obra multimedia, y cualquiera sea otro elemento o forma de expresión (de forma colectiva y en adelante, (los "Materiales"), son suministrados según nuestro leal saber y entender, sin embargo, no es posible descartar errores de contenido o del material, razón por la cual la Administración no asume responsabilidad por la calidad, veracidad o exactitud, de la información publicada ni por las omisiones, errores o discrepancias que pudiesen encontrarse en la información publicada o servicio brindado.

    -
    -

    Respecto a la información publicada en los Servicios, la Administración no confiere garantías de ningún tipo, expresas ni implícitas, excluyendo, más no limitado a:

    -

    - Garantías de comerciabilidad o adecuación para un propósito particular.

    -

    - Estabilidad ininterrumpida del Servicio.

    -

    - Existencia de errores o virus, así como sobre su seguridad.

    -

    - Exactitud, confiabilidad, calificación o certificación para su descarga.

    -
    -

    La Administración no se hace responsable sobre el uso que el usuario haga de los contenidos incluidos en los Servicios, ni de las decisiones que tome respecto a éstos. La información respecto a los productos y servicios de {{ NOMBRE_SERVIDOR }},incluida en sus Servicios es brindada con fines edcuativos no comerciales y no constituye oferta de venta para sus usuarios.

    -
    -

    {{ NOMBRE_SERVIDOR }} se reserva el derecho de a discreción:

    -

    - Corregir cualquier error, omisión o inexactitud en los datos brindados.

    -

    - Cambiar o actualizar, descontinuar o eliminar la información contenida en los Servicios en cualquier momento y sin previo aviso.

    -

    - Así como también se exime de responsabilidad sobre lo que refiere a la puntualidad, falta de almacenamiento, inexactitud o entrega incorrecta de cualquier información o dato de los que “normalmente” proporcione, ya sea temporal o permanentemente.

    -
    -
    -
    - RESPONSABILIDADES Y OBLIGACIONES DE REGISTRO Y CONTRASEÑAS -

    El usufructo de las principales funciones fundamentales y objetivas del Servicio estará siempre determinado y sujeto a la existencia de un registro personal e intransferible (dígase cuenta de usuario). La Administración le exhorta a proporcionar información veraz y comprobable según fuese la misma solicitada. Más allá de un carácter intrusivo, dicha información se haría utilizar estricta y discretamente en aras de posibilitar a futuro, una resolución por parte de la Administración, de alguna situación extraordinaria que surgiese por descuidos de parte del usuario, intentando así dictar la misma en la forma menos perjudicial posible para con este.

    -
    -

    Pertenece completa y estrictamente, y recae sobre el usuario la responsabilidad de mantener confidenciales y de forma segura las contraseñas establecidas para con el Servicio. Así como de las acciones a través del uso de su registro que pudiesen estar estas bajo autorización o no indistintamente por parte del usuario. Queda así también bajo responsabilidad del usuario, la notificación inmediata a la Administración si éste notase alguna actividad sospechosa, uso no autorizado o registro no concebido en el que figurase su propia cuenta como objeto de manipulación, o violación.

    -
    -

    Queda terminantemente prohibido el intercambio, comercio, compra-venta y cualquier actividad similar de cualquier forma y bajo cualquier concepto donde figurase e involucrase el conocimiento por parte de cualquiera persona ajena al usuario; de sus credenciales de acceso al Servicio. Violar dichas prohibiciones podría hacer incurrir al usuario en una falta grave, la cual podría sancionarse con penalizaciones de magnitud determinada por la Administración a su juicio y a discreción, la cual en ningún caso quedaría exenta de dicha penalización mediante alegato de uso por parte de terceros y en general bajo ninguna justificación.

    -
    -

    Los personajes del reino que hubiesen sido borrados y alcanzados estos, el límite máximo de tiempo (on-hold) para recuperación, establecido por la Administración, desaparecerán permanentemente, claudicando con ellos la posibilidad de restauración o asistencia por parte de la Administración en estos casos, independientemente y descartando por completo la voluntad del usuario para con tal acción en dicho momento.

    -
    -
    -
    - HACER TRAMPA -

    Se entiende como trampa cualquier acto fuera y dentro del servidor, que intente alterar o de hecho altere e interfiera con las reglas o con el comportamiento normal del servidor. Así como intentar o de hecho aventajarse sobre otros jugadores. Entre ello, a título meramente enunciativo y sin ánimo limitativo, cualquiera de los siguientes comportamientos, ya sea en su propio nombre o por cuenta de terceros será considerado como tal:

    -

    - Acceder a los Servicios de manera no autorizada (incluyendo el uso de software modificado o malicioso).

    -

    - Utilizar varias cuentas a la vez en un mismo reino de una forma en la que controlar varios personajes al unísono y sobre el mismo reino, suponga una desventaja de cualquier índole para otros usuarios con respecto infractor.

    -

    - Utilizar cualquier técnica o software para alterar o manipular las variables preestablecidas por el servidor y la Administración.

    -
    -

    Se prohíbe terminantemente hacer trampas y la Administración se reserva el derecho de denegar a cualquier usuario el acceso al Servicio, siempre y cuando hubiere incurrido o fuese objeto de una falta de este tipo ya fuese directa e indirectamente.

    -
    -
    -
    - COMPRAS, RECLAMACIONES, REEMBOLSOS Y GARANTÍAS -

    {{ NOMBRE_SERVIDOR }} no representa ni constituye entidad remesadora ni de servicios financieros de ninguna índole y en consecuencia no posee control o responsabilidad alguna sobre el origen de aquellos fondos utilizados para realizar compras hacia su comunidad. Así como tampoco se responsabiliza por los riesgos que el usuario tomase bajo su propia voluntad, a la hora de ejecutar operaciones financieras de cualquier tipo.

    -
    -

    Las compras: serán consideradas siempre de fe voluntaria y en ningún caso, obligación de ningún tipo para ningún usuario. Nótese además que quedan terminantemente prohibidos las compras por parte de personas naturales menores de 18 años o dígase en cualquier caso aquellas personas que no alcanzaran aún al amparo de las entidades legales locales y pertinentes que le competan, su mayoría de edad.

    -
    -

    El Servicio: en cualquier caso, no requiere ningún tipo de compra para un uso y disfrute total y a plenitud. Dicho esto, el usuario contará con un canal de comunicación oficial vía email contacto@novawow.com con el objetivo de notificar a la Administración de cualquier error cometido por cualquiera de las partes, en aras de emitir y hacer efectiva justa resolución

    -
    -

    La Administración: garantiza y se compromete en brindar al comprador toda información que requiriese sobre las formas o métodos de compra vigentes para con la comunidad. Así como aclarar cualquier duda que pudiese surgir sobre los pasos o procesos establecidos de las mismas, siempre y cuando dicha información resida dentro de los límites que competan al comprador y posea un propósito únicamente informativo.

    -
    -

    Tomado en cuenta lo anterior, el comprador cuenta con garantías de reembolso monetario y de servicios, atendiendo a aspectos y condiciones expuestos a continuación.

    -

    Monetarios:

    -

    Tomando en cuenta el carácter que posee cualquier transacción monetaria hacia las arcas de la comunidad, la Administración se compromete a reembolsar (libre de costos por transacción, a consideración de la misma), de forma total o parcial, en definitiva, colegiada conjuntamente entre ambas partes, aquellas compras donde no intencionalmente, el usuario quedase bajo la inconveniencia a la hora de realizar la misma, de errores por su parte en la cuantía del monto total a transferir. Siendo dicho error en todos los casos veraz y de comprobable naturaleza a consideración de la administración.

    -
    -

    Tomando en cuenta que los servicios adquiridos se reciben de forma inmediata y no constituyen, determinan, pactan ni modifican plazo alguno para su uso. Así como tampoco para el usufructo del servicio y sus funcionalidades los cuales, recalcado sea de paso, se consideran completamente gratuitos, de carácter educativo y carecen de plazos de admisión y vencimiento.

    -
    -

    La Administración no garantiza ni proporcionará en ningún caso ni bajo ningún concepto o circunstancia, reembolso alguno sobre compras o cualquier bien en general transferido a la comunidad. Nótense y háganse aclaratorios lo siguientes casos:

    -

    - A usuarios cuyas cuentas hayan sido sancionadas temporal o permanentemente por incurrir en alguna falta expuesta en cualquiera de sus Términos y Condiciones.

    -

    - A usuarios que hubiesen tomado la decisión de no continuar (parcial o temporalmente) haciendo uso de cualquiera de los servicios prestados por La Administración.

    -
    -

    En ninguno de estos casos la Administración presentará completa ni parcialmente traza alguna de ninguno de sus registros monetarios o de cualquier tipo para fines resolutorios.

    -
    -

    Igualmente, el usuario dispone de las respectivas vías y canales, dígase dentro de los foros del Servicio, o servidor de Discord para presentar una apelación formal ante un administrador y ser así evaluado el caso para posterior resolución. La Administración se reserva el derecho de evaluar más no garantizar y a discreción, reembolsos en aquellos casos donde después de estudiado y conciliado, fuese considerado por la misma, justo y extraordinario.

    -
    -

    De servicios:

    -

    La Administración le garantiza la integridad y funcionalidad de todos los artículos comprados en su tienda. En todo caso, ésta le proveerá un término de 48 horas posterior a su compra para solicitar reemplazos por artículos o servicios de igual valor, o también y a solicitud, la devolución íntegra de los servicios utilizados en aquellos casos donde se encontrarse dicho artículo o servicio roto o inutilizable, o hubiere sido comprado o elegido por error, todo esto siempre y después de efectuado un análisis pertinente por parte de un administrador, y de demostradas pruebas auténticas y concluyentes por parte del usuario a fines con el caso.

    -
    -

    Así como también la Administración garantizará la restauración del servicio por problemas técnicos y puntuales, entre el Servicio y las plataformas de pago siempre ajenos al comprador, no fuesen entregados dichos servicios de forma automática e inmediata a nombre del mismo.

    -
    -
    -
    - POLÍTICA DE PRIVACIDAD -

    El Servicio y la Administración recopilan información referente al usuario tales como dirección de correo electrónico, nombre, contraseña, idioma y zona horaria, con el fin de brindarle una mejor optimización cada vez a sus servicios, utilizando esta información para acomodar las funcionalidades a un mejor rendimiento a fin con el usuario y sus requerimientos. Dicha información también podrá ser usada para mantener al usuario mediante canales de comunicaciones independientes y no referentes al Servicio, informando sobre noticias importantes, actualizaciones, problemas técnicos o cualquier otra información de tal índole que considerase la Administración relevante a los intereses del usuario.

    -
    -

    La Administración y el Servicio se comprometen a salvaguardar la confidencialidad sobre tales datos, así como a garantizar la NO divulgación de estos, exceptuando los casos donde las leyes vigentes que rigiesen sobre la Administración lo consideraran necesario y mandatorio por motivos no referentes a {{ NOMBRE_SERVIDOR }}.

    -
    -

    Error 404

    -

    info@novawow.com

    -
    -
    -
    -
    -
    \ No newline at end of file diff --git a/home/templates/partials/trade_points.html b/home/templates/partials/trade_points.html deleted file mode 100644 index 3a59965..0000000 --- a/home/templates/partials/trade_points.html +++ /dev/null @@ -1,156 +0,0 @@ -
    -
    -
    -

    Comercio de PD

    -
    - -
    -
    -

    La herramienta Comercio de PD permite comprar o vender PD a cambio de oro de una forma segura.

    -
    -

    El sistema permite crear códigos de 1 solo uso que valdrán X cantidad de PD a cambio de X cantidad de oro.

    -

    Los vendedores disponen de la sección VENTA para crear el código y los compradores disponen de la sección COMPRA para canjear dicho código.

    -
    -
    -

    Venta

    -

    1) Decide cuánto valdrá el código en PD.

    -

    2) Decide por cuánto oro se podrá canjear.

    -

    3) Escoge a qué personaje de la cuenta se enviará el oro cuando el código sea canjeado.

    -

    4) Completa los datos con la contraseña y Token de Seguridad.

    -

    5) Usa el botón CREAR CÓDIGO.

    -

    6) Un recuadro aparecerá con los datos del Código creado con el ID que se debe usar para enviar a otro jugador.

    -
    -
    -
    -

    Compra

    -

    1) Ingresa ID del código que te ha dado otro jugador.

    -

    2) Chequea el valor en PD y costo en oro con el botón CHEQUEAR CÓDIGO.

    -

    3) Escoge de qué personaje de la cuenta se descontará el oro cuando el código sea canjeado.

    -

    4) Completa los datos con la contraseña y Token de Seguridad.

    -

    5) Usa el botón CANJEAR CÓDIGO.

    -

    6) Un recuadro aparecerá con los datos del Código canjeado, PD recibidos y oro retirado del personaje seleccionado.

    -
    -
    -
    -

    Cada código tiene una duración de 1 hora.

    -

    Una vez canjeado el código:

    -

    - Los PD serán retirados de la cuenta del creador del código y enviados a quien lo haya canjeado.

    -

    - El oro será retirado y enviado por correo al creador.

    -

    - La transacción quedará registrada en el Historial de PD y PV.

    -
    -

    La cuenta de quien canjee el código será bloqueada por 5s como medida de seguridad si el trámite es exitoso.

    -
    -

    Si aún no tienes Token de seguridad, puedes solicitarlo aquí.

    -
    -
    - NOTA -
    -

    Por favor, asegúrate de haber escrito todos los datos correctos ya que esta acción no es reversible una vez realizada.

    -
    -
    - -
    -
    -
    - - - - -
    -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    ¿Cuánto valdrá el código en PD?
    ¿Cuánto costará el código en oro?
    ¿A qué personaje se enviará el oro?
    - -

    -
    -
    - -
    -
    -
    -
    -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Ingresa el ID del código de PD
    ¿De qué personaje se cobrará el oro?
    - -

    -
    -
    -
    -
    -
    -
    - -
    -
    -
    -
    -
    -
    -
    \ No newline at end of file diff --git a/home/templates/partials/trans_history.html b/home/templates/partials/trans_history.html deleted file mode 100644 index bcf0e29..0000000 --- a/home/templates/partials/trans_history.html +++ /dev/null @@ -1,40 +0,0 @@ -
    -
    -
    -

    Historial de transacciones

    -
    -
    -

    Información

    - - -
    -
    -
    -

    Aquí verás la información de las transacciones realizadas.

    -
    -

    ¿Has tenido algún problema con alguna transacción?

    -

    Si has tenido un problema con alguna de las transacciones o no has recibido tus productos, puedes contactarnos en nuestro Discord o a través del correo contacto@novawow.com.

    -
    -
    - -
    -
    -

    Historial de transacciones

    -
    -
    - {% load django_vite %} - -
    - {{ transactions_data|json_script:"history-table-data" }} - {% vite_asset 'src/entries/history_table.tsx' %} -
    -
    - -
    -
    -
    -
    -
    -
    diff --git a/home/templates/partials/transfer_cancel.html b/home/templates/partials/transfer_cancel.html deleted file mode 100644 index e69de29..0000000 diff --git a/home/templates/partials/transfer_character.html b/home/templates/partials/transfer_character.html deleted file mode 100644 index 7328d99..0000000 --- a/home/templates/partials/transfer_character.html +++ /dev/null @@ -1,23 +0,0 @@ -
    -
    -
    -

    Transferir personaje

    -
    -
    -

    Información

    - -
    -
    - {% load django_vite %} - -
    - {{ character_names|json_script:"transfer-characters-data" }} - {% vite_asset 'src/entries/transfer_character.tsx' %} -
    -
    -
    -
    -
    diff --git a/home/templates/partials/transfer_d_points.html b/home/templates/partials/transfer_d_points.html deleted file mode 100644 index 40d871a..0000000 --- a/home/templates/partials/transfer_d_points.html +++ /dev/null @@ -1,33 +0,0 @@ -
    -
    -
    -

    Transferir pd

    -
    - -
    -
    -

    La herramienta Transferir PD permite transferir PD desde tu cuenta a cualquier otra cuenta.

    -
    -

    Al usar el botón TRANSFERIR PD se transferirá la cantidad que hayas escogido desde tu cuenta a la cuenta del personaje que hayas indicado.

    -
    -

    Si aún no tienes Token de seguridad, puedes solicitarlo aquí.

    -
    -
    - NOTA -
    -

    Por favor, asegúrate de haber escrito el nombre del personaje correcto ya que esta acción no es reversible una vez realizada.

    -
    -
    - -
    -
    -
    - No tienes PD - -
    -
    -
    -
    -
    -
    -
    \ No newline at end of file diff --git a/home/templates/partials/transfer_success.html b/home/templates/partials/transfer_success.html deleted file mode 100644 index e69de29..0000000 diff --git a/home/templates/partials/unstuck_character.html b/home/templates/partials/unstuck_character.html deleted file mode 100644 index 4803513..0000000 --- a/home/templates/partials/unstuck_character.html +++ /dev/null @@ -1,39 +0,0 @@ -
    -
    -
    -
    -

    Desbloquear personaje

    -
    -
    -
    -

    Información

    - - -
    -
    -

    La herramienta Desbloquear personaje te permite desbloquear un personaje que esté atascado y lo mueve a la ubicación de su Piedra Hogar.

    -

    Usa esta opción cuando tu personaje se encuentre atrapado y ninguna otra opción te sirva.

    -
    - NOTA -
    -

    Se requiere que el personaje esté desconectado.

    -

    Se puede repetir la acción en un mismo personaje cada 12 horas.

    -
    -
    -
    -

    Escoge el personaje que quieres desbloquear

    - {% load django_vite %} - -
    - {{ characters|json_script:"character-purchase-data" }} - {% vite_asset 'src/entries/character_purchase.tsx' %} -
    -
    -
    -
    -
    -
    diff --git a/home/templates/partials/video.html b/home/templates/partials/video.html deleted file mode 100644 index fe1d43c..0000000 --- a/home/templates/partials/video.html +++ /dev/null @@ -1,13 +0,0 @@ -
    - -
    -
    - -
    -
    - -
    \ No newline at end of file diff --git a/home/templates/partials/videos.html b/home/templates/partials/videos.html deleted file mode 100644 index d79eed7..0000000 --- a/home/templates/partials/videos.html +++ /dev/null @@ -1,50 +0,0 @@ - -
    -
    -
    - {% if content_creator %} -
    -

    {{ content_creator.title }}

    -
    -
    -
    - {{ content_creator.description|safe }} -

    - - {% if content_creator.youtube_video_url or content_creator.avatar_image_url %} -
    -
    - {% if content_creator.avatar_image_url %} - Avatar - {% endif %} - - {% if content_creator.youtube_video_url %} - - {% endif %} - -

    {{ content_creator.subtitle }}

    -
    -

    Tipo de contenido:

    -

    {{ content_creator.content_type }}

    -
    -

    Medios:

    -
      - {% if content_creator.youtube_channel_url %} -
    • YouTube
    • - {% endif %} - {% if content_creator.facebook_page_url %} -
    • Facebook
    • - {% endif %} -
    -
    -
    - {% endif %} -
    -
    - {% else %} -

    No hay información disponible en este momento.

    - {% endif %} -
    -
    -
    diff --git a/home/templates/partials/vote_points.html b/home/templates/partials/vote_points.html deleted file mode 100644 index 9756792..0000000 --- a/home/templates/partials/vote_points.html +++ /dev/null @@ -1,65 +0,0 @@ -
    -
    -
    -

    Votar por nosotros

    -
    - -
    -
    - ¿Qué son las votaciones? -

    Una votación es la acción de darle un voto al servidor en una página web que ofrezca listas de tops.

    -

    Como recompensa a realizar una votación por {{ NOMBRE_SERVIDOR }}, se otorgan PV.

    -
    -
    -
    - ¿Qué son los PV? -

    Brindamos a nuestros usuarios la oportunidad de obtener niveles con beneficios exclusivos en nuestra comunidad en línea. Estos beneficios incluyen acceso a herramientas especiales, contenido exclusivo y mejoras significativas en la experiencia de nuestro sitio web. Al realizar una votación, los usuarios reciben PV, una unidad de reconocimiento interna que refleja su compromiso y progreso en nuestra plataforma. Los PV permiten alcanzar nuevos niveles y desbloquear recompensas exclusivas. Destacamos que los PV son una forma única de valorar y premiar la participación activa de nuestros usuarios en la comunidad.

    -
    -
    -
    - ¿Cómo se puede votar? -

    Para votar por {{ NOMBRE_SERVIDOR }}, se debe hacer click en los botones que aparecen debajo, y cada uno de ellos redirigirá a la página web del top correspondiente.

    -

    Una vez allí, deberás darle un voto al servidor y así recibirás tus PV.

    -
    -
    -
    - ¿Cuántos PV se reciben por cada voto? -

    En la lista inferior, cada sitio indica cuántos PV otorgará si se realiza una votación exitosa.

    -
    -
    -
    - ¿Cada cuánto se puede votar? -

    Sólo se puede votar por cada sitio cada 12 horas.

    -

    El recuadro informa sobre la última vez que has votado en cada sitio.

    -
    -
    -
    - ¿Por qué el voto no cuenta en la web? -

    Usamos el sistema de postback por lo que cada sitio web de votos nos envía una respuesta cada vez que un usuario vota por nuestro servidor.

    -

    Una vez que la respuesta es analizada en nuestra web, se sumarán los PV correspondientes siempre que el usuario haya votado correctamente.

    -

    Cada sitio de votación tiene, además, sus propias restricciones como sólo permitir un voto por IP por lo cual puede suceder que el voto no cuente si se usan varias cuentas para votar en el mismo día.

    -
    -
    -
    - ¿Tienes dudas o problemas para votar? -

    Si a pesar de leer este instructivo aún tienes dudas, el equipo de {{ NOMBRE_SERVIDOR }} podrá asesorarte en cualquier momento. Puedes colocar un ticket solicitando ayuda para votar.

    -
    -
    - -
    -

    Sitios de votación

    -
    - {% load django_vite %} - -
    - {{ vote_sites_data|json_script:"vote-points-data" }} - {% vite_asset 'src/entries/vote_points.tsx' %} -
    -
    -
    -
    -
    -
    \ No newline at end of file diff --git a/home/templates/realms/novawow_players.html b/home/templates/realms/novawow_players.html deleted file mode 100644 index 95be040..0000000 --- a/home/templates/realms/novawow_players.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% include 'partials/novawow_players.html' %} - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} \ No newline at end of file diff --git a/home/templates/realms/novawow_realm.html b/home/templates/realms/novawow_realm.html deleted file mode 100644 index 223e6c0..0000000 --- a/home/templates/realms/novawow_realm.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% include 'partials/novawow-realm.html' %} - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} \ No newline at end of file diff --git a/home/templates/recruit/recruit_a_friend.html b/home/templates/recruit/recruit_a_friend.html deleted file mode 100644 index b24a380..0000000 --- a/home/templates/recruit/recruit_a_friend.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% include 'partials/recruit_a_friend.html' %} - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} \ No newline at end of file diff --git a/home/templates/server_status/server_status.html b/home/templates/server_status/server_status.html deleted file mode 100644 index be3b83f..0000000 --- a/home/templates/server_status/server_status.html +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - -
    {{ server.name }} {{ game_version }}2587
    Login{{ status }}
    Hora

    {{ server.address }}

    diff --git a/home/templates/store/store_novawow.html b/home/templates/store/store_novawow.html deleted file mode 100644 index 80c6f69..0000000 --- a/home/templates/store/store_novawow.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% include 'partials/store/store_novawow.html' %} - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} \ No newline at end of file diff --git a/home/templates/store/store_novawow_cancel.html b/home/templates/store/store_novawow_cancel.html deleted file mode 100644 index 094b5ec..0000000 --- a/home/templates/store/store_novawow_cancel.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% include 'partials/store/store_novawow_cancel.html' %} - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} \ No newline at end of file diff --git a/home/templates/store/store_novawow_success.html b/home/templates/store/store_novawow_success.html deleted file mode 100644 index 914d1ea..0000000 --- a/home/templates/store/store_novawow_success.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {% include 'partials/head.html' %} - {% include 'partials/header.html' %} - {% include 'partials/video.html' %} - {% include 'partials/store/store_novawow_success.html' %} - {% include 'partials/social.html' %} - {% include 'partials/footer.html' %} - {% include 'partials/final.html' %} \ No newline at end of file diff --git a/home/tests.py b/home/tests.py deleted file mode 100644 index 7ce503c..0000000 --- a/home/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/home/urls.py b/home/urls.py deleted file mode 100644 index a588541..0000000 --- a/home/urls.py +++ /dev/null @@ -1,87 +0,0 @@ -from django.urls import path -from . import views -from .views import home_view, download_client_view, content_creators_view, stripe_webhook - -urlpatterns = [ - path('', views.home_view, name='index'), # Ruta para la página principal - path('download-client/', views.download_client_view, name='download_client'), - path('content-creators/', views.content_creators_view, name='content_creators'), - path('log-in/', views.login_view, name='login'), - path('log-out/', views.logout_view, name='log-out'), - path('select-account/', views.select_account_view, name='select-account'), - path('create-account/', views.register_view, name='register'), - path('novawow-realm/', views.novawow_realm_view, name='novawow_realm'), - path('recover/', views.recover_account_view, name='recover'), - path('reset-password', views.reset_password_view, name='reset_password'), - path('contact-us/', views.contact_us_view, name='contact_us'), - path('legal-notice/', views.legal_notice_view, name='legal_notice'), - path('terms-and-conditions/', views.terms_and_conditions_view, name='terms_and_conditions'), - path('privacy-policy/', views.privacy_policy_view, name='privacy_policy'), - path('refund-policy/', views.refund_policy_view, name='refund_policy'), - path('cookies/', views.cookies_view, name='cookies'), - path('download-addons/', views.download_addons_view, name='download_addons'), - path('recruit-a-friend/', views.recruit_a_friend_view, name='recruit_a_friend'), - path('my-account/', views.my_account, name='my-account'), - path('not-found/', views.not_found, name='not-found'), - path('change-password/', views.change_password_view, name='change_password'), - path('security-token/', views.security_token_view, name='security_token'), - path('change-email/', views.change_email_view, name='change-email'), - path('confirm-old-email/', views.confirm_old_email_view, name='confirm-old-email'), - path('confirm-new-email/', views.confirm_new_email_view, name='confirm-new-email'), - path('expired-link/', views.expired_link_view, name='expired-link'), - path('promo-code/', views.promo_code_view, name='promo_code'), - path('transfer-d-points/', views.transfer_d_points_view, name='transfer_d_points'), - path('rename-guild/', views.rename_guild_view, name='rename-guild'), - path('vote-points/', views.vote_points_view, name='vote_points'), - path('d-points/', views.d_points_view, name='d_points'), - path('points-history/', views.points_history_view, name='points_history'), - path('trans-history/', views.trans_history_view, name='trans_history'), - path('ban-history/', views.ban_history_view, name='ban-history'), - path('security-history/', views.security_history_view, name='security_history'), - path('trade-points/', views.trade_points_view, name='trade_points'), - path('activate-account', views.activate_account_view, name='activate_account'), - path('help', views.help_view, name='help'), - path('novawow-players', views.novawow_players_view, name='novawow_players'), - path('unstuck-character/', views.unstuck_character_view, name='unstuck-character'), - path('revive-character/', views.revive_character_view, name='revive-character'), - path('rename-character/', views.rename_character_view, name='rename-character'), - path('rename-success/', views.rename_success_view, name='rename-success'), - path('rename-cancel/', views.rename_cancel_view, name='rename-cancel'), - path('customize-character/', views.customize_character_view, name='customize-character'), - path('customize-success/', views.customize_success_view, name='customize-success'), - path('customize-cancel/', views.customize_cancel_view, name='customize-cancel'), - path('change-race-character/', views.change_race_character_view, name ='change-race-character'), - path('change-race-success/', views.change_race_success_view, name='change-race-success'), - path('change-race-cancel/' , views.change_race_cancel_view, name='change-race-cancel'), - path('change-faction-character/', views.change_faction_character_view, name='change-faction-character'), - path('change-faction-success/', views.change_faction_success_view, name='change-faction-success'), - path('change-faction-cancel/' , views.change_faction_cancel_view, name='change-faction-cancel'), - path('level-up-character/', views.level_up_character_view, name='level_up_character'), - path('level-up-success/', views.level_up_success_view, name='level-up-success'), - path('level-up-cancel/' , views.level_up_cancel_view, name='level-up-cancel'), - path('gold-character/' , views.gold_character_view, name='gold-character'), - path('gold-success/' , views.gold_success_view, name='gold-success'), - path('gold-cancel/' , views.gold_cancel_view, name='gold-cancel'), - path('transfer-character/' , views.transfer_character_view, name='transfer-character'), - path('transfer-success/' , views.transfer_success_view, name='transfer-success'), - path('transfer-cancel/' , views.transfer_cancel_view, name='transfer-cancel'), - path('store-novawow/buscar/categoria//', views.store_novawow_buscar_categoria_view, name='store_novawow/buscar/categoria/'), - path('store-novawow/' , views.store_novawow_view, name='store-novawow'), - path('store-novawow/success', views.store_novawow_success_view, name='store-novawow/success'), - path('store-novawow/cancel', views.store_novawow_cancel_view, name='store-novawow/cancel'), - path('maintenance/', views.maintenance_view, name='maintenance'), - path('restore-character/', views.restore_character_view, name='restore-character'), - path('restore-items/', views.restore_items_view, name='restore-items'), - path('quest-character/', views.quest_character_view, name='quest-character'), - path('send-gift/', views.send_gift_view, name='send-gift'), - path("webhook/stripe/", stripe_webhook, name="stripe-webhook"), - path('guild-success/', views.guild_rename_success_view, name='guild-success'), - path('guild-cancel/', views.guild_rename_cancel_view, name='guild-cancel'), - path("pagar-sumup/", views.pagar_sumup, name="pagar_sumup"), - path("battlepay/", views.battlepay_view, name="battlepay"), - path("battlepay/pay/", views.battlepay_pay_view, name="battlepay_pay"), - path("sumup-webhook/", views.sumup_webhook, name="sumup_webhook"), - -] - -handler404 = 'home.views.not_found' diff --git a/home/views/__init__.py b/home/views/__init__.py deleted file mode 100644 index 46bcfcd..0000000 --- a/home/views/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -"""Paquete de vistas de NovaWoW (antes un único home/views.py de ~3200 líneas). - -Dividido por secciones. El `import *` de cada submódulo re-exporta las vistas -para que `home.views.`, `from home.views import ` y la referencia -por cadena `'home.views.not_found'` (handler404) sigan funcionando igual. -""" -from ._base import * # noqa: F401,F403 -from .auth import * # noqa: F401,F403 -from .pages import * # noqa: F401,F403 -from .account import * # noqa: F401,F403 -from .recruit import * # noqa: F401,F403 -from .characters import * # noqa: F401,F403 -from .store import * # noqa: F401,F403 -from .points import * # noqa: F401,F403 -from .guild import * # noqa: F401,F403 -from .history import * # noqa: F401,F403 -from .players import * # noqa: F401,F403 -from .payments import * # noqa: F401,F403 -from .api import * # noqa: F401,F403 diff --git a/home/views/_base.py b/home/views/_base.py deleted file mode 100644 index 27565f5..0000000 --- a/home/views/_base.py +++ /dev/null @@ -1,101 +0,0 @@ -"""Imports y utilidades compartidas por el paquete de vistas (`home.views`). - -Cada submódulo de vistas hace `from ._base import *` para heredar estos imports -y el decorador `require_paid_stripe`. -""" -import socket, hashlib, gmpy2, binascii, os, re, json, logging, secrets -from collections import Counter -from datetime import datetime, timedelta -from decimal import Decimal, ROUND_HALF_UP -from functools import wraps - -from django import forms -from django.conf import settings -from django.contrib import messages -from django.contrib.auth import logout -from django.db import connections -from django.http import HttpResponse, JsonResponse -from django.shortcuts import render, redirect -from django.urls import reverse -from django.utils import timezone -from django.utils.crypto import get_random_string -from django.utils.timezone import now -from django.views.decorators.csrf import csrf_exempt -import stripe - -from ..ac_soap import execute_soap_command -from ..library_correo import enviar_correo -from ..pagos_stripe import create_checkout_session, is_session_paid -from ..pagos_sumup import crear_checkout, crear_checkout_battlepay, obtener_checkout -from ..zone_definitions import get_zone_name -from .. import bnet -from ..models import ( - Noticia, ClienteCategoria, ServerSelection, RecruitAFriend, DownloadClientPage, - ContentCreator, RecruitReward, ClaimedReward, AccountActivation, SecurityToken, - GuildRenameSettings, VoteSite, VoteLog, HomeApiPoints, UnstuckHistory, ReviveHistory, - RenamePrice, CustomizePrice, ChangeRacePrice, ChangeFactionPrice, LevelUpPrice, - GoldPrice, TransferPrice, Category, Item, StripeLog, LoginAttempt, Pedido, - PasswordReset, -) - -# Configuración del logger -logger = logging.getLogger(__name__) - - -def verify_turnstile(request): - """Verifica el token de Cloudflare Turnstile enviado por el formulario. - - Devuelve True si no hay secreto configurado (captcha desactivado) o si Cloudflare - confirma el token. Ante fallo de red devuelve False (fail-closed). - """ - import json - import urllib.parse - import urllib.request - - secret = settings.TURNSTILE_SECRET_KEY - if not secret: - return True - token = request.POST.get('cf-turnstile-response', '').strip() - if not token: - return False - payload = urllib.parse.urlencode({ - 'secret': secret, - 'response': token, - 'remoteip': request.META.get('REMOTE_ADDR', ''), - }).encode() - try: - req = urllib.request.Request( - 'https://challenges.cloudflare.com/turnstile/v0/siteverify', data=payload) - with urllib.request.urlopen(req, timeout=5) as resp: - result = json.loads(resp.read().decode()) - return bool(result.get('success')) - except Exception as e: - logger.warning("Turnstile: fallo al verificar: %s", e) - return False - - -def require_paid_stripe(redirect_to='index'): - """ - Protege las vistas de "éxito" de pago: exige un `session_id` de Stripe - realmente PAGADO en la URL (Stripe lo añade tras el pago) y evita re-entregas - marcando el StripeLog como `fulfilled`. Cierra el bypass de ir directo a la - success-url sin pagar. - """ - def deco(view): - @wraps(view) - def wrapper(request, *args, **kwargs): - session_id = request.GET.get('session_id') - if not is_session_paid(session_id): - messages.error(request, 'No se ha podido verificar el pago.') - return redirect(redirect_to) - log = StripeLog.objects.filter(session_id=session_id).first() - if log is not None and log.fulfilled: - messages.info(request, 'Este pago ya había sido procesado.') - return redirect(redirect_to) - response = view(request, *args, **kwargs) - if log is not None: - log.fulfilled = True - log.save(update_fields=['fulfilled']) - return response - return wrapper - return deco diff --git a/home/views/account.py b/home/views/account.py deleted file mode 100644 index 0bfbf72..0000000 --- a/home/views/account.py +++ /dev/null @@ -1,268 +0,0 @@ -from ._base import * - -dias_semana = ['Lunes', 'Martes', 'Miércoles', 'Jueves', 'Viernes', 'Sábado', 'Domingo'] -meses = ['Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre'] - - -def get_class_css(class_id): - class_map = { - 1: 'warrior', - 2: 'paladin', - 3: 'hunter', - 4: 'rogue', - 5: 'priest', - 6: 'death-knight', - 7: 'shaman', - 8: 'mage', - 9: 'warlock', - 11: 'druid' - } - return class_map.get(class_id, 'unknown') -def get_class_text_css(class_id): - text_class_map = { - 1: 'warrior', - 2: 'paladin', - 3: 'hunter', - 4: 'rogue', - 5: 'priest', - 6: 'death-knight', - 7: 'shaman', - 8: 'mage', - 9: 'warlock', - 11: 'druid' - } - return text_class_map.get(class_id, 'unknown') -def formatear_fecha(fecha): - """Formatea la fecha al estilo solicitado.""" - dia_semana = dias_semana[fecha.weekday()] - mes = meses[fecha.month - 1] - return f"{dia_semana} {fecha.day:02d} de {mes} del {fecha.year}" -def my_account(request): - # Verificar si la sesión contiene la cuenta de juego seleccionada - username = request.session.get('username') - - # Si aún no se ha elegido cuenta de juego pero hay login Battle.net, - # enviar al selector; si no, al inicio de sesión. - if not username: - if request.session.get('bnet_id'): - return redirect('select-account') - return redirect('login') - - is_logged_in = True # Si el usuario está en la sesión, asumimos que está conectado - - # Obtener información básica de la cuenta - with connections['acore_auth'].cursor() as cursor: - cursor.execute(""" - SELECT id, username, reg_mail, email, last_ip, last_attempt_ip, joindate - FROM account - WHERE username = %s - """, [username]) - account_data = cursor.fetchone() - - if not account_data: - return redirect('index') - - account_id = account_data[0] - - # Obtener los personajes asociados a la cuenta - characters = get_account_characters(account_id) - has_characters = bool(characters) - - # Obtener el estado de la cuenta - account_status = get_account_status(account_id) - - # Obtener los puntos (DP y VP) del usuario desde la tabla `home_api_points` - user_points = HomeApiPoints.objects.filter(accountID=account_id).first() - dp = user_points.dp if user_points else 0 - vp = user_points.vp if user_points else 0 - - - # Obtener el estado del token de seguridad - security_token = SecurityToken.objects.filter(user_id=account_id).first() - if security_token: - token_status = "Solicitado" - token_date = security_token.created_at.strftime('%H:%M:%S %d-%m-%Y') - else: - token_status = "Sin solicitar" - token_date = None - - # Créditos Battlepay de la cuenta Battle.net - with connections['acore_auth'].cursor() as cursor: - battlepay_credits = bnet.get_battlepay_credits(cursor, request.session.get('bnet_id')) - - return render(request, 'my-account/my-account.html', { - 'is_logged_in': is_logged_in, - 'battlepay_credits': battlepay_credits, - 'user_info': { - 'username': account_data[1], - 'reg_mail': account_data[2], - 'email': account_data[3], - 'last_ip': account_data[4], - 'last_attempt_ip': account_data[5], - 'joindate': formatear_fecha(account_data[6]), - - }, - 'account_status': account_status, - 'characters': characters, - 'has_characters': has_characters, - 'dp': dp, - 'vp': vp, - 'token_status': token_status, - 'token_date': token_date - }) -def get_account_status(account_id): - with connections['acore_auth'].cursor() as cursor: - # Obtener información de la tabla 'account' - cursor.execute(""" - SELECT username, reg_mail, email, last_ip, last_attempt_ip, joindate, recruiter - FROM account - WHERE id = %s - """, [account_id]) - account_data = cursor.fetchone() - - if not account_data: - return None - - # Verificar si la cuenta está baneada - cursor.execute(""" - SELECT unbandate, UNIX_TIMESTAMP() - FROM account_banned - WHERE id = %s AND active = 1 - """, [account_id]) - ban_data = cursor.fetchone() - - # Obtener el número de amigos reclutados - cursor.execute(""" - SELECT id - FROM account - WHERE recruiter = %s - """, [account_id]) - recruited_accounts = cursor.fetchall() - recruited_count = len(recruited_accounts) - - # Contar cuántos personajes de amigos reclutados han alcanzado el nivel 80 - if recruited_count > 0: - recruited_ids = [acc[0] for acc in recruited_accounts] - with connections['acore_characters'].cursor() as char_cursor: - char_cursor.execute(""" - SELECT COUNT(*) - FROM characters - WHERE account IN %s AND level = 80 - """, [tuple(recruited_ids)]) - level_80_count = char_cursor.fetchone()[0] - else: - level_80_count = 0 - - # Asignar los datos a variables - user_info = { - 'username': account_data[0] if account_data[0] else 'No disponible', - 'reg_mail': account_data[1] if account_data[1] else 'No disponible', - 'email': account_data[2] if account_data[2] else 'No disponible', - 'last_ip': account_data[3] if account_data[3] else 'Nunca', - 'last_attempt_ip': account_data[4] if account_data[4] else 'Nunca', - 'joindate': account_data[5].strftime("%A %d de %B del %Y a las %H:%M:%S Horas") if account_data[5] else 'No disponible', - 'is_recruited': bool(account_data[6]), - 'recruited_count': recruited_count, - 'recruited_level_80_count': level_80_count - } - - # Información sobre la suspensión, si existe - if ban_data: - unban_timestamp, current_timestamp = ban_data - remaining_time = unban_timestamp - current_timestamp - user_info['is_banned'] = True - user_info['unban_date'] = datetime.fromtimestamp(unban_timestamp).strftime("%A %d de %B del %Y a las %H:%M:%S Horas") - user_info['remaining_time'] = remaining_time - else: - user_info['is_banned'] = False - - return user_info -def get_character_image(class_id, race, gender): - """ - Devuelve la imagen del personaje basada en la raza, clase y género. - `gender`: 0 = Hombre, 1 = Mujer - """ - # Definir los nombres de archivos según el género - gender_suffix = 'male' if gender == 0 else 'female' - - # Raza y clases compatibles - race_class_map = { - 10: [2, 3, 4, 5, 6, 8, 9], # Elfo de Sangre - 8: [1, 3, 4, 5, 6, 7, 8], # Trol - 6: [1, 3, 6, 7, 11], # Tauren - 5: [1, 4, 5, 6, 8, 9], # No-Muerto - 2: [1, 3, 4, 6, 7, 9], # Orco - 11: [1, 2, 3, 5, 6, 7, 8], # Draenei - 7: [1, 4, 6, 8, 9], # Gnomo - 4: [1, 3, 4, 5, 6, 11], # Elfo de la Noche - 3: [1, 2, 3, 4, 5, 6], # Enano - 1: [1, 2, 4, 5, 6, 8, 9] # Humano - } - - # Verificar si la clase es compatible con la raza - if class_id not in race_class_map.get(race, []): - return 'nw-themes/nw-ryu/nw-images/nw-classes/unknown.webp' # Imagen por defecto si no es compatible - - # Construir el nombre del archivo basado en la raza y el género - race_image_map = { - 10: f"big-blood-elf-{gender_suffix}.webp", - 8: f"big-troll-{gender_suffix}.webp", - 6: f"big-tauren-{gender_suffix}.webp", - 5: f"big-undead-{gender_suffix}.webp", - 2: f"big-orc-{gender_suffix}.webp", - 11: f"big-draenei-{gender_suffix}.webp", - 7: f"big-gnome-{gender_suffix}.webp", - 4: f"big-night-elf-{gender_suffix}.webp", - 3: f"big-dwarf-{gender_suffix}.webp", - 1: f"big-human-{gender_suffix}.webp" - } - - # Devolver la imagen según la raza y género, o una imagen por defecto - return f"nw-themes/nw-ryu/nw-images/nw-races/{race_image_map.get(race, 'unknown.webp')}" -def get_account_characters(account_id): - """Obtiene los personajes de la cuenta dada.""" - with connections['acore_characters'].cursor() as cursor: - cursor.execute(""" - SELECT name, level, money, race, class, zone, gender - FROM characters - WHERE account = %s - """, [account_id]) - characters = cursor.fetchall() - - character_list = [] - for char in characters: - name, level, money, race, class_id, zone, gender = char - gold = money // 10000 - silver = (money % 10000) // 100 - copper = money % 100 - - # Obtener la clase CSS y la imagen antes de pasarla a la plantilla - class_css = get_class_css(class_id) - image_url = get_character_image(class_id, race, gender) - - # Obtener el nombre de la zona usando la función get_zone_name - zone_name = get_zone_name(zone) - - character_list.append({ - 'name': name, - 'level': level, - 'gold': gold, - 'silver': silver, - 'copper': copper, - 'race': race, - 'class_id': class_id, - 'zone': zone_name, - 'class_css': class_css, - 'image_url': image_url - }) - return character_list -def get_account_id(username): - # Consulta para obtener el account_id a partir del username - with connections['acore_auth'].cursor() as cursor: - cursor.execute(""" - SELECT id - FROM account - WHERE username = %s - """, [username]) - result = cursor.fetchone() - return result[0] if result else None diff --git a/home/views/api.py b/home/views/api.py deleted file mode 100644 index 19088fd..0000000 --- a/home/views/api.py +++ /dev/null @@ -1,109 +0,0 @@ -from ._base import * -from .pages import check_server_status, get_online_characters_count -from .auth import _set_game_account_session -from .account import get_account_characters, get_account_status, formatear_fecha - -# Endpoints JSON que alimentan las islas React/TSX del frontend (Vite). -# Se montan bajo /api/ (fuera de i18n_patterns). De momento con JsonResponse; -# si crece la superficie de la API se puede introducir DRF. - - -def home_news_api(request): - """Últimas noticias para la isla de la portada.""" - noticias = Noticia.objects.order_by('-fecha_publicacion')[:50] - data = [ - { - "id": n.id, - "titulo": n.titulo, - "fecha": n.fecha_publicacion.isoformat() if n.fecha_publicacion else None, - "contenido": n.contenido, - "enlace": n.enlace or None, - } - for n in noticias - ] - return JsonResponse({"noticias": data}) - - -def home_status_api(request): - """Estado del servidor para la isla de la portada (nombre, online, login, dirección).""" - server = ServerSelection.objects.first() - online = get_online_characters_count() - status = "Online" if (server and check_server_status(server.address, server.port)) else "Offline" - expansion = server.expansion if server else "Unknown" - return JsonResponse( - { - "server_name": server.name if server else None, - "address": server.address if server else None, - "expansion": expansion, - "online_characters": online, - "status": status, - } - ) - - -def account_select_api(request): - """Selecciona la cuenta de juego de la bnet en sesión (isla del selector). - - Vía GET (sin POST): es idempotente y solo permite elegir cuentas ligadas a la - propia cuenta Battle.net de la sesión, así que no expone nada de otros usuarios. - """ - bnet_id = request.session.get('bnet_id') - if not bnet_id: - return JsonResponse({"success": False, "error": "No autenticado"}, status=401) - try: - selected_id = int(request.GET.get("account_id", "")) - except (TypeError, ValueError): - return JsonResponse({"success": False, "error": "Cuenta no válida"}) - with connections['acore_auth'].cursor() as cursor: - accounts = bnet.get_game_accounts_for_bnet(cursor, bnet_id) - chosen = next((g for g in accounts if g["id"] == selected_id), None) - if not chosen: - return JsonResponse({"success": False, "error": "Cuenta de juego no válida."}) - _set_game_account_session(request, chosen) - return JsonResponse({"success": True, "redirect": reverse("my-account")}) - - -def account_me_api(request): - """Datos del panel «Mi cuenta» (isla): info de cuenta, estado, puntos, - créditos Battlepay, token de seguridad y personajes. Requiere sesión.""" - username = request.session.get('username') - if not username: - return JsonResponse({"authenticated": False}, status=401) - try: - with connections['acore_auth'].cursor() as cursor: - cursor.execute( - "SELECT id, username, reg_mail, email, last_ip, last_attempt_ip, joindate " - "FROM account WHERE username = %s", [username]) - row = cursor.fetchone() - if not row: - return JsonResponse({"authenticated": False}, status=404) - account_id = row[0] - characters = get_account_characters(account_id) - status = get_account_status(account_id) - points = HomeApiPoints.objects.filter(accountID=account_id).first() - sectoken = SecurityToken.objects.filter(user_id=account_id).first() - with connections['acore_auth'].cursor() as cursor: - credits = bnet.get_battlepay_credits(cursor, request.session.get('bnet_id')) - except Exception as e: - logger.error("account_me_api: %s", e) - return JsonResponse({"authenticated": True, "error": "No se pudieron cargar los datos de la cuenta."}, status=200) - - return JsonResponse({ - "authenticated": True, - "user_info": { - "bnet_email": request.session.get('bnet_email') or row[3], - "username": row[1], - "reg_mail": row[2], - "email": row[3], - "last_ip": row[4], - "last_attempt_ip": row[5], - "joindate": formatear_fecha(row[6]), - }, - "account_status": status, - "dp": points.dp if points else 0, - "vp": points.vp if points else 0, - "battlepay_credits": credits, - "token_status": "Solicitado" if sectoken else "Sin solicitar", - "token_date": sectoken.created_at.strftime('%H:%M:%S %d-%m-%Y') if sectoken else None, - "characters": characters, - }) diff --git a/home/views/auth.py b/home/views/auth.py deleted file mode 100644 index e18a863..0000000 --- a/home/views/auth.py +++ /dev/null @@ -1,719 +0,0 @@ -from ._base import * - - -class LoginForm(forms.Form): - email = forms.EmailField(max_length=320, required=True) - password = forms.CharField(widget=forms.PasswordInput, required=True) -def sha1(data): - """Función auxiliar para calcular SHA-1.""" - return hashlib.sha1(data).digest() -def calculate_srp6_verifier(username, password, salt): - """Calcula el verifier usando SRP6 basado en la implementación PHP.""" - g = gmpy2.mpz(7) - N = gmpy2.mpz('894B645E89E1535BBDAD5B8B290650530801B18EBFBF5E8FAB3C82872A3E9BB7', 16) - - # Paso 1: Calcular H(username:password) - h1 = sha1((username.upper() + ':' + password.upper()).encode('utf-8')) - - # Usar el salt proporcionado (sin invertir) - salt_bytes = bytes.fromhex(salt) - - # Paso 2: Calcular H(salt + H(username:password)) - h2 = sha1(salt_bytes + h1) - - # Convertir el hash a un entero (little-endian) - h2_int = gmpy2.mpz.from_bytes(h2, 'little') - - # Calcular el verifier usando g^h2 mod N - verifier = gmpy2.powmod(g, h2_int, N) - verifier_bytes = verifier.to_bytes(32, 'little') - - # Convertir el resultado a hexadecimal en mayúsculas - return binascii.hexlify(verifier_bytes).decode('utf-8').upper() -def authenticate(email, password): - """Autentica una cuenta Battle.net por email (SRP6 v2).""" - try: - with connections['acore_auth'].cursor() as cursor: - account = bnet.get_bnet_account_by_email(cursor, email) - if not account: - return None - if bnet.bnet_verify(account['email'], password, account['salt'], account['verifier']): - return account - return None - except Exception: - return None -def _set_game_account_session(request, game_account): - """Fija en sesión la cuenta de juego seleccionada (para las demás vistas).""" - request.session['username'] = game_account['username'].upper() - request.session['account_id'] = game_account['id'] -def login_view(request): - if request.method == 'POST': - if not verify_turnstile(request): - return JsonResponse({'success': False, 'error': 'Verificación anti-bots fallida. Reintenta.'}) - form = LoginForm(request.POST) - if form.is_valid(): - email = form.cleaned_data['email'] - password = form.cleaned_data['password'] - ip_address = request.META.get('REMOTE_ADDR') - - account = authenticate(email, password) - if account: - log_login_attempt(email, "Exito", ip_address) - - # Guardar la identidad Battle.net en la sesión - request.session['bnet_id'] = account['id'] - request.session['bnet_email'] = account['email'] - - # Resolver las cuentas de juego enlazadas - with connections['acore_auth'].cursor() as cursor: - game_accounts = bnet.get_game_accounts_for_bnet(cursor, account['id']) - - if len(game_accounts) == 1: - # Una sola cuenta de juego: seleccionar automáticamente - _set_game_account_session(request, game_accounts[0]) - elif len(game_accounts) > 1: - # Varias: dejar la selección pendiente (my-account redirige al selector) - request.session.pop('username', None) - request.session.pop('account_id', None) - # 0 cuentas: se queda sin username; el selector mostrará el aviso - - return JsonResponse({'success': True}) - else: - log_login_attempt(email, "Fracaso", ip_address) - return JsonResponse({'success': False, 'error': "Correo o contraseña incorrectos"}) - else: - return JsonResponse({'success': False, 'error': "Formulario no valido"}) - - form = LoginForm() - return render(request, 'auth/login.html', {'form': form}) -def select_account_view(request): - """Selector de cuenta de juego para cuentas Battle.net con varias.""" - bnet_id = request.session.get('bnet_id') - if not bnet_id: - return redirect('login') - - with connections['acore_auth'].cursor() as cursor: - game_accounts = bnet.get_game_accounts_for_bnet(cursor, bnet_id) - - # La selección la hace la isla React vía GET (/api/account/select/), sin POST. - - # Autoseleccionar si solo hay una - if len(game_accounts) == 1: - _set_game_account_session(request, game_accounts[0]) - return redirect('my-account') - - return render(request, 'auth/select_account.html', { - 'game_accounts': game_accounts, - 'bnet_email': request.session.get('bnet_email'), - }) -def logout_view(request): - for key in ('username', 'account_id', 'bnet_id', 'bnet_email'): - request.session.pop(key, None) - return redirect('index') -def register_view(request): - if request.method == 'POST': - if not verify_turnstile(request): - return JsonResponse({'success': False, 'message': 'Verificación anti-bots fallida. Reintenta.'}) - password = request.POST.get('password', '').strip() - conf_password = request.POST.get('conf-password', '').strip() - email = request.POST.get('email', '').strip() - conf_email = request.POST.get('conf-email', '').strip() - recruiter = request.POST.get('recruiter', '').strip() - - # En 3.4.3 la identidad es el email (cuenta Battle.net); no hay username. - if not password or not email: - return JsonResponse({'success': False, 'message': 'Por favor, complete todos los campos.'}) - - if password != conf_password: - return JsonResponse({'success': False, 'message': 'Las contraseñas no coinciden.'}) - - if len(password) > 16: - return JsonResponse({'success': False, 'message': 'La contraseña no debe exceder los 16 caracteres.'}) - - if email != conf_email or not re.match(r'^[a-zA-Z0-9._%+-]+@gmail\.com$', email): - return JsonResponse({'success': False, 'message': 'El correo electrónico no es válido.'}) - - # Verificar si ya existe una cuenta Battle.net con ese email - email_norm = bnet.normalize_email(email) - with connections['acore_auth'].cursor() as cursor: - cursor.execute("SELECT COUNT(*) FROM battlenet_accounts WHERE email = %s", [email_norm]) - if cursor.fetchone()[0] > 0: - return JsonResponse({'success': False, 'message': 'Ya existe una cuenta con ese correo electrónico.'}) - - # Comprobar que no haya un registro de activación pendiente para ese email - if AccountActivation.objects.filter(email=email, old_email__isnull=True).exists(): - AccountActivation.objects.filter(email=email, old_email__isnull=True).delete() - - # Validar y obtener recruiter_id (id de cuenta de juego del reclutador) - recruiter_id = 0 - if recruiter: - with connections['acore_auth'].cursor() as cursor: - cursor.execute("SELECT id FROM account WHERE username = %s", [recruiter]) - recruiter_data = cursor.fetchone() - if recruiter_data: - recruiter_id = recruiter_data[0] - else: - with connections['acore_characters'].cursor() as char_cursor: - char_cursor.execute("SELECT account FROM characters WHERE name = %s", [recruiter]) - character_data = char_cursor.fetchone() - if character_data: - recruiter_id = character_data[0] - else: - return JsonResponse({'success': False, 'message': 'El reclutador ingresado no existe.'}) - - # Generar un hash único para activación. Los salt/verifier se calculan al - # activar (tanto el de bnet como el de la cuenta de juego). - activation_hash = get_random_string(32) - - AccountActivation.objects.create( - email=email, - password=password, - recruiter_id=recruiter_id, - hash=activation_hash - ) - - # Enviar correo de activación - activation_link = f"{settings.URL_PRINCIPAL}/es/activate-account?act={activation_hash}" - context = { - 'username': email, - 'password': password, - 'activation_link': activation_link, - 'NOMBRE_SERVIDOR': settings.NOMBRE_SERVIDOR - } - enviar_correo( - subject=f'Activación de la cuenta {email} - {settings.NOMBRE_SERVIDOR}', - to_email=email, - template='emails/activation.html', - context=context - ) - - message = f""" -
    - La cuenta '{email}' ha sido creada.

    - Se ha enviado un enlace de activación al correo {email}. -
    - """ - return JsonResponse({'success': True, 'message': message}) - - return render(request, 'auth/register.html') -def activate_account_view(request): - activation_hash = request.GET.get('act') - - try: - activation = AccountActivation.objects.get(hash=activation_hash) - - if activation.is_expired(): - return render(request, 'auth/activation_invalid.html') - - email_norm = bnet.normalize_email(activation.email) - password = activation.password - last_ip = request.META.get('REMOTE_ADDR') - - with connections['acore_auth'].cursor() as cursor: - # Evitar duplicados si el enlace se usa dos veces - cursor.execute("SELECT id FROM battlenet_accounts WHERE email = %s", [email_norm]) - existing = cursor.fetchone() - if existing: - activation.delete() - return render(request, 'auth/activation_success.html') - - # 1) Crear la cuenta Battle.net (SRP6 v2) - bnet_salt, bnet_verifier, srp_version = bnet.bnet_make_registration(email_norm, password) - cursor.execute( - "INSERT INTO battlenet_accounts (email, srp_version, salt, verifier) " - "VALUES (%s, %s, %s, %s)", - [email_norm, srp_version, bnet_salt, bnet_verifier] - ) - - # 2) Recuperar el id de la cuenta bnet recién creada - cursor.execute("SELECT id FROM battlenet_accounts WHERE email = %s", [email_norm]) - bnet_id = cursor.fetchone()[0] - - # 3) Crear la cuenta de juego enlazada (#1, SRP6 Grunt) - game_username = bnet.make_game_account_username(bnet_id, 1) - game_salt, game_verifier = bnet.game_make_registration(game_username, password) - cursor.execute( - "INSERT INTO account " - "(username, salt, verifier, email, reg_mail, recruiter, joindate, " - " last_ip, expansion, battlenet_account, battlenet_index) " - "VALUES (%s, %s, %s, %s, %s, %s, NOW(), %s, %s, %s, %s)", - [ - game_username, game_salt, game_verifier, - email_norm, email_norm, activation.recruiter_id or 0, - last_ip, 2, bnet_id, 1 - ] - ) - - activation.delete() - return render(request, 'auth/activation_success.html') - - except AccountActivation.DoesNotExist: - return render(request, 'auth/activation_invalid.html') -def recover_account_view(request): - """Recuperación de cuenta (Battle.net, por email). 3 tipos: - password (enlace de reset), accountname (lista de cuentas), activation (reenvío). - Respuestas genéricas para no revelar si el correo existe (anti-enumeración). - """ - if request.method == 'POST': - if not verify_turnstile(request): - return JsonResponse({'success': False, 'message': 'Verificación anti-bots fallida. Reintenta.'}) - rtype = request.POST.get('type', '').strip() - email = request.POST.get('email', '').strip() - if not email or not re.match(r'^[a-zA-Z0-9._%+-]+@gmail\.com$', email): - return JsonResponse({'success': False, 'message': 'Introduce un correo de Gmail válido.'}) - - if rtype == 'password': - try: - with connections['acore_auth'].cursor() as cursor: - account = bnet.get_bnet_account_by_email(cursor, email) - except Exception as e: - logger.warning("recover(password): BD de cuentas no disponible: %s", e) - account = None - if account: - token = get_random_string(64) - PasswordReset.objects.create(email=email, token=token) - reset_link = f"{settings.URL_PRINCIPAL}/es/reset-password?token={token}" - enviar_correo( - subject=f'Restablecer contraseña - {settings.NOMBRE_SERVIDOR}', - to_email=email, - template='emails/password_reset.html', - context={'reset_link': reset_link, 'email': email, - 'NOMBRE_SERVIDOR': settings.NOMBRE_SERVIDOR, - 'URL_PRINCIPAL': settings.URL_PRINCIPAL}, - ) - return JsonResponse({'success': True, 'message': 'Si existe una cuenta con ese correo, te hemos enviado un enlace para restablecer la contraseña.'}) - - if rtype == 'accountname': - try: - with connections['acore_auth'].cursor() as cursor: - account = bnet.get_bnet_account_by_email(cursor, email) - game_accounts = bnet.get_game_accounts_for_bnet(cursor, account['id']) if account else [] - except Exception as e: - logger.warning("recover(accountname): BD de cuentas no disponible: %s", e) - account, game_accounts = None, [] - if account: - enviar_correo( - subject=f'Tus cuentas de juego - {settings.NOMBRE_SERVIDOR}', - to_email=email, - template='emails/account_names.html', - context={'email': email, 'accounts': game_accounts, - 'NOMBRE_SERVIDOR': settings.NOMBRE_SERVIDOR, - 'URL_PRINCIPAL': settings.URL_PRINCIPAL}, - ) - return JsonResponse({'success': True, 'message': 'Si existe una cuenta con ese correo, te hemos enviado la lista de cuentas asociadas.'}) - - if rtype == 'activation': - activation = AccountActivation.objects.filter( - email=email, old_email__isnull=True).order_by('-created_at').first() - if activation: - activation_link = f"{settings.URL_PRINCIPAL}/es/activate-account?act={activation.hash}" - enviar_correo( - subject=f'Activación de la cuenta {email} - {settings.NOMBRE_SERVIDOR}', - to_email=email, - template='emails/activation.html', - context={'username': email, 'password': activation.password, - 'activation_link': activation_link, - 'NOMBRE_SERVIDOR': settings.NOMBRE_SERVIDOR}, - ) - return JsonResponse({'success': True, 'message': 'Si hay una activación pendiente para ese correo, te hemos reenviado el enlace.'}) - - return JsonResponse({'success': False, 'message': 'Opción no válida.'}) - - return render(request, 'auth/recover_account.html') - - -def reset_password_view(request): - """Fija una nueva contraseña a partir de un token de PasswordReset (por email). - Re-deriva el verifier SRP6 v2 de la cuenta Battle.net (igual que change_password).""" - if request.method == 'POST': - token = request.POST.get('token', '').strip() - new_password = request.POST.get('new-password', '').strip() - conf_password = request.POST.get('conf-password', '').strip() - - pr = PasswordReset.objects.filter(token=token, used=False).first() - if not pr or pr.is_expired(): - return JsonResponse({'success': False, 'message': 'El enlace no es válido o ha caducado.'}) - if not new_password or new_password != conf_password: - return JsonResponse({'success': False, 'message': 'Las contraseñas no coinciden.'}) - if len(new_password) > 16: - return JsonResponse({'success': False, 'message': 'La contraseña no debe exceder los 16 caracteres.'}) - - new_salt, new_verifier, srp_version = bnet.bnet_make_registration(pr.email, new_password) - try: - with connections['acore_auth'].cursor() as cursor: - account = bnet.get_bnet_account_by_email(cursor, pr.email) - if not account: - return JsonResponse({'success': False, 'message': 'No se ha encontrado la cuenta.'}) - cursor.execute( - "UPDATE battlenet_accounts SET srp_version = %s, salt = %s, verifier = %s WHERE id = %s", - [srp_version, new_salt, new_verifier, account['id']] - ) - except Exception as e: - logger.error("reset_password: error al actualizar la cuenta: %s", e) - return JsonResponse({'success': False, 'message': 'No se ha podido actualizar la contraseña. Inténtalo más tarde.'}) - pr.used = True - pr.save(update_fields=['used']) - return JsonResponse({'success': True, - 'message': 'Contraseña restablecida. Ya puedes iniciar sesión.', - 'redirect': reverse('login')}) - - token = request.GET.get('token', '').strip() - pr = PasswordReset.objects.filter(token=token, used=False).first() - valid = bool(pr and not pr.is_expired()) - return render(request, 'auth/reset_password.html', {'token': token, 'valid': valid}) -def change_password_view(request): - # Verificar si el usuario está autenticado mediante la sesión - username = request.session.get('username') - if not username: - return redirect('login') - - # Obtener el usuario desde la base de datos de `acore_auth` - user_data = get_user_from_acore(username) - if not user_data: - return redirect('login') - - user_id = user_data['id'] - email = user_data['email'] - - if request.method == 'POST': - current_password = request.POST.get('cur-password', '').strip() - new_password = request.POST.get('new-password', '').strip() - conf_new_password = request.POST.get('conf-new-password', '').strip() - security_token = request.POST.get('security-token', '').strip() - - # Validar campos vacíos - if not current_password or not new_password or not conf_new_password or not security_token: - return JsonResponse({'success': False, 'message': 'Por favor, complete todos los campos.'}) - - # Validar que la nueva contraseña coincida con su confirmación - if new_password != conf_new_password: - return JsonResponse({'success': False, 'message': 'Las contraseñas no coinciden.'}) - - # Validar longitud de la nueva contraseña - if len(new_password) > 16: - return JsonResponse({'success': False, 'message': 'La contraseña nueva no debe exceder los 16 caracteres.'}) - - # Verificar si el token de seguridad ha sido generado - existing_token = SecurityToken.objects.filter(user_id=user_id).first() - if not existing_token: - return JsonResponse({'success': False, 'message': 'No tienes un token de seguridad generado. Por favor, genera uno antes de cambiar tu contraseña.'}) - - # Verificar si el token proporcionado es correcto - if existing_token.token != security_token: - return JsonResponse({'success': False, 'message': 'El token ingresado es incorrecto.'}) - - # Verificar la contraseña actual contra la cuenta Battle.net (por email) - if not authenticate(email, current_password): - return JsonResponse({'success': False, 'message': 'La contraseña actual es incorrecta.'}) - - # Generar salt y verifier SRP6 v2 para la nueva contraseña (bnet) - new_salt_bytes, new_verifier_bytes, srp_version = bnet.bnet_make_registration(email, new_password) - - # Localizar la cuenta bnet (por sesión o por email) y actualizarla - bnet_id = request.session.get('bnet_id') - with connections['acore_auth'].cursor() as cursor: - if not bnet_id: - account = bnet.get_bnet_account_by_email(cursor, email) - bnet_id = account['id'] if account else None - if not bnet_id: - return JsonResponse({'success': False, 'message': 'No se ha encontrado la cuenta.'}) - cursor.execute( - "UPDATE battlenet_accounts SET srp_version = %s, salt = %s, verifier = %s WHERE id = %s", - [srp_version, new_salt_bytes, new_verifier_bytes, bnet_id] - ) - - # Cerrar la sesión por seguridad - for key in ('username', 'account_id', 'bnet_id', 'bnet_email'): - request.session.pop(key, None) - - # Redirigir al usuario a la página de inicio de sesión - return JsonResponse({ - 'success': True, - 'message': 'Contraseña cambiada exitosamente. Has sido desconectado por seguridad.', - 'redirect': True - }) - - return render(request, 'account/change_password.html') -def get_user_from_acore(username): - """ - Obtiene la información del usuario desde la base de datos `acore_auth`. - """ - with connections['acore_auth'].cursor() as cursor: - cursor.execute("SELECT id, email FROM account WHERE username = %s", [username]) - result = cursor.fetchone() - if result: - return {'id': result[0], 'email': result[1]} - return None -def security_token_view(request): - # Verificar si el usuario está autenticado mediante la sesión - username = request.session.get('username') - if not username: - return redirect('index') - - # Obtener el usuario desde la base de datos de `acore_auth` - user_data = get_user_from_acore(username) - if not user_data: - return redirect('index') - - user_id = user_data['id'] - email = user_data['email'] - ip_address = request.META.get('REMOTE_ADDR') - - # Verificar si existe un token activo para mostrar la fecha en la plantilla - existing_token = SecurityToken.objects.filter(user_id=user_id).first() - - # Obtener la fecha del token o "Sin solicitar" si no hay token - token_date = "Sin solicitar" - if existing_token: - token_date = existing_token.created_at.strftime('%H:%M:%S %d-%m-%Y') - - if request.method == 'POST': - # Verificar si el correo del usuario está vacío - if not email: - # Si no tiene correo, devolver un mensaje de error en formato JSON - return JsonResponse({ - 'success': False, - 'message': f'Debes añadir un correo electrónico a tu cuenta antes de solicitar un Token de seguridad.' - }) - # Verificar si ya existe un token y si se puede generar uno nuevo después de 7 días - if existing_token: - # Comprobar si han pasado al menos 7 días desde la creación del token - days_since_creation = (timezone.now() - existing_token.created_at).days - if days_since_creation < 7: - remaining_days = 7 - days_since_creation - return JsonResponse({ - 'success': False, - 'message': f'Sólo puedes solicitar un nuevo Token de seguridad cada 7 días, faltan {remaining_days} días para generar un nuevo Token.' - }) - - # Generar un nuevo token - token = secrets.token_urlsafe(4)[:6] - expires_at = timezone.now() + timedelta(days=7) - - # Eliminar el token anterior y crear uno nuevo - if existing_token: - existing_token.delete() - - # Crear y guardar el nuevo token - new_token = SecurityToken.objects.create( - user_id=user_id, - token=token, - ip_address=ip_address, - expires_at=expires_at # Esta fecha solo se usa para el control de los 7 días - ) - - # Enviar el correo con el token al usuario - send_security_token_email(email, username, token, ip_address) - - # Obtener la nueva fecha del token - new_token_date = new_token.created_at.strftime('%H:%M:%S %d-%m-%Y') - - # Responder con un mensaje exitoso y la nueva fecha - return JsonResponse({ - 'success': True, - 'message': f'Se ha enviado el Token de seguridad al correo {email}.', - 'token_date': new_token_date - }) - - # Pasar `token_date` al contexto de la plantilla - return render(request, 'account/security_token.html', { - 'token_date': token_date - }) -def send_security_token_email(email, username, token, ip_address): - """ - Envía un correo electrónico con el token de seguridad. - """ - context = { - 'username': username, - 'token': token, - 'ip_address': ip_address, - 'NOMBRE_SERVIDOR': settings.NOMBRE_SERVIDOR - } - - # Llamada a tu función personalizada `enviar_correo` - enviar_correo( - subject=f'Token de seguridad de la cuenta {username} - {settings.NOMBRE_SERVIDOR}', - to_email=email, - template='emails/security_token.html', # Asegúrate de que este template exista - context=context - ) -def change_email_view(request): - username = request.session.get('username') - if not username: - return redirect('login') - - # Obtener el usuario desde la base de datos de `acore_auth` - with connections['acore_auth'].cursor() as cursor: - cursor.execute(""" - SELECT id, email, reg_mail - FROM account - WHERE username = %s - """, [username]) - account_data = cursor.fetchone() - - if not account_data: - return redirect('login') - - user_id, current_email, reg_email = account_data - security_token = SecurityToken.objects.filter(user_id=user_id).first() - - if request.method == 'POST': - print("Datos recibidos:", request.POST) - - current_password = request.POST.get('cur-password', '').strip() - current_email_input = request.POST.get('cur-email', '').strip() - new_email = request.POST.get('new-email', '').strip() - conf_new_email = request.POST.get('conf-new-email', '').strip() - token = request.POST.get('security-token', '').strip() - - # Validar los campos - if not all([current_password, current_email_input, new_email, conf_new_email, token]): - return JsonResponse({'success': False, 'message': 'Por favor, complete todos los campos.'}) - - if current_email_input != current_email: - return JsonResponse({'success': False, 'message': 'El correo actual no es correcto.'}) - - if new_email != conf_new_email: - return JsonResponse({'success': False, 'message': 'Los correos no coinciden.'}) - - if not re.match(r'^[a-zA-Z0-9._%+-]+@gmail\.com$', new_email): - return JsonResponse({'success': False, 'message': 'El nuevo correo no es válido.'}) - - if not security_token or security_token.token != token: - return JsonResponse({'success': False, 'message': 'Token de seguridad incorrecto.'}) - - # Verificar la contraseña contra la cuenta Battle.net (por email actual) - if not authenticate(current_email, current_password): - return JsonResponse({'success': False, 'message': 'Contraseña incorrecta.'}) - - # Generar hashes - old_email_hash = secrets.token_urlsafe(16) - new_email_hash = secrets.token_urlsafe(16) - - # Guardar en `AccountActivation`. Se conserva la contraseña porque al - # cambiar el email hay que recalcular el verifier de bnet (el usuario SRP - # depende del email). - AccountActivation.objects.create( - username=username, - email=new_email, - old_email=current_email, - password=current_password, - recruiter_id=user_id, - hash=new_email_hash, - old_email_hash=old_email_hash - ) - - # Enviar confirmación al correo actual - confirm_old_email_link = f"{settings.URL_PRINCIPAL}/es/confirm-old-email?hash={old_email_hash}" - context_old_email = { - 'username': username, - 'confirm_link': confirm_old_email_link, - 'NOMBRE_SERVIDOR': settings.NOMBRE_SERVIDOR - } - enviar_correo( - subject=f'Confirme el cambio de correo para {username}', - to_email=current_email, - template='emails/confirm_old_email.html', - context=context_old_email - ) - - return JsonResponse({'success': True, 'message': 'Se ha enviado un correo de confirmación al correo actual.'}) - - return render(request, 'account/change_email.html') -def confirm_old_email_view(request): - hash = request.GET.get('hash') - activation = AccountActivation.objects.filter(old_email_hash=hash, is_used=False).first() - - if not activation or activation.is_expired(): - return JsonResponse({ - 'success': False, - 'message': 'Enlace no válido o expirado.', - 'redirect_url': reverse('expired-link') - }) - - # Marcar solo el old_email_hash como usado - activation.is_used = True - activation.save() - - # Enviar correo al nuevo correo para confirmar el cambio - activation_link = f"{settings.URL_PRINCIPAL}/es/confirm-new-email?hash={activation.hash}" - context_new_email = { - 'username': activation.username, - 'activation_link': activation_link, - 'NOMBRE_SERVIDOR': settings.NOMBRE_SERVIDOR - } - enviar_correo( - subject=f'Confirme su nuevo correo para {activation.username}', - to_email=activation.email, - template='emails/confirm_new_email.html', - context=context_new_email - ) - - return JsonResponse({'success': True, 'message': 'Correo confirmado. Se ha enviado un enlace al nuevo correo.'}) -def confirm_new_email_view(request): - hash = request.GET.get('hash') - activation = AccountActivation.objects.filter(hash=hash, is_new_email_used=False).first() - - if not activation or activation.is_expired(): - return JsonResponse({ - 'success': False, - 'message': 'Enlace no válido o expirado.', - 'redirect_url': reverse('expired-link') - }) - - # Marcar solo el new_email_hash como usado - activation.is_new_email_used = True - activation.save() - - # Cambiar el email en la cuenta Battle.net implica recalcular salt/verifier, - # porque el usuario SRP = SHA256(UPPER(email)). - new_email_norm = bnet.normalize_email(activation.email) - old_email_norm = bnet.normalize_email(activation.old_email or '') - new_salt, new_verifier, srp_version = bnet.bnet_make_registration(new_email_norm, activation.password) - - with connections['acore_auth'].cursor() as cursor: - # Localizar la cuenta bnet por su email actual (el antiguo) - cursor.execute("SELECT id FROM battlenet_accounts WHERE email = %s", [old_email_norm]) - row = cursor.fetchone() - if not row: - return JsonResponse({ - 'success': False, - 'message': 'No se ha encontrado la cuenta Battle.net.', - }) - bnet_id = row[0] - - # Actualizar email + verifier en battlenet_accounts - cursor.execute( - "UPDATE battlenet_accounts SET email = %s, srp_version = %s, salt = %s, verifier = %s WHERE id = %s", - [new_email_norm, srp_version, new_salt, new_verifier, bnet_id] - ) - # Actualizar el email de las cuentas de juego enlazadas (cosmético) - cursor.execute( - "UPDATE account SET email = %s, reg_mail = %s WHERE battlenet_account = %s", - [new_email_norm, new_email_norm, bnet_id] - ) - - # Enviar notificación al correo anterior - context_old_email = { - 'username': activation.username, - 'new_email': activation.email, - 'NOMBRE_SERVIDOR': settings.NOMBRE_SERVIDOR - } - enviar_correo( - subject=f'Su correo ha sido cambiado en {settings.NOMBRE_SERVIDOR}', - to_email=activation.old_email, - template='emails/old_email_notification.html', - context=context_old_email - ) - - return JsonResponse({'success': True, 'message': 'El cambio de correo ha sido confirmado y completado con éxito.'}) -def expired_link_view(request): - return render(request, 'account/expired_link.html') -def log_login_attempt(username, status, ip_address): - # Registrar intento de inicio de sesión en el historial - LoginAttempt.objects.create( - username=username, - status=status, - ip_address=ip_address, - timestamp=timezone.now() - ) diff --git a/home/views/characters.py b/home/views/characters.py deleted file mode 100644 index e8f956e..0000000 --- a/home/views/characters.py +++ /dev/null @@ -1,1005 +0,0 @@ -from ._base import * - - -def rename_character_view(request): - username = request.session.get('username') - if not username: - return redirect('login') - - # 🔹 Obtener información de la cuenta desde acore_auth - with connections['acore_auth'].cursor() as cursor: - cursor.execute(""" - SELECT id, email, last_ip FROM account WHERE username = %s - """, [username]) - account_info = cursor.fetchone() - - if not account_info: - return JsonResponse({'success': False, 'message': 'Error: Cuenta no encontrada.'}) - - account_id, email, acore_ip = account_info - client_ip = request.META.get('REMOTE_ADDR', '') - - # 🔹 Obtener personajes del usuario desde la base de datos `acore_characters` - with connections['acore_characters'].cursor() as cursor: - cursor.execute(""" - SELECT name FROM characters WHERE account = %s - """, [account_id]) - characters = [row[0] for row in cursor.fetchall()] - - # 🔹 Obtener el precio del renombrado - price_obj = RenamePrice.objects.first() - price = price_obj.price if price_obj else 2.00 - - # 🔹 Ejecutar el comando SOAP para verificar el servidor - command = f".server info" - response = execute_soap_command(command) - - if request.method == 'POST': - character_name = request.POST.get('character') - - if response is None: - return JsonResponse({'success': False, 'message': 'Error de comunicación con el servidor. Intente nuevamente más tarde.'}) - - if not character_name: - return JsonResponse({'success': False, 'message': 'Por favor, seleccione un personaje.'}) - - if character_name not in characters: - return JsonResponse({'success': False, 'message': 'No tienes permiso para renombrar este personaje.'}) - - # 🔹 Crear sesión de pago con Stripe - success_url = request.build_absolute_uri(reverse('rename-success')) - cancel_url = request.build_absolute_uri(reverse('rename-cancel')) - stripe_response = create_checkout_session( - account_id=account_id, - username=username, - email=email, - acore_ip=acore_ip, - amount=price, - character_name=character_name, - success_url=success_url, - cancel_url=cancel_url, - product_name=f"Renombrar personaje: {character_name}" - ) - - if stripe_response["success"]: - request.session['rename_character'] = character_name - return JsonResponse({ - 'success': True, - 'session_id': stripe_response["session_id"], - 'stripe_public_key': settings.STRIPE_PUBLIC_KEY - }) - else: - return JsonResponse({'success': False, 'message': f'Error al iniciar el pago: {stripe_response["error"]}'}) - - return render(request, 'account/rename_character.html', {'characters': characters, 'price': price}) -@require_paid_stripe() -def rename_success_view(request): - # Procesar el comando SOAP tras el pago exitoso - character_name = request.session.get('rename_character') - if not character_name: - return redirect('rename-character') - - command = f".char rename {character_name}" - response = execute_soap_command(command) - - if response is None or response.strip() == "": - message = f"El personaje {character_name} ha sido marcado para renombrar." - request.session.pop('rename_character', None) # Limpiar la sesión - return render(request, 'account/rename_success.html', {'message': message}) - else: - return render(request, 'account/rename_success.html', {'message': f"Error: {response}"}) -def rename_cancel_view(request): - username = request.session.get('username') - if not username: - return redirect('login') - return render(request, 'account/rename_cancel.html') -def restore_character_view(request): - # Verifica si el usuario está autenticado y obtiene su nombre de usuario - username = request.session.get('username') - if not username: - return redirect('login') # Redirige si no hay sesión - - # Obtener la ID de cuenta del usuario conectado a partir del sistema de autenticación personalizada - try: - with connections['acore_auth'].cursor() as cursor: - cursor.execute("SELECT id FROM account WHERE username = %s", [username]) - result = cursor.fetchone() - - if not result: - # Si no se encuentra la cuenta, mostrar un error - messages.error(request, "No se encontró la cuenta asociada a ese usuario.") - return redirect('login') # Redirige al login si no se encuentra la cuenta - - account_id = result[0] # Obtén el ID de la cuenta - except Exception as e: - messages.error(request, f"Ocurrió un error al recuperar la cuenta: {str(e)}") - return redirect('login') - - # Verificar si se ha seleccionado un personaje para restaurar - if request.method == 'POST': - # Obtener el GUID del personaje seleccionado para restaurar - guid = request.POST.get('guid') - - if not guid: - return JsonResponse({'success': False, 'message': 'No se seleccionó un personaje para restaurar.'}) - - # Restaurar vía SOAP y responder en JSON (isla React) - try: - command = f".char del restore {guid}" # Comando SOAP para restaurar el personaje - response = execute_soap_command(command) # Ejecuta el comando SOAP - - if response == "success": - return JsonResponse({ - 'success': True, - 'message': 'Personaje restaurado exitosamente.', - 'redirect': reverse('my-account'), - }) - else: - return JsonResponse({'success': False, 'message': 'Hubo un error al restaurar el personaje.'}) - except Exception as e: - return JsonResponse({'success': False, 'message': f'Hubo un error al intentar restaurar el personaje: {str(e)}'}) - - # Realizar la consulta para obtener los personajes eliminados de la cuenta - with connections['acore_characters'].cursor() as cursor: - cursor.execute(""" - SELECT guid, level, deleteInfos_Name, deleteInfos_Account, deleteDate - FROM characters - WHERE deleteInfos_Account = %s AND deleteDate IS NOT NULL - """, [account_id]) # Filtra por account_id - - # Obtener los resultados de la consulta - deleted_characters = cursor.fetchall() - - # Si no hay personajes eliminados - if not deleted_characters: - return render(request, 'account/restore_character.html', { - 'message': 'No hay personajes eliminados que puedas recuperar.' - }) - - # Crear un listado de personajes eliminados con su nombre y nivel (si lo tienes) - character_list = [] - for character in deleted_characters: - guid, level, name, account_id, delete_date = character - # Agregar la información del personaje al listado - character_list.append({ - 'name': name, - 'guid': guid, - 'level': level, - 'delete_date': delete_date - }) - - # Renderiza la plantilla con los personajes eliminados - return render(request, 'account/restore_character.html', { - 'character_list': character_list, - 'character_data': [ - {'guid': c['guid'], 'name': c['name'], 'level': c['level']} - for c in character_list - ], - }) -def restore_items_view(request): - username = request.session.get('username') - if not username: - return redirect('login') - - characters = [] - deleted_items = [] - selected_character = None - - try: - # Obtener la ID de cuenta del usuario - with connections['acore_auth'].cursor() as cursor: - cursor.execute("SELECT id FROM account WHERE username = %s", [username]) - result = cursor.fetchone() - if result: - account_id = result[0] - else: - messages.error(request, "No se encontró la cuenta asociada.") - return redirect('login') - - # Obtener los personajes del usuario - with connections['acore_characters'].cursor() as cursor: - cursor.execute("SELECT name FROM characters WHERE account = %s", [account_id]) - characters = [row[0] for row in cursor.fetchall()] - - except Exception as e: - messages.error(request, f"Error al obtener los personajes: {str(e)}") - - # La isla React usa AJAX (X-Requested-With); en ese caso respondemos JSON. - is_ajax = request.headers.get('X-Requested-With') == 'XMLHttpRequest' - - if request.method == "GET" and "character_name" in request.GET: - selected_character = request.GET.get("character_name") - - if selected_character: - try: - # Ejecutar comando SOAP para obtener ítems eliminados del personaje seleccionado - command = f".item restore list {selected_character}" - response = execute_soap_command(command) - - # Procesar la respuesta del servidor - for line in response.split("\n"): - match = re.search(r"Recover id: (\d+) \| Item: (.+) \((\d+)\) \| Count: (\d+)", line) - if match: - recover_id = match.group(1) - item_name = match.group(2) - item_id = match.group(3) - count = match.group(4) - deleted_items.append({ - "recover_id": recover_id, - "item_name": item_name, - "item_id": item_id, - "count": count - }) - - except Exception as e: - if is_ajax: - return JsonResponse({'error': f'Error al obtener los ítems eliminados: {str(e)}'}) - messages.error(request, f"Error al obtener los ítems eliminados: {str(e)}") - - if is_ajax: - return JsonResponse({'items': deleted_items}) - - if request.method == "POST": - recover_id = request.POST.get("recover_id") - selected_character = request.POST.get("character_name") - - if not recover_id or not selected_character: - if is_ajax: - return JsonResponse({'success': False, 'message': 'Selecciona un personaje y un ítem para restaurar.'}) - messages.error(request, "Selecciona un personaje y un ítem para restaurar.") - return redirect('restore-items') - - try: - # Ejecutar comando SOAP para restaurar el ítem - command = f".item restore {recover_id} {selected_character}" - response = execute_soap_command(command) - ok = "success" in response.lower() - except Exception as e: - if is_ajax: - return JsonResponse({'success': False, 'message': f'Error al restaurar el ítem: {str(e)}'}) - messages.error(request, f"Error al restaurar el ítem: {str(e)}") - return redirect('restore-items') - - if is_ajax: - msg = ('Ítem restaurado exitosamente.' if ok - else 'No se pudo restaurar el ítem.') - return JsonResponse({'success': ok, 'message': msg}) - - messages.success(request, "Ítem restaurado exitosamente.") if ok else messages.error(request, "No se pudo restaurar el ítem.") - return redirect('restore-items') - - return render(request, 'account/restore_items.html', { - 'characters': characters, - 'deleted_items': deleted_items, - 'selected_character': selected_character, - 'character_names': characters, - }) -def quest_character_view(request): - username = request.session.get('username') - if not username: - return redirect('login') - return render(request, 'account/quest_character.html') -def send_gift_view(request): - username = request.session.get('username') - if not username: - return redirect('login') - return render(request, 'account/send_gift.html') -def customize_character_view(request): - username = request.session.get('username') - if not username: - return redirect('login') - - # ?? Obtener información de la cuenta desde acore_auth - with connections['acore_auth'].cursor() as cursor: - cursor.execute(""" - SELECT id, email, last_ip FROM account WHERE username = %s - """, [username]) - account_info = cursor.fetchone() - - if not account_info: - return JsonResponse({'success': False, 'message': 'Error: Cuenta no encontrada.'}) - - account_id, email, acore_ip = account_info - client_ip = request.META.get('REMOTE_ADDR', '') - - # ?? Obtener personajes del usuario desde la base de datos `acore_characters` - with connections['acore_characters'].cursor() as cursor: - cursor.execute(""" - SELECT name FROM characters WHERE account = %s - """, [account_id]) - - characters = [row[0] for row in cursor.fetchall()] - - # Obtener el precio de la personalización (si se gestiona desde el admin) - price_obj = CustomizePrice.objects.first() - price = price_obj.price if price_obj else 1.00 - - if request.method == 'POST': - character_name = request.POST.get('character') - - # Validar si el personaje fue seleccionado - if not character_name: - return JsonResponse({'success': False, 'message': 'Por favor, seleccione un personaje.'}) - - # Verificar si el personaje pertenece al usuario - if character_name not in characters: - return JsonResponse({'success': False, 'message': 'No tienes permiso para personalizar este personaje.'}) - - # Crear sesión de pago con Stripe - success_url = request.build_absolute_uri(reverse('customize-success')) - cancel_url = request.build_absolute_uri(reverse('customize-cancel')) - stripe_response = create_checkout_session( - account_id=account_id, - username=username, - email=email, - acore_ip=acore_ip, - amount=price, - character_name=character_name, - success_url=success_url, - cancel_url=cancel_url, - product_name=f"Personalizar personaje: {character_name}" - ) - - - if stripe_response["success"]: - # Guardar temporalmente el nombre del personaje en la sesión - request.session['customize_character'] = character_name - return JsonResponse({ - 'success': True, - 'session_id': stripe_response["session_id"], - 'stripe_public_key': settings.STRIPE_PUBLIC_KEY - }) - else: - return JsonResponse({'success': False, 'message': f'Error al iniciar el pago: {stripe_response["error"]}'}) - - return render(request, 'account/customize_character.html', {'characters': characters, 'price': price}) -@require_paid_stripe() -def customize_success_view(request): - # Procesar el comando SOAP tras el pago exitoso - character_name = request.session.get('customize_character') - if not character_name: - return redirect('customize-character') - - command = f".char customi {character_name}" - response = execute_soap_command(command) - - if response is None or response.strip() == "": - message = f"El personaje {character_name} ha sido marcado para personalización." - request.session.pop('customize_character', None) # Limpiar la sesión - return render(request, 'account/customize_success.html', {'message': message}) - else: - return render(request, 'account/customize_success.html', {'message': f"Error: {response}"}) -def customize_cancel_view(request): - return render(request, 'account/customize_cancel.html') -def change_race_character_view(request): - username = request.session.get('username') - if not username: - return redirect('login') - - # Obtener personajes del usuario desde la base de datos `acore_characters` - with connections['acore_auth'].cursor() as cursor: - cursor.execute(""" - SELECT id, email, last_ip FROM account WHERE username = %s - """, [username]) - account_info = cursor.fetchone() - - if not account_info: - return JsonResponse({'success': False, 'message': 'Error: Cuenta no encontrada.'}) - - account_id, email, acore_ip = account_info - client_ip = request.META.get('REMOTE_ADDR', '') - - # ?? Obtener personajes del usuario desde la base de datos `acore_characters` - with connections['acore_characters'].cursor() as cursor: - cursor.execute(""" - SELECT name FROM characters WHERE account = %s - """, [account_id]) - characters = [row[0] for row in cursor.fetchall()] - - # Obtener el precio del cambio de raza - price_obj = ChangeRacePrice.objects.first() - price = price_obj.price if price_obj else 10.00 # Precio predeterminado si no hay registro - - if request.method == 'POST': - character_name = request.POST.get('character') - - # Validar si el personaje fue seleccionado - if not character_name: - return JsonResponse({'success': False, 'message': 'Por favor, seleccione un personaje.'}) - - # Verificar si el personaje pertenece al usuario - if character_name not in characters: - return JsonResponse({'success': False, 'message': 'No tienes permiso para cambiar la raza de este personaje.'}) - - # Crear sesión de pago con Stripe - success_url = request.build_absolute_uri(reverse('change-race-success')) - cancel_url = request.build_absolute_uri(reverse('change-race-cancel')) - stripe_response = create_checkout_session( - account_id=account_id, - username=username, - email=email, - acore_ip=acore_ip, - amount=price, - character_name=character_name, - success_url=success_url, - cancel_url=cancel_url, - product_name=f"Cambio de Raza del personaje: {character_name}" - ) - - if stripe_response["success"]: - # Guardar el personaje en la sesión para el cambio - request.session['change_race_character'] = character_name - return JsonResponse({ - 'success': True, - 'session_id': stripe_response["session_id"], - 'stripe_public_key': settings.STRIPE_PUBLIC_KEY - }) - else: - return JsonResponse({'success': False, 'message': f'Error al iniciar el pago: {stripe_response["error"]}'}) - - return render(request, 'account/change_race.html', {'characters': characters, 'price': price}) -@require_paid_stripe() -def change_race_success_view(request): - # Procesar el comando SOAP tras el pago exitoso - character_name = request.session.get('change_race_character') - if not character_name: - return redirect('change-race') - - command = f".char changerace {character_name}" - response = execute_soap_command(command) - - if response is None or response.strip() == "": - message = f"El personaje {character_name} ha sido marcado para cambio de raza." - request.session.pop('change_race_character', None) # Limpiar la sesión - return render(request, 'account/change_race_success.html', {'message': message}) - else: - return render(request, 'account/change_race_success.html', {'message': f"Error: {response}"}) -def change_race_cancel_view(request): - return render(request, 'account/change_race_cancel.html') -def change_faction_character_view(request): - username = request.session.get('username') - if not username: - return redirect('login') - - # Obtener personajes del usuario desde la base de datos `acore_characters` - with connections['acore_auth'].cursor() as cursor: - cursor.execute(""" - SELECT id, email, last_ip FROM account WHERE username = %s - """, [username]) - account_info = cursor.fetchone() - - if not account_info: - return JsonResponse({'success': False, 'message': 'Error: Cuenta no encontrada.'}) - - account_id, email, acore_ip = account_info - client_ip = request.META.get('REMOTE_ADDR', '') - - # ?? Obtener personajes del usuario desde la base de datos `acore_characters` - with connections['acore_characters'].cursor() as cursor: - cursor.execute(""" - SELECT name FROM characters WHERE account = %s - """, [account_id]) - characters = [row[0] for row in cursor.fetchall()] - - # Obtener el precio del cambio de facción - price_obj = ChangeFactionPrice.objects.first() - price = price_obj.price if price_obj else 10.00 - - if request.method == 'POST': - character_name = request.POST.get('character') - - # Validar si el personaje fue seleccionado - if not character_name: - return JsonResponse({'success': False, 'message': 'Por favor, seleccione un personaje.'}) - - # Verificar si el personaje pertenece al usuario - if character_name not in characters: - return JsonResponse({'success': False, 'message': 'No tienes permiso para cambiar la facción de este personaje.'}) - - # Crear sesión de pago con Stripe - success_url = request.build_absolute_uri(reverse('change-faction-success')) - cancel_url = request.build_absolute_uri(reverse('change-faction-cancel')) - stripe_response = create_checkout_session( - account_id=account_id, - username=username, - email=email, - acore_ip=acore_ip, - amount=price, - character_name=character_name, - success_url=success_url, - cancel_url=cancel_url, - product_name=f"Cambiar Faccion del personaje: {character_name}" - ) - - if stripe_response["success"]: - # Guardar el personaje seleccionado en la sesión - request.session['change_faction_character'] = character_name - return JsonResponse({ - 'success': True, - 'session_id': stripe_response["session_id"], - 'stripe_public_key': settings.STRIPE_PUBLIC_KEY - }) - else: - return JsonResponse({'success': False, 'message': f'Error al iniciar el pago: {stripe_response["error"]}'}) - - return render(request, 'account/change_faction.html', {'characters': characters, 'price': price}) -@require_paid_stripe() -def change_faction_success_view(request): - # Procesar el comando SOAP tras el pago exitoso - character_name = request.session.get('change_faction_character') - if not character_name: - return redirect('change-faction-character') - - command = f".char changef {character_name}" - response = execute_soap_command(command) - - if response is None or response.strip() == "": - message = f"El personaje {character_name} ha cambiado de facción con éxito." - request.session.pop('change_faction_character', None) # Limpiar la sesión - return render(request, 'account/change_faction_success.html', {'message': message}) - else: - return render(request, 'account/change_faction_success.html', {'message': f"Error: {response}"}) -def change_faction_cancel_view(request): - return render(request, 'account/change_faction_cancel.html') -def level_up_character_view(request): - username = request.session.get('username') - if not username: - return redirect('login') - - # Obtener personajes del usuario desde la base de datos `acore_characters` - with connections['acore_auth'].cursor() as cursor: - cursor.execute(""" - SELECT id, email, last_ip FROM account WHERE username = %s - """, [username]) - account_info = cursor.fetchone() - - if not account_info: - return JsonResponse({'success': False, 'message': 'Error: Cuenta no encontrada.'}) - - account_id, email, acore_ip = account_info - client_ip = request.META.get('REMOTE_ADDR', '') - - # ?? Obtener personajes del usuario desde la base de datos `acore_characters` - with connections['acore_characters'].cursor() as cursor: - cursor.execute(""" - SELECT name FROM characters WHERE account = %s - """, [account_id]) - characters = [row[0] for row in cursor.fetchall()] - - # Obtener el precio de subir a nivel 80 - price_obj = LevelUpPrice.objects.first() - price = price_obj.price if price_obj else 10.00 - - if request.method == 'POST': - character_name = request.POST.get('character') - - # Validar si el personaje fue seleccionado - if not character_name: - return JsonResponse({'success': False, 'message': 'Por favor, seleccione un personaje.'}) - - # Verificar si el personaje pertenece al usuario - if character_name not in characters: - return JsonResponse({'success': False, 'message': 'No tienes permiso para subir de nivel a este personaje.'}) - - # Crear sesión de pago con Stripe - success_url = request.build_absolute_uri(reverse('level-up-success')) - cancel_url = request.build_absolute_uri(reverse('level-up-cancel')) - stripe_response = create_checkout_session( - account_id=account_id, - username=username, - email=email, - acore_ip=acore_ip, - amount=price, - character_name=character_name, - success_url=success_url, - cancel_url=cancel_url, - product_name=f"Subir el Nivel 80 del personaje: {character_name}" - ) - - if stripe_response["success"]: - # Guardar el personaje en la sesión para usarlo después del pago - request.session['levelup_character'] = character_name - return JsonResponse({ - 'success': True, - 'session_id': stripe_response["session_id"], - 'stripe_public_key': settings.STRIPE_PUBLIC_KEY - }) - else: - return JsonResponse({'success': False, 'message': f'Error al iniciar el pago: {stripe_response["error"]}'}) - - return render(request, 'account/level_up.html', {'characters': characters, 'price': price}) -@require_paid_stripe() -def level_up_success_view(request): - character_name = request.session.get('levelup_character') - if not character_name: - return redirect('level-up-character') - - # Ejecutar el comando SOAP - command = f".char level {character_name} 80" - response = execute_soap_command(command) - - if response is None or response.strip() == "": - message = f"El personaje {character_name} ha sido subido al nivel 80." - else: - message = f"Error: {response}" - - # Limpiar la sesión - request.session.pop('levelup_character', None) - - return render(request, 'account/level_up_success.html', {'message': message}) -def level_up_cancel_view(request): - return render(request, 'account/level_up_cancel.html') -def gold_character_view(request): - username = request.session.get('username') - if not username: - return redirect('login') - - # Obtener personajes y estado de conexión del usuario - try: - with connections['acore_auth'].cursor() as cursor: - cursor.execute(""" - SELECT id, email, last_ip FROM account WHERE username = %s - """, [username]) - account_info = cursor.fetchone() - - if not account_info: - return JsonResponse({'success': False, 'message': 'Error: Cuenta no encontrada.'}) - - account_id, email, acore_ip = account_info - client_ip = request.META.get('REMOTE_ADDR', '') - - # Obtener personajes del usuario desde la base de datos `acore_characters` - with connections['acore_characters'].cursor() as cursor: - cursor.execute(""" - SELECT name, online, money FROM characters WHERE account = %s - """, [account_id]) - characters_data = cursor.fetchall() - - characters = {row[0]: {'online': row[1], 'money': row[2]} for row in characters_data} - - except Exception as e: - return JsonResponse({'success': False, 'message': f'Error al obtener los personajes: {str(e)}'}) - - # Obtener precios del oro - gold_prices = GoldPrice.objects.all().order_by('gold_amount') - - if request.method == 'POST': - character_name = request.POST.get('character') - gold_amount = request.POST.get('gold_amount') - - # Validaciones iniciales - if not character_name or not gold_amount: - return JsonResponse({'success': False, 'message': 'Seleccione un personaje y una cantidad de oro.'}) - - if character_name not in characters: - return JsonResponse({'success': False, 'message': 'No tienes permiso para enviar oro a este personaje.'}) - - if characters[character_name]['online'] == 1: - return JsonResponse({'success': False, 'message': 'El personaje debe estar desconectado para recibir oro.'}) - - try: - gold_amount = int(gold_amount) - except ValueError: - return JsonResponse({'success': False, 'message': 'Cantidad de oro no válida.'}) - - gold_price = GoldPrice.objects.filter(gold_amount=gold_amount).first() - if not gold_price: - return JsonResponse({'success': False, 'message': 'Cantidad de oro no válida.'}) - - # El precio va en euros (con decimales); el helper lo pasa a céntimos (×100) - stripe_price = gold_price.price - - # Crear sesión de pago con Stripe - success_url = request.build_absolute_uri(reverse('gold-success')) - cancel_url = request.build_absolute_uri(reverse('gold-cancel')) - - # Llamada corregida a create_checkout_session con todos los parámetros requeridos - stripe_response = create_checkout_session( - account_id=account_id, - username=username, - email=email, - acore_ip=acore_ip, - amount=stripe_price, # El precio calculado - character_name=character_name, - success_url=success_url, - cancel_url=cancel_url, - product_name=f"Aumentar el Oro: {gold_amount} al Personaje: {character_name}" - ) - - if stripe_response["success"]: - # Guardar en sesión los datos para el callback tras el pago - request.session['gold_transaction'] = { - 'character_name': character_name, - 'gold_amount': gold_amount - } - return JsonResponse({ - 'success': True, - 'session_id': stripe_response["session_id"], - 'stripe_public_key': settings.STRIPE_PUBLIC_KEY - }) - else: - return JsonResponse({'success': False, 'message': f'Error al iniciar el pago: {stripe_response["error"]}'}) - - return render(request, 'account/gold_character.html', { - 'characters': characters, - 'gold_prices': gold_prices, - 'character_names': list(characters.keys()), - 'gold_options': [{'gold_amount': p.gold_amount, 'price': str(p.price)} for p in gold_prices], - }) -@require_paid_stripe() -def gold_success_view(request): - # Procesar el pago exitoso - transaction = request.session.pop('gold_transaction', None) - if not transaction: - return redirect('gold-character') - - character_name = transaction['character_name'] - gold_amount = int(transaction['gold_amount']) - - # Calcular el oro en formato del juego - game_gold_amount = gold_amount * 10000 - - # Actualizar la cantidad de oro en la base de datos - with connections['acore_characters'].cursor() as cursor: - cursor.execute(""" - UPDATE characters - SET money = money + %s - WHERE name = %s - """, [game_gold_amount, character_name]) - - return render(request, 'account/gold_success.html', {'character_name': character_name, 'gold_amount': gold_amount}) -def gold_cancel_view(request): - return render(request, 'account/gold_cancel.html') -def transfer_character_view(request): - username = request.session.get('username') - if not username: - return redirect('login') - - # Obtener el precio de transferencia - price_obj = TransferPrice.objects.first() - price = price_obj.price if price_obj else 10.00 - - # Obtener el ID de usuario desde acore_auth.account - with connections['acore_auth'].cursor() as cursor: - cursor.execute("SELECT id, email, last_ip FROM account WHERE username = %s", [username]) - user_data = cursor.fetchone() - if not user_data: - return JsonResponse({'success': False, 'message': 'Cuenta no encontrada. Por favor, inicie sesión nuevamente.'}) - - user_id, email, acore_ip = user_data # ID de la cuenta, email y la última IP - - # Obtener personajes de la cuenta de origen - with connections['acore_characters'].cursor() as cursor: - cursor.execute(""" - SELECT name, class, online - FROM characters - WHERE account = %s - """, [user_id]) - characters = cursor.fetchall() - - if request.method == 'POST': - character_name = request.POST.get('character') - destination_account = request.POST.get('destination_account') - token = request.POST.get('security_token') - - # Validaciones - if not all([character_name, destination_account, token]): - return JsonResponse({'success': False, 'message': 'Por favor, complete todos los campos.'}) - - character = next((char for char in characters if char[0] == character_name), None) - if not character: - return JsonResponse({'success': False, 'message': 'No tienes permiso para transferir este personaje.'}) - - # Verificar si el personaje está desconectado - if character[2] == 1: # Si está conectado - return JsonResponse({'success': False, 'message': 'El personaje debe estar desconectado para ser transferido.'}) - - # Verificar el token de seguridad - security_token = SecurityToken.objects.filter(user_id=user_id, token=token).first() - if not security_token: - return JsonResponse({'success': False, 'message': 'Token de seguridad incorrecto.'}) - - # Verificar si el token ha expirado - if security_token.expires_at < timezone.now(): - return JsonResponse({'success': False, 'message': 'El token ha expirado. Solicite uno nuevo.'}) - - # Verificar si la cuenta de destino existe - with connections['acore_auth'].cursor() as cursor: - cursor.execute("SELECT id FROM account WHERE username = %s", [destination_account]) - destination_account_data = cursor.fetchone() - - if not destination_account_data: - return JsonResponse({'success': False, 'message': 'La cuenta de destino no existe.'}) - - # Verificar restricciones en la cuenta de destino - destination_account_id = destination_account_data[0] - with connections['acore_characters'].cursor() as cursor: - cursor.execute(""" - SELECT account, name, class, level, online - FROM characters - WHERE account = %s - """, [destination_account_id]) - destination_characters = cursor.fetchall() - - if not any(char[3] >= 55 and char[2] != 6 for char in destination_characters): - return JsonResponse({'success': False, 'message': 'La cuenta destino debe tener al menos un personaje de nivel 55 o superior que no sea Caballero de la Muerte.'}) - - if any(char[2] == 6 for char in destination_characters): - return JsonResponse({'success': False, 'message': 'La cuenta destino no puede tener un Caballero de la Muerte.'}) - - # Verificar que ninguna cuenta esté online - for char in destination_characters: - if char[4] == 1: - return JsonResponse({'success': False, 'message': 'Todos los personajes de la cuenta destino deben estar desconectados.'}) - for char in characters: - if char[2] == 1: - return JsonResponse({'success': False, 'message': 'Todos los personajes de la cuenta origen deben estar desconectados.'}) - - # Crear sesión de pago con Stripe - success_url = request.build_absolute_uri(reverse('transfer-success')) - cancel_url = request.build_absolute_uri(reverse('transfer-cancel')) - - # Llamada corregida a create_checkout_session con todos los parámetros requeridos - stripe_response = create_checkout_session( - username=username, - email=email, - acore_ip=acore_ip, - account_id=user_id, # Pasar el account_id - amount=price, # El precio de la transferencia - character_name=character_name, - success_url=success_url, - cancel_url=cancel_url, - product_name=f"Transferir el personaje: {character_name} a la cuenta de destino: {destination_account}" - ) - - if stripe_response["success"]: - # Guardar los datos de la transferencia en la sesión para usarlos después del pago - request.session['transfer_data'] = { - 'character_name': character_name, - 'destination_account': destination_account - } - return JsonResponse({ - 'success': True, - 'session_id': stripe_response["session_id"], - 'stripe_public_key': settings.STRIPE_PUBLIC_KEY - }) - else: - return JsonResponse({'success': False, 'message': f'Error al iniciar el pago: {stripe_response["error"]}'}) - - return render(request, 'account/transfer_character.html', { - 'characters': characters, - 'price': price, - 'character_names': [c[0] for c in characters], - }) -@require_paid_stripe() -def transfer_success_view(request): - transfer_data = request.session.get('transfer_data') - if not transfer_data: - return redirect('transfer-character') - - character_name = transfer_data['character_name'] - destination_account = transfer_data['destination_account'] - - # Ejecutar comando SOAP - command = f".char changeaccount {destination_account} {character_name}" - response = execute_soap_command(command) - - if response is None or response.strip() == "": - message = f"El personaje {character_name} ha sido transferido a la cuenta {destination_account}." - request.session.pop('transfer_data', None) # Limpiar sesión - return render(request, 'account/transfer_success.html', {'message': message}) - else: - return render(request, 'account/transfer_success.html', {'message': f"Error: {response}"}) -def transfer_cancel_view(request): - return render(request, 'account/transfer_cancel.html') -def revive_character_view(request): - username = request.session.get('username') - if not username: - return redirect('login') - - # Obtener personajes del usuario desde la base de datos `acore_characters` - with connections['acore_characters'].cursor() as cursor: - cursor.execute(""" - SELECT name, online - FROM characters - WHERE account = ( - SELECT id - FROM acore_auth.account - WHERE username = %s - ) - """, [username]) - characters_data = cursor.fetchall() - characters = {row[0]: row[1] for row in characters_data} # Diccionario con nombre y estado de conexión - - if request.method == 'POST': - character_name = request.POST.get('character') - - # Validar si el personaje fue seleccionado - if not character_name: - return JsonResponse({'success': False, 'message': 'Por favor, seleccione un personaje.'}) - - # Verificar si el personaje pertenece al usuario - if character_name not in characters: - return JsonResponse({'success': False, 'message': 'No tienes permiso para revivir este personaje.'}) - - # Verificar si el personaje está desconectado - if characters[character_name] == 1: # Si está conectado - return JsonResponse({'success': False, 'message': 'El personaje debe estar desconectado para ser revivido.'}) - - # Verificar el historial de uso del comando `revive` - last_revive = ReviveHistory.objects.filter(character_name=character_name).order_by('-used_at').first() - if last_revive and last_revive.used_at > now() - timedelta(hours=12): - return JsonResponse({'success': False, 'message': 'El personaje solo puede revivirse una vez cada 12 horas.'}) - - # Ejecutar el comando SOAP - command = f".revive {character_name}" - response = execute_soap_command(command) - - # None = fallo de comunicación con el servidor - if response is None: - return JsonResponse({'success': False, 'message': 'Error de comunicación con el servidor. Intente nuevamente más tarde.'}) - - # Respuesta vacía = éxito (el comando .revive no devuelve texto si va bien) - if response.strip() == "": - ReviveHistory.objects.create(character_name=character_name, used_at=now()) - return JsonResponse({'success': True, 'message': f'El personaje {character_name} ha sido revivido con éxito.'}) - - # Respuesta no vacía = el servidor devolvió un mensaje/error - return JsonResponse({'success': False, 'message': f'{response}'}) - - return render(request, 'account/revive_character.html', {'characters': characters.keys()}) -def unstuck_character_view(request): - username = request.session.get('username') - if not username: - return redirect('login') - - # Obtener personajes del usuario desde la base de datos `acore_characters` - with connections['acore_characters'].cursor() as cursor: - cursor.execute(""" - SELECT name, online - FROM characters - WHERE account = ( - SELECT id - FROM acore_auth.account - WHERE username = %s - ) - """, [username]) - characters_data = cursor.fetchall() - characters = {row[0]: row[1] for row in characters_data} # Diccionario con nombre y estado de conexión - - if request.method == 'POST': - character_name = request.POST.get('character') - - # Validar si el personaje fue seleccionado - if not character_name: - return JsonResponse({'success': False, 'message': 'Por favor, seleccione un personaje.'}) - - # Verificar si el personaje pertenece al usuario - if character_name not in characters: - return JsonResponse({'success': False, 'message': 'No tienes permiso para desbloquear este personaje.'}) - - # Verificar si el personaje está desconectado - if characters[character_name] == 1: # Si está conectado - return JsonResponse({'success': False, 'message': 'El personaje debe estar desconectado para ser desbloqueado.'}) - - # Verificar el historial de uso del comando - last_unstuck = UnstuckHistory.objects.filter(character_name=character_name).order_by('-used_at').first() - if last_unstuck and last_unstuck.used_at > now() - timedelta(hours=12): - return JsonResponse({'success': False, 'message': 'El personaje solo puede desbloquearse una vez cada 12 horas.'}) - - # Ejecutar el comando SOAP - command = f".unstuck {character_name} startzone" - response = execute_soap_command(command) - - if response is None: - return JsonResponse({'success': False, 'message': 'Error de comunicación con el servidor. Intente nuevamente más tarde.'}) - - # Analizar la respuesta - if f"You are summoning {character_name} (offline)" in response: - # Registrar el uso del comando - UnstuckHistory.objects.create(character_name=character_name, used_at=now()) - return JsonResponse({'success': True, 'message': f'El personaje {character_name} ha sido desbloqueado con éxito.'}) - else: - return JsonResponse({'success': False, 'message': f'Error: {response}'}) - - return render(request, 'account/unstuck_character.html', {'characters': characters.keys()}) diff --git a/home/views/guild.py b/home/views/guild.py deleted file mode 100644 index 2cab51a..0000000 --- a/home/views/guild.py +++ /dev/null @@ -1,127 +0,0 @@ -from ._base import * -from .auth import authenticate, get_user_from_acore - - -def rename_guild_view(request): - username = request.session.get('username') - if not username: - return redirect('login') - - user_data = get_user_from_acore(username) - if not user_data: - return redirect('login') - - user_id = user_data['id'] - email = user_data['email'] - - security_token = SecurityToken.objects.filter(user_id=user_id).first() - guild_settings, created = GuildRenameSettings.objects.get_or_create(defaults={'cost': 10}) - rename_cost = guild_settings.cost - - with connections['acore_characters'].cursor() as cursor: - cursor.execute(""" - SELECT g.guildid, g.name, gm.rank, c.name - FROM guild g - JOIN guild_member gm ON g.guildid = gm.guildid - JOIN characters c ON c.guid = gm.guid - WHERE c.account = %s AND gm.rank = 0 - """, [user_id]) - guild_leader_data = cursor.fetchall() - - if not guild_leader_data: - return JsonResponse({'success': False, 'message': 'Error: No tienes personajes que sean Maestros de Hermandad en tu cuenta.'}) - - with connections['acore_auth'].cursor() as cursor: - cursor.execute(""" - SELECT id, email, last_ip FROM account WHERE username = %s - """, [username]) - account_info = cursor.fetchone() - - if not account_info: - return JsonResponse({'success': False, 'message': 'Error: Cuenta no encontrada.'}) - - account_id, email, acore_ip = account_info - client_ip = request.META.get('REMOTE_ADDR', '') - - if request.method == 'POST': - old_guild_name = request.POST.get('old-guild-name', '').strip() - new_guild_name = request.POST.get('new-guild-name', '').strip() - conf_new_guild_name = request.POST.get('conf-new-guild-name', '').strip() - current_password = request.POST.get('cur-password', '').strip() - token = request.POST.get('security-token', '').strip() - - if not old_guild_name or not new_guild_name or not conf_new_guild_name or not current_password or not token: - return JsonResponse({'success': False, 'message': 'Por favor, completa todos los campos.'}) - - if not authenticate(email, current_password): - return JsonResponse({'success': False, 'message': 'La contraseña actual es incorrecta.'}) - - if not security_token or security_token.token != token: - return JsonResponse({'success': False, 'message': 'Token de seguridad incorrecto.'}) - - if new_guild_name != conf_new_guild_name: - return JsonResponse({'success': False, 'message': 'Los nombres nuevos no coinciden.'}) - - success_url = request.build_absolute_uri(reverse('guild-success')) - cancel_url = request.build_absolute_uri(reverse('guild-cancel')) - - stripe_response = create_checkout_session( - account_id=user_id, - username=username, - email=email, - acore_ip=acore_ip, - amount=rename_cost, - character_name=old_guild_name, - success_url=success_url, - cancel_url=cancel_url, - product_name=f"Renombrar hermandad: {old_guild_name} → {new_guild_name}" - ) - - if stripe_response["success"]: - request.session['guild_rename'] = { - 'old_name': old_guild_name, - 'new_name': new_guild_name - } - return JsonResponse({ - 'success': True, - 'session_id': stripe_response["session_id"], - 'stripe_public_key': settings.STRIPE_PUBLIC_KEY - }) - else: - return JsonResponse({'success': False, 'message': f'Error al iniciar el pago: {stripe_response["error"]}'}) - - return render(request, 'account/rename_guild.html', { - 'guild_leader_data': guild_leader_data, - 'rename_cost': rename_cost, - 'guild_names': [g[1] for g in guild_leader_data], - }) -@require_paid_stripe() -def guild_rename_success_view(request): - # Obtener los datos de la sesión - guild_rename_data = request.session.get('guild_rename') - if not guild_rename_data: - return redirect('rename-cancel') # Redirigir si no hay datos de renombrado - - old_guild_name = guild_rename_data.get('old_name') - new_guild_name = guild_rename_data.get('new_name') - - if not old_guild_name or not new_guild_name: - return redirect('rename-cancel') # Redirigir si faltan datos - - # Ejecutar el comando SOAP - command = f".guild rename {old_guild_name} {new_guild_name}" - response = execute_soap_command(command) - - # Limpiar la sesión después de la ejecución - request.session.pop('guild_rename', None) - - if response is None or response.strip() == "": - message = f"La Hermandad '{old_guild_name}' ha cambiado su nombre a '{new_guild_name}'. Cierra sesión y vuelve a entrar para que los cambios se reflejen en el juego." - return render(request, 'account/guild_rename_success.html', {'message': message}) - else: - return render(request, 'account/guild_rename_success.html', {'message': f"Error: {response}"}) -def guild_rename_cancel_view(request): - """ - Vista para la página de intercambio de puntos. - """ - return render(request, 'account/guild_rename_cancel.html') diff --git a/home/views/history.py b/home/views/history.py deleted file mode 100644 index a71e6ea..0000000 --- a/home/views/history.py +++ /dev/null @@ -1,97 +0,0 @@ -from ._base import * -from .account import get_account_id - - -def format_datetime(timestamp): - if timestamp: - return datetime.utcfromtimestamp(timestamp).strftime('%d-%m-%Y %H:%M:%S') - return "No disponible" - - -def get_ban_history(account_id): - # Consulta de baneos de la cuenta - with connections['acore_auth'].cursor() as cursor: - cursor.execute(""" - SELECT bandate, unbandate, banreason, active, account.username - FROM account_banned - JOIN account ON account_banned.id = account.id - WHERE account_banned.id = %s - """, [account_id]) - account_bans = cursor.fetchall() - - # Formatear las fechas - account_bans = [(format_datetime(ban[0]), format_datetime(ban[1]), ban[2], ban[3], ban[4]) for ban in account_bans] - - # Consulta de baneos de los personajes asociados a la cuenta - with connections['acore_characters'].cursor() as cursor: - cursor.execute(""" - SELECT bandate, unbandate, banreason, active, characters.name - FROM character_banned - JOIN characters ON character_banned.guid = characters.guid - WHERE characters.account = %s - """, [account_id]) - character_bans = cursor.fetchall() - - # Formatear las fechas - character_bans = [(format_datetime(ban[0]), format_datetime(ban[1]), ban[2], ban[3], ban[4]) for ban in character_bans] - - return account_bans, character_bans -def calculate_unmute_date(mute_date, mute_duration): - if isinstance(mute_date, int): - mute_date = datetime.fromtimestamp(mute_date) - return mute_date + timedelta(seconds=mute_duration) -def ban_history_view(request): - # Verificar si el usuario tiene un `username` en la sesión - username = request.session.get('username') - if not username: - return redirect('login') - - # Obtener el account_id a partir del username - account_id = get_account_id(username) - if not account_id: - return redirect('login') # O redirigir a otra página si no se encuentra el account_id - - print(f"Account ID: {account_id}") # Depurar el account_id - - account_bans, character_bans = get_ban_history(account_id) - - def _row(b, origen): - return { - 'fecha_baneo': b[0] or 'No disponible', - 'fecha_desbaneo': b[1] or 'No disponible', - 'motivo': b[2], - 'estado': 'Activo' if b[3] == 1 else 'Inactivo', - 'origen': f'{origen}: {b[4]}', - } - - ban_rows = ([_row(b, 'Cuenta') for b in account_bans] - + [_row(b, 'Personaje') for b in character_bans]) - - return render(request, 'account/ban_history.html', { - 'account_bans': account_bans, - 'character_bans': character_bans, - 'ban_rows': ban_rows, - 'now': datetime.now(), - }) -def security_history_view(request): - username = request.session.get('username') - if not username: - return redirect('login') # Si no está autenticado, redirigir al login - - # Obtener los últimos 20 intentos de inicio de sesión - login_attempts = LoginAttempt.objects.filter(username=username).order_by('-timestamp')[:20] - - login_attempts_data = [ - { - 'username': a.username, - 'status': a.status, - 'ip_address': a.ip_address, - 'timestamp': a.timestamp.strftime('%d-%m-%Y %H:%M:%S') if a.timestamp else '', - } - for a in login_attempts - ] - - return render(request, 'account/security_history.html', { - 'login_attempts': login_attempts, - 'login_attempts_data': login_attempts_data, - }) diff --git a/home/views/pages.py b/home/views/pages.py deleted file mode 100644 index 42e152d..0000000 --- a/home/views/pages.py +++ /dev/null @@ -1,92 +0,0 @@ -from ._base import * - - -def get_online_characters_count(): - # Si la BD de personajes (AzerothCore) aún no está disponible/poblada, - # no rompemos la portada: devolvemos 0 como hace el context_processor. - try: - with connections['acore_characters'].cursor() as cursor: - cursor.execute("SELECT COUNT(*) FROM characters WHERE online = 1") - result = cursor.fetchone() - return result[0] if result else 0 - except Exception as e: - logger.warning("No se pudo contar personajes en línea: %s", e) - return 0 -def check_server_status(host, port): - if not host or host == "N/A": - return False - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - s.settimeout(1) - try: - s.connect((host, port)) - s.close() - return True - except (socket.timeout, ConnectionRefusedError, socket.gaierror): - return False -def home_view(request): - noticias = Noticia.objects.order_by('-fecha_publicacion')[:50] - online_characters = get_online_characters_count() - server_selection = ServerSelection.objects.first() - status = check_server_status(server_selection.address, server_selection.port) if server_selection else None - game_version = server_selection.expansion if server_selection else 'Unknown' - - context = { - 'noticias': noticias, - 'server': server_selection, - 'status': 'Online' if status else 'Offline', - 'expansion': game_version, - 'online_characters': online_characters, - } - return render(request, 'home/home.html', context) -def download_client_view(request): - download_content = DownloadClientPage.objects.first() - categorias = ClienteCategoria.objects.all() - return render(request, 'download/download_client.html', { - 'download_content': download_content, - 'categorias': categorias, - }) -def download_addons_view(request): - return redirect("https://foro.novawow.com/files/") -def content_creators_view(request): - content_creator = ContentCreator.objects.first() - return render(request, 'community/content_creators.html', {'content_creator': content_creator}) -def server_status_view(request): - server_selection = ServerSelection.objects.first() - if server_selection: - status = check_server_status(server_selection.address, server_selection.port) - game_version = server_selection.expansion - else: - status = None - game_version = None - - return render(request, 'server_status/server_status.html', { - 'server': server_selection, - 'status': 'Online' if status else 'Offline', - 'game_version': game_version, - }) -def novawow_realm_view(request): - return render(request, 'realms/novawow_realm.html') -def contact_us_view(request): - return render(request, 'contact/contact_us.html') -def legal_notice_view(request): - return render(request, 'legal/legal_notice.html') -def maintenance_view(request): - return render(request, 'maintenance/maintenance.html') -def not_found(request, exception=None): - return render(request, '404.html', status=404) -def terms_and_conditions_view(request): - return render(request, 'legal/terms_and_conditions.html') -def privacy_policy_view(request): - return render(request, 'legal/privacy_policy.html') -def refund_policy_view(request): - return render(request, 'legal/refund_policy.html') -def cookies_view(request): - return render(request, 'legal/cookies.html') -def help_view(request): - # Verificar si el usuario está logueado - if 'username' not in request.session: - # Si no está logueado, redirigir al inicio de sesión - return redirect('index') - - # Si está logueado, renderizar la página de ayuda - return render(request, 'community/help.html') diff --git a/home/views/payments.py b/home/views/payments.py deleted file mode 100644 index 3f887d5..0000000 --- a/home/views/payments.py +++ /dev/null @@ -1,132 +0,0 @@ -from ._base import * - - -@csrf_exempt -def stripe_webhook(request): - payload = request.body - sig_header = request.META.get("HTTP_STRIPE_SIGNATURE") - - try: - event = stripe.Webhook.construct_event(payload, sig_header, settings.STRIPE_WEBHOOK_SECRET) - - if event["type"] == "checkout.session.completed": - session = event["data"]["object"] - session_id = session["id"] - stripe_ip = request.META.get("REMOTE_ADDR") # IP detectada - - # 🔹 Actualizar la IP de Stripe en el log - StripeLog.objects.filter(session_id=session_id).update(stripe_ip=stripe_ip, timestamp=now()) - - return JsonResponse({"success": True}) - - except stripe.error.SignatureVerificationError: - return JsonResponse({"success": False, "message": "Firma de webhook no válida."}, status=400) -def get_pending_battlepay_orders(account_id): - """Órdenes Battlepay PENDING de una cuenta de juego.""" - with connections['acore_auth'].cursor() as cursor: - cursor.execute( - "SELECT id, reference, product_id, product_name, price_eur, status, created_at " - "FROM battlepay_orders WHERE account_id = %s AND status = 'PENDING' " - "ORDER BY created_at DESC", - [account_id], - ) - cols = [c[0] for c in cursor.description] - return [dict(zip(cols, row)) for row in cursor.fetchall()] -def _mark_order_paid_by_reference(reference): - """Marca una orden como PAID solo si estaba PENDING. Devuelve filas afectadas.""" - with connections['acore_auth'].cursor() as cursor: - cursor.execute( - "UPDATE battlepay_orders SET status = 'PAID', paid_at = UNIX_TIMESTAMP() " - "WHERE reference = %s AND status = 'PENDING'", - [reference], - ) - return cursor.rowcount -def battlepay_view(request): - """Lista las órdenes Battlepay pendientes de la cuenta de juego seleccionada.""" - account_id = request.session.get('account_id') - if not account_id: - if request.session.get('bnet_id'): - return redirect('select-account') - return redirect('login') - - orders = get_pending_battlepay_orders(account_id) - return render(request, 'account/battlepay.html', { - 'orders': orders, - 'username': request.session.get('username'), - }) -def battlepay_pay_view(request): - """Crea un checkout de SumUp para pagar una orden Battlepay pendiente.""" - account_id = request.session.get('account_id') - if not account_id: - return redirect('login') - if request.method != 'POST': - return redirect('battlepay') - - reference = request.POST.get('reference', '').strip() - - # Validar que la orden pertenece a la cuenta y sigue pendiente - with connections['acore_auth'].cursor() as cursor: - cursor.execute( - "SELECT product_name, price_eur FROM battlepay_orders " - "WHERE reference = %s AND account_id = %s AND status = 'PENDING'", - [reference, account_id], - ) - row = cursor.fetchone() - - if not row: - return render(request, 'account/pago_sumup.html', {'error': 'Orden no encontrada o ya procesada.'}) - - product_name, price_eur = row - try: - checkout = crear_checkout_battlepay(reference, price_eur, product_name) - except Exception as e: - return render(request, 'account/pago_sumup.html', {'error': str(e)}) - - return render(request, 'account/pago_sumup.html', { - 'checkout_id': checkout.get('id'), - 'reference': reference, - 'product_name': product_name, - 'price_eur': price_eur, - }) -def pagar_sumup(request): - """Alias de compatibilidad: envía al listado de órdenes Battlepay.""" - return redirect('battlepay') -@csrf_exempt -def sumup_webhook(request): - """Webhook de SumUp: marca la orden Battlepay como PAID al completarse el pago.""" - if request.method != "POST": - return JsonResponse({"error": "Método no permitido"}, status=405) - - try: - data = json.loads(request.body or b'{}') - except json.JSONDecodeError: - return JsonResponse({"error": "JSON inválido"}, status=400) - - payload = data.get('payload') or data.get('data') or {} - reference = data.get('checkout_reference') or payload.get('checkout_reference') - status = (data.get('status') or payload.get('status') or '').upper() - checkout_id = data.get('id') or payload.get('checkout_id') or payload.get('id') - event_type = (data.get('event_type') or '').lower() - - # Si falta la referencia o el estado, consultarlo a la API de SumUp - if (not reference or status not in ('PAID', 'SUCCESSFUL')) and checkout_id: - try: - checkout = obtener_checkout(checkout_id) - reference = reference or checkout.get('checkout_reference') - status = (checkout.get('status') or status).upper() - except Exception as e: - logger.error("SumUp webhook: no se pudo consultar el checkout %s: %s", checkout_id, e) - - pagado = status in ('PAID', 'SUCCESSFUL') or event_type == 'transaction.successful' - - if reference and pagado: - try: - afectadas = _mark_order_paid_by_reference(reference) - except Exception as e: - logger.error("SumUp webhook: error actualizando la orden %s: %s", reference, e) - return JsonResponse({"error": "Error interno"}, status=500) - if afectadas: - return JsonResponse({"message": "Orden marcada como pagada"}, status=200) - return JsonResponse({"message": "Orden no pendiente o inexistente"}, status=200) - - return JsonResponse({"message": "Evento ignorado"}, status=200) diff --git a/home/views/players.py b/home/views/players.py deleted file mode 100644 index 683082d..0000000 --- a/home/views/players.py +++ /dev/null @@ -1,228 +0,0 @@ -from ._base import * - - -def get_race_icon(race): - race_icons = { - 1: "human-male", - 2: "orc-male", - 3: "dwarf-male", - 4: "night-elf-male", - 5: "undead-male", - 6: "tauren-male", - 7: "gnome-male", - 8: "troll-male", - 9: "goblin-male", - 10: "blood-elf-male", - 11: "draenei-male" - } - return race_icons.get(race, "unknown") -def get_race_iconss(race): - race_iconss = { - 1: "human", - 2: "orc", - 3: "dwarf", - 4: "night-elf", - 5: "undead", - 6: "tauren", - 7: "gnome", - 8: "troll", - 9: "goblin", - 10: "blood-elf", - 11: "draenei" - } - return race_iconss.get(race, "unknown") -def get_class_icon(char_class): - class_icons = { - 1: "warrior", - 2: "paladin", - 3: "hunter", - 4: "rogue", - 5: "priest", - 6: "death-knight", - 7: "shaman", - 8: "mage", - 9: "warlock", - 11: "druid" - } - return class_icons.get(char_class, "unknown") -def get_class_name(char_class): - names = { - 1: "Guerrero", - 2: "Paladín", - 3: "Cazador", - 4: "Pícaro", - 5: "Sacerdote", - 6: "Caballero de la Muerte", - 7: "Chamán", - 8: "Mago", - 9: "Brujo", - 11: "Druida" - } - return names.get(char_class, "Desconocido") -def get_achievement_name(achievement_id): - achievement_names = { - 457: "¡Primero del reino! Nivel 80", - 458: "¡Primero del reino! Pícaro nivel 80", - 463: "¡Primero del reino! Brujo nivel 80", - 461: "¡Primero del reino! Caballero de la Muerte nivel 80", - 462: "¡Primero del reino! Cazador nivel 80", - 467: "¡Primero del reino! Chamán nivel 80", - 466: "¡Primero del reino! Druida nivel 80", - 459: "¡Primero del reino! Guerrero nivel 80", - 460: "¡Primero del reino! Mago nivel 80", - 465: "¡Primero del reino! Paladín nivel 80", - 464: "¡Primero del reino! Sacerdote nivel 80", - 1408: "¡Primero del reino! Humano nivel 80", - 1407: "¡Primero del reino! Enano nivel 80", - 1409: "¡Primero del reino! Elfo de la noche nivel 80", - 1404: "¡Primero del reino! Gnomo nivel 80", - 1406: "¡Primero del reino! Draenei nivel 80", - 1410: "¡Primero del reino! Orco nivel 80", - 1413: "¡Primero del reino! Renegado nivel 80", - 1411: "¡Primero del reino! Tauren nivel 80", - 1412: "¡Primero del reino! Trol nivel 80", - 1405: "¡Primero del reino! Elfo de sangre nivel 80" - } - return achievement_names.get(achievement_id, "Logro Desconocido") -def novawow_players_view(request): - # Verificar si el usuario está logueado - if 'username' not in request.session: - return redirect('index') - - # Obtener las 5 hermandades con más miembros - with connections['acore_characters'].cursor() as cursor: - guild_query = """ - SELECT g.name, c.name AS leader, - CASE WHEN c.race IN (1, 3, 4, 7, 11) THEN 'alliance' ELSE 'horde' END AS faction, - COUNT(gm.guid) AS members, - g.createdate - FROM guild g - JOIN characters c ON c.guid = g.leaderguid - LEFT JOIN guild_member gm ON gm.guildid = g.guildid - GROUP BY g.guildid - ORDER BY members DESC - LIMIT 5 - """ - cursor.execute(guild_query) - guilds = cursor.fetchall() - - formatted_guilds = [ - (name, leader, faction, members, datetime.fromtimestamp(createdate).strftime('%d-%m-%Y')) - for name, leader, faction, members, createdate in guilds - ] - - # Obtener el top 10 de jugadores con más muertes con honor - with connections['acore_characters'].cursor() as cursor: - kills_query = """ - SELECT name, race, class, totalKills, todayKills - FROM characters - ORDER BY totalKills DESC - LIMIT 10 - """ - cursor.execute(kills_query) - top_kills = cursor.fetchall() - - players_with_kills = [ - { - 'name': name, - 'race_icon': f"big-{get_race_icon(race)}", - 'class_icon': f"{get_class_icon(char_class)}-medium", - 'total_kills': total_kills, - 'today_kills': today_kills - } - for name, race, char_class, total_kills, today_kills in top_kills - ] - - # Obtener personajes por clase y facción - with connections['acore_characters'].cursor() as cursor: - class_faction_query = """ - SELECT class, - SUM(CASE WHEN race IN (1, 3, 4, 7, 11) THEN 1 ELSE 0 END) AS alliance_count, - SUM(CASE WHEN race IN (2, 5, 6, 8, 10) THEN 1 ELSE 0 END) AS horde_count, - COUNT(*) AS total_count - FROM characters - GROUP BY class - """ - cursor.execute(class_faction_query) - class_faction_data = cursor.fetchall() - - classes_data = [ - { - 'class_icon': f"{get_class_icon(char_class)}-chest", - 'class_name': get_class_name(char_class), - 'total': total_count, - 'alliance': alliance_count, - 'horde': horde_count - } - for char_class, alliance_count, horde_count, total_count in class_faction_data - ] - - # Obtener el top 10 de jugadores con más logros - with connections['acore_characters'].cursor() as cursor: - achievements_query = """ - SELECT c.name, c.race, c.class, MAX(cap.counter) AS achievement_points - FROM character_achievement_progress cap - JOIN characters c ON cap.guid = c.guid - GROUP BY c.guid - ORDER BY achievement_points DESC - LIMIT 10 - """ - cursor.execute(achievements_query) - top_achievements = cursor.fetchall() - - players_with_achievements = [ - { - 'name': name, - 'race_icon': f"big-{get_race_icon(race)}", - 'class_icon': f"{get_class_icon(char_class)}-medium", - 'achievement_points': achievement_points - } - for name, race, char_class, achievement_points in top_achievements - ] - - # Obtener logros "Primeros del Reino" - with connections['acore_characters'].cursor() as cursor: - first_realm_query = """ - SELECT ca.achievement, c.name, c.race, c.class, c.gender, ca.date - FROM character_achievement ca - JOIN characters c ON ca.guid = c.guid - WHERE ca.achievement IN ( - 457, 458, 463, 461, 462, 467, 466, 459, 460, 465, 464, - 1408, 1407, 1409, 1404, 1406, 1410, 1413, 1411, 1412, 1405 - ) - ORDER BY ca.date ASC - """ - cursor.execute(first_realm_query) - first_realm_achievements = cursor.fetchall() - - first_realm_data = [] - for achievement in first_realm_achievements: - achievement_id, name, race, char_class, gender, date = achievement - formatted_date = datetime.fromtimestamp(date).strftime('%H:%M:%S %d-%m-%Y') - - # Determinar el ícono correcto basado en el logro - if achievement_id == 457: - icon_url = f"{settings.URL_PRINCIPAL}/static/nw-themes/nw-ryu/nw-images/nw-icons/achievement-level-80.jpg" - elif achievement_id in [1408, 1407, 1409, 1404, 1406, 1410, 1413, 1411, 1412, 1405]: - race_iconss = get_race_iconss(race) - gender_suffix = "male" if gender == 0 else "female" - icon_url = f"{settings.URL_PRINCIPAL}/static/nw-themes/nw-ryu/nw-images/nw-races/big-{race_iconss}-{gender_suffix}.webp" - else: - icon_url = f"{settings.URL_PRINCIPAL}/static/nw-themes/nw-ryu/nw-images/nw-classes/{get_class_icon(char_class)}-medium.jpg" - - first_realm_data.append({ - 'achievement_name': get_achievement_name(achievement_id), - 'name': name, - 'race_iconss': f"big-{get_race_iconss(race)}", - 'icon_url': icon_url, - 'date': formatted_date - }) - - # Renderizar la plantilla con los datos obtenidos - return render(request, 'realms/novawow_players.html', { - 'guilds': formatted_guilds, - 'players_with_kills': players_with_kills, - 'players_with_achievements': players_with_achievements, - 'classes_data': classes_data, - 'first_realm_data': first_realm_data - }) diff --git a/home/views/points.py b/home/views/points.py deleted file mode 100644 index d54736f..0000000 --- a/home/views/points.py +++ /dev/null @@ -1,113 +0,0 @@ -from ._base import * - - -def promo_code_view(request): - return render(request, 'account/promo_code.html') -def transfer_d_points_view(request): - # Renderizar la plantilla para la transferencia de puntos - return render(request, 'account/transfer_d_points.html') -def vote_points_view(request): - # Verificar si el usuario tiene un `username` en la sesión - username = request.session.get('username') - if not username: - return redirect('login') - - # Obtener el `account_id` basado en el `username` - with connections['acore_auth'].cursor() as cursor: - cursor.execute("SELECT id FROM account WHERE username = %s", [username]) - account_data = cursor.fetchone() - - if not account_data: - return JsonResponse({'success': False, 'message': 'Cuenta no encontrada.'}) - - account_id = account_data[0] - - if request.method == 'POST': - vote_url = request.POST.get('vote').strip() - - site = VoteSite.objects.filter(url=vote_url).first() - if not site: - return JsonResponse({'success': False, 'message': 'Sitio de votación no encontrado.'}) - - # Verificar si ya ha votado en las últimas 12 horas y 30 minutos - last_vote = VoteLog.objects.filter(account_id=account_id, vote_site=site).order_by('-created_at').first() - now = timezone.now() - if last_vote: - time_diff = now - last_vote.created_at - cooldown_period = timedelta(hours=12, minutes=30) - - if time_diff < cooldown_period: - remaining_time = cooldown_period - time_diff - hours, remainder = divmod(remaining_time.seconds, 3600) - minutes, _ = divmod(remainder, 60) - return JsonResponse({ - 'success': False, - 'message': f'Tienes que esperar {hours} horas y {minutes} minutos antes de volver a votar.' - }) - - # Acreditar puntos al usuario y registrar el voto - user_points = HomeApiPoints.objects.filter(accountID=account_id).first() - if user_points: - user_points.vp += site.points - user_points.save() - else: - HomeApiPoints.objects.create(accountID=account_id, vp=site.points, dp=0) - - # Registrar el voto - VoteLog.objects.create(account_id=account_id, vote_site=site, last_vote_time=now) - - return JsonResponse({'success': True, 'message': f'Has votado en {site.name}. Se han acreditado {site.points} PV.'}) - - vote_sites = VoteSite.objects.all() - return render(request, 'account/vote_points.html', { - 'vote_sites': vote_sites, - 'vote_sites_data': [ - {'name': s.name, 'image_url': s.image_url, 'points': s.points, 'url': s.url} - for s in vote_sites - ], - }) -def d_points_view(request): - """ - Vista para la página de D-Points. - Simplemente renderiza la plantilla d_points.html. - """ - return render(request, 'account/d_points.html') -def points_history_view(request): - return render(request, 'account/points_history.html') -def trans_history_view(request): - """ - Vista para mostrar el historial de transacciones de la cuenta y sus personajes. - """ - username = request.session.get('username') # Obtener el nombre de usuario de la sesión - - if username: - # Filtramos las transacciones que corresponden al usuario y a los personajes asociados - transactions = StripeLog.objects.filter(username=username) - else: - transactions = [] - - transactions_data = [ - { - 'account_id': t.account_id, - 'username': t.username, - 'character_name': t.character_name, - 'acore_ip': t.acore_ip, - 'email': t.email, - 'product_name': t.product_name, - 'amount': f"{t.amount} €", - 'timestamp': t.timestamp.strftime('%d-%m-%Y %H:%M:%S') if t.timestamp else '', - 'mode': t.mode, - } - for t in transactions - ] - - # Renderizamos la vista con el historial de transacciones - return render(request, 'account/trans_history.html', { - 'transactions': transactions, - 'transactions_data': transactions_data, - }) -def trade_points_view(request): - """ - Vista para la página de intercambio de puntos. - """ - return render(request, 'account/trade_points.html') diff --git a/home/views/recruit.py b/home/views/recruit.py deleted file mode 100644 index f4ec1b3..0000000 --- a/home/views/recruit.py +++ /dev/null @@ -1,161 +0,0 @@ -from ._base import * -from .account import get_account_status, get_account_characters - - -def recruit_a_friend_view(request): - username = request.session.get('username') - account_status = None - characters = [] - claimed_rewards_count = 0 - - # Si el usuario está logueado, obtener su información - if username: - # Obtener información del usuario - with connections['acore_auth'].cursor() as cursor: - cursor.execute("SELECT id, last_ip FROM account WHERE username = %s", [username]) - account_data = cursor.fetchone() - - if account_data: - account_id = account_data[0] - user_ip = account_data[1] - account_status = get_account_status(account_id) - characters = get_account_characters(account_id) - claimed_rewards_count = ClaimedReward.objects.filter(account_id=account_id).count() - else: - account_id = None - user_ip = None - else: - account_id = None - user_ip = None - - # Obtener todas las recompensas - all_rewards = RecruitReward.objects.all() - available_rewards = [] - - # Si el usuario está logueado, filtrar recompensas disponibles - if account_id: - available_rewards = [reward for reward in all_rewards if not ClaimedReward.objects.filter(account_id=account_id, recruit_reward=reward).exists()] - - # Manejar solicitudes POST (solo si el usuario está logueado) - if request.method == 'POST': - if not username: - return JsonResponse({'success': False, 'message': 'Debes estar logueado para reclamar una recompensa.'}) - - if not account_id or account_status is None: - return JsonResponse({'success': False, 'message': 'No se ha encontrado tu cuenta.'}) - - reward_id = request.POST.get('reward_id') - character_name = request.POST.get('character') - - if not reward_id or not character_name: - return JsonResponse({'success': False, 'message': 'Selecciona una recompensa y un personaje.'}) - - try: - reward = RecruitReward.objects.get(id=reward_id) - - # Verificar si ya se ha reclamado la recompensa - if ClaimedReward.objects.filter(account_id=account_id, recruit_reward=reward).exists(): - return JsonResponse({'success': False, 'message': 'Ya has reclamado esta recompensa.'}) - - # Verificar si cumple los requisitos - if account_status['recruited_level_80_count'] < reward.required_friends: - return JsonResponse({'success': False, 'message': 'No cumples con los requisitos.'}) - - # Obtener amigos reclutados que han alcanzado el nivel 80 - recruited_friends = get_recruited_friends_at_level_80(account_id, reward.required_friends) - - # Comprobar si alguno de los reclutados es válido - if len(recruited_friends) < reward.required_friends: - return JsonResponse({'success': False, 'message': 'No cumples con los requisitos o los amigos tienen la misma IP.'}) - - # Comprobar si alguno de los reclutados tiene la misma IP que el usuario - if check_recruited_ip_conflict(recruited_friends, user_ip): - return JsonResponse({'success': False, 'message': 'No puedes reclamar recompensas de amigos que tienen la misma IP.'}) - - # Ejecutar el comando SOAP - command = f".send items {character_name} 'Recompensa de Recluta Amigo' 'Tu amigo alcanzó el Nivel 80' {reward.item_id}:{reward.item_quantity}" - response = execute_soap_command(command) - - if response and "Mail sent to" in response: - # Registrar la recompensa como reclamada - ClaimedReward.objects.create( - account_id=account_id, - username=username, - recruit_reward=reward, - character_name=character_name, - claimed_at=timezone.now(), - ip_address=request.META.get('REMOTE_ADDR') - ) - claimed_rewards_count = ClaimedReward.objects.filter(account_id=account_id).count() - - return JsonResponse({ - 'success': True, - 'message': f"Recompensa '{reward.reward_name}' entregada a {character_name}.", - 'claimed_count': claimed_rewards_count - }) - else: - return JsonResponse({'success': False, 'message': 'Error al entregar la recompensa.'}) - - except RecruitReward.DoesNotExist: - return JsonResponse({'success': False, 'message': 'Recompensa no encontrada.'}) - - # Renderizar la página para usuarios no logueados y logueados - return render(request, 'recruit/recruit_a_friend.html', { - 'account_status': account_status, - 'all_rewards': all_rewards, - 'available_rewards': available_rewards, - 'characters': characters, - 'claimed_rewards_count': claimed_rewards_count, - 'is_logged_in': username is not None - }) -def check_recruited_ip_conflict(recruited_accounts, user_ip): - """ - Verifica si alguna de las cuentas reclutadas tiene la misma IP que la cuenta del reclutador. - """ - if not recruited_accounts: - return False - - with connections['acore_auth'].cursor() as cursor: - cursor.execute(""" - SELECT last_ip FROM account WHERE id IN %s - """, [tuple(recruited_accounts)]) - recruited_ips = [ip[0] for ip in cursor.fetchall()] - - return user_ip in recruited_ips -def get_recruited_friends_at_level_80(account_id, required_count): - """ - Obtiene una lista de IDs de amigos reclutados que han alcanzado el nivel 80 y - no tienen la misma IP que el reclutador. - """ - # Primero obtener los IDs de cuentas reclutadas desde `acore_auth` - with connections['acore_auth'].cursor() as auth_cursor: - auth_cursor.execute(""" - SELECT id, last_ip FROM account WHERE recruiter = %s - """, [account_id]) - recruited_accounts = auth_cursor.fetchall() - - if not recruited_accounts: - return [] - - # Extraer IDs y IPs de las cuentas reclutadas - recruited_ids = [acc[0] for acc in recruited_accounts] - recruited_ips = {acc[0]: acc[1] for acc in recruited_accounts} - - # Obtener la IP del usuario actual - with connections['acore_auth'].cursor() as cursor: - cursor.execute("SELECT last_ip FROM account WHERE id = %s", [account_id]) - user_ip = cursor.fetchone()[0] - - # Luego, verificar cuáles de esas cuentas tienen personajes al nivel 80 en `acore_characters` - with connections['acore_characters'].cursor() as char_cursor: - char_cursor.execute(""" - SELECT DISTINCT account FROM characters - WHERE level = 80 AND account IN %s - LIMIT %s - """, [tuple(recruited_ids), required_count]) - recruited_at_level_80 = [acc[0] for acc in char_cursor.fetchall()] - - # Filtrar cuentas que no tienen la misma IP que el usuario actual - valid_recruits = [acc for acc in recruited_at_level_80 if recruited_ips[acc] != user_ip] - - return valid_recruits diff --git a/home/views/store.py b/home/views/store.py deleted file mode 100644 index 7449dbe..0000000 --- a/home/views/store.py +++ /dev/null @@ -1,173 +0,0 @@ -from ._base import * - - -def store_novawow_cancel_view(request): - return render(request, 'store/store_novawow_cancel.html') -def store_novawow_view(request): - username = request.session.get('username') - if not username: - return redirect('login') - - category_name = request.GET.get("category") - - # Obtener personajes del usuario desde la base de datos acore_characters - with connections['acore_characters'].cursor() as cursor: - cursor.execute(""" - SELECT name - FROM characters - WHERE account = ( - SELECT id - FROM acore_auth.account - WHERE username = %s - ) - """, [username]) - characters = [row[0] for row in cursor.fetchall()] - - # Obtener categorías e ítems - categories = Category.objects.prefetch_related("items").all() - store_data = {category.name: list(category.items.values()) for category in categories} - - # Si se está solicitando una categoría específica, devolver solo los ítems de esa categoría - if category_name: - try: - category = Category.objects.get(name=category_name) - except Category.DoesNotExist: - return JsonResponse({'success': False, 'message': 'Categoría no encontrada'}, status=404) - return JsonResponse({'success': True, 'items': list(category.items.values())}) - - # Obtener carrito desde la sesión - carrito = request.session.get('carrito', []) - - # Calcular el precio total con 2 decimales - total_price = sum(Decimal(item['precio']) for item in carrito) - total_price_with_iva = (total_price * Decimal('1.21')).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP) - - if request.method == 'POST': - # Obtener datos del body - data = json.loads(request.body) - character_name = data.get('character') - carrito_cliente = data.get('carrito') or [] - - if not character_name: - return JsonResponse({'success': False, 'message': 'Por favor, selecciona un personaje.'}) - - # Verificar si el personaje pertenece al usuario - if character_name not in characters: - return JsonResponse({'success': False, 'message': 'No tienes permiso para este personaje.'}) - - if not carrito_cliente: - return JsonResponse({'success': False, 'message': 'El carrito está vacío.'}) - - # VALIDAR ÍTEMS Y PRECIOS CONTRA LA BASE DE DATOS (nunca confiar en el - # precio ni en los ítems que manda el cliente). - try: - ids_solicitados = [int(item['id']) for item in carrito_cliente] - except (KeyError, TypeError, ValueError): - return JsonResponse({'success': False, 'message': 'Carrito no válido.'}) - - items_db = {i.item_id: i for i in Item.objects.filter(item_id__in=ids_solicitados)} - - carrito = [] # carrito saneado que se guardará en sesión - total_price = Decimal('0.00') - for item in carrito_cliente: - item_id = int(item['id']) - db_item = items_db.get(item_id) - if not db_item: - return JsonResponse({'success': False, 'message': f'Ítem no disponible en la tienda (ID {item_id}).'}) - try: - cantidad = max(1, int(item.get('cantidad', 1))) - except (TypeError, ValueError): - cantidad = 1 - total_price += db_item.price * cantidad - carrito.append({'id': item_id, 'nombre': db_item.name, 'cantidad': cantidad}) - - total_price_with_iva = (total_price * Decimal('1.21')).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP) - - # Guardar en sesión el carrito YA VALIDADO (para la entrega tras el pago) - request.session['carrito'] = carrito - request.session.modified = True - - # Descripción del producto a partir de los datos validados - product_description = ", ".join([f"{it['nombre']} x{it['cantidad']}" for it in carrito]) - - # Obtener detalles del usuario - with connections['acore_auth'].cursor() as cursor: - cursor.execute("SELECT id, email, last_ip FROM account WHERE username = %s", [username]) - user_data = cursor.fetchone() - - if not user_data: - return JsonResponse({'success': False, 'message': 'Cuenta no encontrada.'}) - - user_id, email, acore_ip = user_data # ID de la cuenta, email y la última IP - - # Crear sesión de pago con Stripe - success_url = request.build_absolute_uri(reverse('store-novawow/success')) - cancel_url = request.build_absolute_uri(reverse('store-novawow/cancel')) - - # Llamada corregida a create_checkout_session con todos los parámetros requeridos - stripe_response = create_checkout_session( - username=username, - email=email, - acore_ip=acore_ip, - account_id=user_id, # Pasar el account_id - amount=total_price_with_iva, # El precio con IVA - character_name=character_name, - success_url=success_url, - cancel_url=cancel_url, - product_name=f"Compra de ítems para el personaje: {character_name} : {product_description}" - ) - - if stripe_response["success"]: - request.session['store_novawow'] = character_name - return JsonResponse({ - 'success': True, - 'session_id': stripe_response["session_id"], - 'stripe_public_key': settings.STRIPE_PUBLIC_KEY - }) - else: - return JsonResponse({'success': False, 'message': f'Error al iniciar el pago: {stripe_response["error"]}'}) - - return render(request, 'store/store_novawow.html', { - 'characters': characters, - 'store_data': store_data, - 'categories': categories, - 'total_price': total_price_with_iva - }) -@require_paid_stripe(redirect_to='store-novawow') -def store_novawow_success_view(request): - # Procesar el comando SOAP tras el pago exitoso - character_name = request.session.get('store_novawow') - carrito = request.session.get('carrito', []) - - if not character_name or not carrito: - return redirect('store-novawow') - - # Contar correctamente los ítems por ID - item_counts = Counter(item["id"] for item in carrito) - - # Generar la lista de ítems en formato "ID:Cantidad" - items_str = " ".join(f"{item_id}:{count}" for item_id, count in item_counts.items()) - - # Construir el comando SOAP - command = f".send items {character_name} 'Gracias por donar a Nova WoW' 'Aquí tienes tu donación' {items_str}" - - # Ejecutar el comando SOAP - response = execute_soap_command(command) - - # Limpiar la sesión después del envío - request.session.pop('store_novawow', None) - request.session.pop('carrito', None) - - if response is None or response.strip() == "": - message = f"Los ítems han sido añadidos al personaje {character_name} correctamente." - return render(request, 'store/store_novawow_success.html', {'message': message}) - else: - return render(request, 'store/store_novawow_success.html', {'message': f"Error: {response}"}) -def store_novawow_buscar_categoria_view(request, categoria): - try: - # Buscar la categoría desde el parámetro de la URL - category = Category.objects.get(name=categoria) - items = category.items.all().values("id", "name", "price", "image_url") - return JsonResponse({"success": True, "items": list(items)}) - except Category.DoesNotExist: - return JsonResponse({"success": False, "message": "Categoría no encontrada"}, status=404) diff --git a/home/zone_definitions.py b/home/zone_definitions.py deleted file mode 100644 index 069ee27..0000000 --- a/home/zone_definitions.py +++ /dev/null @@ -1,9166 +0,0 @@ -# zone_definitions.py - -# Diccionario que mapea ZoneID a nombres de zonas -ZONE_NAMES = { - - 1: "Dun Morogh", - 2: "Playa Larga", - 3: "Tierras Inhóspitas", - 4: "Las Tierras Devastadas", - 5: "- QA and DVD GLOBAL -", - 7: "Cala Aguasnegras", - 8: "Pantano de las Penas", - 9: "Valle de Villanorte", - 10: "Bosque del Ocaso", - 11: "Los Humedales", - 12: "Bosque de Elwynn", - 13: "Árbol del Mundo", - 14: "Durotar", - 15: "Marjal Revolcafango", - 16: "Azshara", - 17: "Los Baldíos del Norte", - 18: "Lago de Cristal", - 19: "Zul'Gurub", - 20: "Arroyo de la Luna", - 21: "Kul Tiras", - 22: "Isla del Programador", - 23: "Río de Villanorte", - 24: "Abadía de Villanorte", - 25: "Montaña Roca Negra", - 26: "Faro", - 28: "Tierras de la Peste del Oeste", - 30: "Nine", - 32: "El Cementerio", - 33: "Norte de la Vega de Tuercespina", - 34: "Mina del Eco", - 35: "Bahía del Botín", - 36: "Montañas de Alterac", - 37: "Lago Nazferiti", - 38: "Loch Modan", - 40: "Páramos de Poniente", - 41: "Paso de la Muerte", - 42: "Villa Oscura", - 43: "Orilla Salvaje", - 44: "Montañas Crestagrana", - 45: "Tierras Altas de Arathi", - 46: "Las Estepas Ardientes", - 47: "Tierras del Interior", - 49: "El Agujero del Muerto", - 51: "La Garganta de Fuego", - 53: "Campamento de Ladrones", - 54: "Cantera de Jaspe", - 55: "Valley of Heroes", - 56: "Vigilia de los Héroes", - 57: "Mina Abisal", - 59: "Viñedos de Villanorte", - 60: "Linde del bosque", - 61: "Cataratas del Trueno", - 62: "Plantación de Calabazas de Brackwell", - 63: "La Granja Pedregosa", - 64: "Los Viñedos de Maclure", - 65: "Cementerio de Dragones", - 66: "Zul'Drak", - 67: "Las Cumbres Tormentosas", - 68: "Lago Sempiterno", - 69: "Villa del Lago", - 70: "Petravista", - 71: "Cascadas Petravista", - 72: "El Portal Oscuro", - 73: "Escara Impía", - 74: "Charca de Lágrimas", - 75: "Rocal", - 76: "Retiro Fangoso", - 77: "Yunquemar", - 80: "Montañas de Ventormenta", - 81: "Jeff NE Quadrant Changed", - 82: "Jeff NW Quadrant", - 83: "Jeff SE Quadrant", - 84: "Jeff SW Quadrant", - 85: "Claros de Tirisfal", - 86: "Lago del Hito", - 87: "Villadorada", - 88: "Aserradero de la Vega del Este", - 89: "Vergel del Lago Espejo", - 91: "Torre de Azora", - 92: "Lago Espejo", - 93: "Túmulo de Vul'Gol", - 94: "Cerro del Cuervo", - 95: "Cañones de Crestagrana", - 96: "Torre de Ilgalar", - 97: "Molino de Alther", - 98: "Cavernas de Rethban", - 99: "Asentamiento Rebelde", - 100: "Expedición de Nesingwary", - 101: "Base de Kurzen", - 102: "Ruinas de Zul'Kunda", - 103: "Ruinas de Zul'Mamwe", - 104: "El Arrecife Mortal", - 105: "Túmulo Ogro Mosh'Ogg", - 106: "Las Reservas", - 107: "Finca de Saldean", - 108: "Colina del Centinela", - 109: "Plantación de Calabazas de Cejade", - 111: "Mina de Jango", - 113: "Mina de la Costa del Oro", - 115: "Faro de Poniente", - 116: "Valle Brumoso", - 117: "Campamento Grom'gol", - 118: "Excavación de Whelgar", - 120: "Cuartel de Arroyoeste", - 121: "Cementerio del Jardín Sereno", - 122: "Ruinas Zuuldaia", - 123: "Ruinas de Bal'lal", - 125: "Ruinas de Kal'ai", - 126: "Ruinas de Tkashi", - 127: "Ruinas de Balia'mah", - 128: "Ruinas de Ziata'jai", - 129: "Ruinas de Mizjah", - 130: "Bosque de Argénteos", - 131: "Kharanos", - 132: "Valle de Crestanevada", - 133: "Nueva Ciudad Manitas", - 134: "Cantera de Gol'Bolar", - 135: "Poblado Peloescarcha", - 136: "El Cubil Pardo", - 137: "Las Birras", - 138: "Refugio Pinobruma", - 139: "Tierras de la Peste del Este", - 141: "Teldrassil", - 142: "Excavación de Vetaferro", - 143: "Fortaleza de Mo'grosh", - 144: "Thelsamar", - 145: "Puerta de Algaz", - 146: "Presa de las Tres Cabezas", - 147: "Cabaña del Errante", - 148: "Costa Oscura", - 149: "Mina de Fuenteplata", - 150: "Puerto de Menethil", - 151: "Isla del Diseñador", - 152: "El Baluarte", - 153: "Ruinas de Lordaeron", - 154: "Camposanto", - 155: "Hoya Nocturácnidas", - 156: "Hacienda Solliden", - 157: "Molinos de Agamand", - 158: "Cripta de la Familia Agamand", - 159: "Rémol", - 160: "Jardines de los Susurros", - 161: "Bancal del Reposo", - 162: "Lago Aguasclaras", - 163: "Refugio de Gunther", - 164: "Granja de Garren", - 165: "Hacienda Balnir", - 166: "Mansión Friogar", - 167: "Avanzada de los Cruzados", - 168: "La Costa Norte", - 169: "Costa Murmurante", - 170: "Lago Lordamere", - 172: "Isla de Fenris", - 173: "Tumba de Faol", - 186: "Dolanaar", - 187: "Darnassus", - 188: "Cañada Umbría", - 189: "Almacén de Brasacerada", - 190: "Vega del Amparo", - 192: "Aserradero Crestanorte", - 193: "Andorhal", - 195: "Escuela de Nigromancia", - 196: "Tumba de Uther", - 197: "Colina de las Penas", - 198: "La Cueva del Llanto", - 199: "Campo de Piedramácula", - 200: "Granja de Dalson", - 201: "Gangrena de Gahrron", - 202: "El Tormento", - 203: "Fortaleza de Mardenholde", - 204: "Aldea Piroleña", - 205: "Dun Modr", - 206: "Fortaleza de Utgarde", - 207: "Mare Magnum", - 208: "Cala del Acorazado", - 209: "Castillo de Colmillo Oscuro", - 210: "Corona de Hielo", - 211: "Lago Glacial", - 212: "Lago de Helm", - 213: "Mina de Elem", - 214: "Mare Magnum", - 215: "Mulgore", - 219: "Hacienda de Alexston", - 220: "Mesa de la Nube Roja", - 221: "Campamento Narache", - 222: "Poblado Pezuña de Sangre", - 223: "Lago Toro de Piedra", - 224: "Caravana Devastada", - 225: "Roca Roja", - 226: "Penumbra de las Celerácnidas", - 227: "Campo de Valgan", - 228: "El Sepulcro", - 229: "Finca de Olsen", - 230: "La Muralla de Cringrís", - 231: "El Desafío de Beren", - 232: "Islas del Alba", - 233: "Molino Ámbar", - 235: "Castillo de Fenris", - 236: "Castillo de Colmillo Oscuro", - 237: "Los Campos Decrépitos", - 238: "Huerta de Malden", - 239: "Los Dominios de Ivar", - 240: "El Campo Muerto", - 241: "El Vergel Pútrido", - 242: "Arboleda del Destello", - 243: "Loma Inhóspita", - 244: "El Raquitismo", - 245: "La Hacienda Yorgen", - 246: "La Caldera", - 247: "Excavación de Limugre", - 249: "Roca Machacamiedo", - 250: "Ruinas de Thaurissan", - 251: "Peñasco Llamarada", - 252: "Bastión de Roca Negra", - 253: "El Pilar de Ceniza", - 254: "Montaña Roca Negra", - 255: "Altar de la Tempestad", - 256: "Aldrassil", - 257: "Gruta Narácnida", - 258: "Roca Mácula", - 259: "Lago Al'Ameth", - 260: "Aldea Brisa Estelar", - 261: "Tierras de los Tuercepinos", - 262: "Túmulo de Ban'ethil", - 263: "La Grieta", - 264: "El Claro del Oráculo", - 265: "Río Primigenio", - 266: "Lago Primigenio", - 267: "Laderas de Trabalomas", - 268: "Cráter de Azshara", - 269: "Dun Algaz", - 271: "Ruinas de Costasur", - 272: "Molino Tarren", - 275: "Castillo de Durnholde", - 276: "Stonewrought Pass", - 277: "Cuevas de la Ladera", - 278: "Granja de Brazie", - 279: "Cráter de Dalaran", - 280: "Strahnbrad", - 281: "Ruinas de Alterac", - 282: "Dominios de los Aplastacresta", - 283: "Cuenca de la Matanza", - 284: "Las Tierras Altas", - 285: "Puerta Austral", - 286: "Los Campos de Lodo", - 287: "Trabalomas", - 288: "Mina Azur", - 289: "Granja Nethander", - 290: "Dun Garok", - 293: "Muralla de Thoradin", - 294: "Playa del Este", - 295: "Playa del Oeste", - 296: "South Seas", - 297: "Isla Jaguero", - 298: "Bahía de Baradin", - 299: "Bahía de Menethil", - 300: "Playa Juncobruma", - 301: "La Costa Salvaje", - 302: "La Costa de Cristal", - 303: "Playa del Molusco", - 305: "Cala Mareanorte", - 306: "Cala Mareasur", - 307: "Los Acantilados Dominantes", - 308: "Mar Adusto", - 309: "Tumba de Barbaférrea", - 310: "Mina de Cristal", - 311: "Ruinas de Aboraz", - 312: "Cayo de Janeiro", - 313: "Mansión Redilnorte", - 314: "Granja Go'shek", - 315: "Granja de Dabyrie", - 316: "Sala Puño de Roca", - 317: "Poblado Secacorteza", - 318: "Cañón Mostacho Seco", - 320: "Refugio de la Zaga", - 321: "Sentencia", - 322: "Naufragios de Aguasnegras", - 323: "Campamento de O'Breen", - 324: "Castillo de Stromgarde", - 325: "Torre de Arathor", - 326: "El Sagrario", - 327: "Cala de Faldir", - 328: "El Arrecife Hundido", - 330: "Puente Thandol", - 331: "Vallefresno", - 332: "Mare Magnum", - 333: "Círculo de Vínculo Este", - 334: "Círculo de Vínculo Oeste", - 335: "Círculo de Vínculo Interior", - 336: "Círculo de Vínculo Exterior", - 337: "Descanso de Apócrifo", - 338: "Fortaleza de Angor", - 339: "Barranco Lethlor", - 340: "Ruinas de Kargath", - 341: "Asentamiento Kosh", - 342: "Asentamiento Boff", - 343: "Asentamiento Wurg", - 344: "Asentamiento Cagg", - 345: "El Final de Agmond", - 346: "Excavación de Piemartillo", - 347: "Gruta Rotapolvo", - 348: "Pico Nidal", - 349: "Fortaleza de los Martillo Salvaje", - 350: "Avanzada Quel'Danil", - 351: "Roca Oculta", - 352: "Zun'watha", - 353: "Shadra'Alor", - 354: "Jintha'Alor", - 355: "El Altar de Zul", - 356: "Seradane", - 357: "Feralas", - 358: "Barranco Cortazarza", - 359: "Bael Modan", - 360: "Mina Ventura y Cía.", - 361: "Frondavil", - 362: "Cerrotajo", - 363: "Valle de los Retos", - 364: "El Cubil", - 365: "Aquelarre del Filo Ardiente", - 366: "Ciudadela del Fuerte del Norte", - 367: "Poblado Sen'jin", - 368: "Islas del Eco", - 369: "Monte del Trueno", - 370: "Barranco Árido", - 371: "Cueva Viento Seco", - 372: "Fuerte de Tiragarde", - 373: "Costa de la Huida", - 374: "Bahía de Garrafilada", - 375: "Costa de Mortojo", - 377: "Río Furia del Sur", - 378: "Campamento Taurajo", - 379: "Avanzada del Puente", - 380: "El Cruce", - 381: "Mina Pedrusco", - 382: "El Fangal", - 383: "Las Colinas Áridas", - 384: "Cima Calígine", - 385: "Northwatch Hold", - 386: "Las Charcas del Olvido", - 387: "Oasis Aguaverde", - 388: "El Oasis Estancado", - 390: "Tierra de Gigantes", - 391: "La Costa Mercante", - 392: "Trinquete", - 393: "Playa Lanza Negra", - 394: "Colinas Pardas", - 395: "Fauceparda", - 396: "Pozo Pezuña Invernal", - 397: "Pozo Tronacuerno", - 398: "Pozo Ferocrín", - 399: "Cresta Horizonte", - 400: "Las Mil Agujas", - 401: "El Escalón de la Marea", - 403: "Posada Reposo Umbrío", - 404: "Excavación de Bael'dun", - 405: "Desolace", - 406: "Sierra Espolón", - 407: "Orgrimmar", - 408: "Isla de Gillijim", - 409: "Isla del Doctor Lapidis", - 410: "Cañón del Ventajo", - 411: "Ruinas de Bathran", - 412: "Las Ruinas de Ordil'Aran", - 413: "Atalaya de Maestra", - 414: "La Ensenada de Zoram", - 415: "Astranaar", - 416: "El Santuario de Aessina", - 417: "Santuario de la Escara", - 418: "Las Ruinas del Polvo Estelar", - 419: "Vega del Aullido", - 420: "Refugio Brisa de Plata", - 421: "Lago Mystral", - 422: "Lago Cielo Estrellado", - 424: "Ruinas del Lago Iris", - 425: "Poza de la Luna de Pureza", - 426: "Refugio de la Algaba", - 427: "El Rincón Lóbrego", - 428: "Senda de la Noche", - 429: "Xavian", - 430: "Satyrnaar", - 431: "Puesto del Hachazo", - 432: "El Túmulo de Dor'danil", - 433: "Río Falfarren", - 434: "Cerro Lumbrevil", - 435: "Barranco del Demonio", - 436: "Cresta del Demonio", - 437: "Aserradero Grito de Guerra", - 438: "Talloumbrío", - 439: "El Desierto de Sal", - 440: "Tanaris", - 441: "Lago Falathim", - 442: "Ruinas de Auberdine", - 443: "Ruinas de Mathystra", - 444: "Torre de Althalaxx", - 445: "Salto de Fonroca", - 446: "Bashal'Aran", - 447: "Ameth'Aran", - 448: "Páramo de los Ancianos", - 449: "La Espada del Maestro", - 450: "Excavación de Tripirrem", - 452: "Cabo de la Niebla", - 453: "Playa del Oleaje", - 454: "Río Culebra", - 455: "Cubil del Bosque Negro", - 456: "Río Fonroca", - 457: "Mar de la Bruma", - 458: "Camino del Oro", - 459: "Atalaya Escarlata", - 460: "Refugio Roca del Sol", - 461: "Risco Cortaviento", - 463: "Lago del Peñasco", - 464: "Lago Mirkfallon", - 465: "La Vega Carbonizada", - 466: "Valle Furia Sangrienta", - 467: "Cima del Espolón", - 468: "El Cubil del Espolón", - 469: "Vega del Gran Bosque", - 470: "Cima del Trueno", - 471: "Mesa de Viento Bravo", - 472: "Mesa de La Piedra de Fuego", - 473: "Rocamanto", - 474: "Hunter Rise", - 475: "Spirit Rise", - 476: "Elder Rise", - 477: "Ruinas de Jubuwal", - 478: "Estanques de Arlithrien", - 479: "Excavación Oximelena", - 480: "Campamento E'thok", - 481: "Risco Pezuña Quebrada", - 482: "Nido Alto", - 483: "Cañón del Chirrido", - 484: "Poblado Viento Libre", - 485: "El Gran Elevador", - 486: "Dominios Galak", - 487: "Guarida Malapluma", - 488: "El Hueco Perdido", - 489: "Thalanaar", - 490: "Cráter de Un'Goro", - 491: "Horado Rajacieno", - 492: "Cementerio del Cerro del Cuervo", - 493: "Claro de la Luna", - 495: "Fiordo Aquilonal", - 496: "Poblado Murohelecho", - 497: "Mansión Cienaluz", - 498: "Madriguera de los Cienorrojo", - 499: "Cueva Niebla Negra", - 500: "Cabo Moggle", - 501: "Siniestro de Beezil", - 502: "Colina de las Brujas", - 503: "Alto del Centinela", - 504: "Torre de la Punta Norte", - 505: "Torre de la Punta Oeste", - 506: "Punta Perdida", - 507: "Ciénaga Azul", - 508: "Ruinas Quebrantarrocas", - 509: "Cubil de la Llama", - 510: "El Pantano del Dragón", - 511: "Ciénaga de Fuego", - 512: "Poblado Pezuñanegra", - 513: "Isla Theramore", - 514: "Ciudadela Garrida", - 515: "Prisión del Acorazado", - 516: "Bahía Revolcafango", - 517: "Cala Furiamarea", - 518: "Playa Tenebruma", - 536: "Granja de Addle", - 537: "Cresta del Penacho en Llamas", - 538: "Fosas de Alquitrán Lakkari", - 539: "Camino del Terror", - 540: "La Cicatriz Serpenteante", - 541: "Refugio de Marshal", - 542: "Roca Fungal", - 543: "Baños de Golakka", - 556: "Loch Modan", - 576: "Refugio de los Mendigos", - 596: "Cementerio de Kodos", - 597: "Campamento del Espíritu Errante", - 598: "Playa de Sar'theris", - 599: "Fortaleza del Hacha de Trueno", - 600: "Cuenca de Bolgan", - 602: "Aquelarre Mannoroc", - 603: "Sargeron", - 604: "Shok'Thokar", - 606: "Poblado Gelkis", - 607: "Valle de las Lanzas", - 608: "Punta de Nijel", - 609: "Poblado Kolkar", - 616: "Monte Hyjal", - 618: "Cuna del Invierno", - 636: "Río Lobonegro", - 637: "Roca de los Kodos", - 638: "Sendero Oculto", - 639: "Roca de los Espíritus", - 640: "Santuario de la Llama Latente", - 656: "Lago Elune'ara", - 657: "El Puerto", - 676: "Terrallende", - 696: "Craftsmen's Terrace", - 697: "Tradesmen's Terrace", - 698: "The Temple Gardens", - 699: "Templo de Elune", - 700: "Cenarion Enclave", - 701: "Warrior's Terrace", - 702: "Aldea Rut'theran", - 716: "Complejo Vetaferro", - 717: "Las Mazmorras", - 718: "Cuevas de los Lamentos", - 719: "Cavernas de Brazanegra", - 720: "Isla de Batalla", - 721: "Gnomeregan", - 722: "Zahúrda Rajacieno", - 736: "Hondonada Ban'ethil", - 796: "Monasterio Escarlata", - 797: "Embarcadero de Jerod", - 798: "Torre de la Peña", - 799: "La Ribera Lóbrega", - 800: "Desfiladero de Crestanevada", - 801: "Valle Escalofrío", - 802: "Monte Luz", - 803: "Granja de Semperámbar", - 804: "Las Colinas Tundra", - 805: "Paso de la Puerta Sur", - 806: "Avanzada de la Puerta Sur", - 807: "Paso de la Puerta Norte", - 808: "Avanzada de la Puerta Norte", - 809: "Puertas de Forjaz", - 810: "Charca Aguaserena", - 811: "Vega Pesadilla", - 812: "Vega Venerácnidas", - 813: "El Baluarte", - 814: "Río Furia del Sur", - 815: "Río Furia del Sur", - 816: "Tierras Crines de Acero", - 817: "Roca del Cráneo", - 818: "Roca Crines Pálidas", - 819: "Cresta Viento Furioso", - 820: "Las Llanuras Doradas", - 821: "Las Llanuras Onduladas", - 836: "Dun Algaz", - 837: "Dun Algaz", - 838: "Paso de la Puerta Norte", - 839: "Paso de la Puerta Sur", - 856: "Arboleda del Crepúsculo", - 876: "Isla de los MJ", - 877: "Delete ME", - 878: "Río Furia del Sur", - 879: "Río Furia del Sur", - 880: "Puente Thandol", - 881: "Puente Thandol", - 896: "Isla del Purgatorio", - 916: "La Finca de Jansen", - 917: "El Campo Funesto", - 918: "La Granja de Molsen", - 919: "Estanque de Stendel", - 920: "Las Colinas Afiladas", - 921: "Paraje de Demont", - 922: "Los Yermos Polvorientos", - 923: "Valle Rompecantos", - 924: "Valle de los Reyes", - 925: "Estación de Algaz", - 926: "La granja de Bucklebree", - 927: "La Playa Plateada", - 928: "Desembarco Mareanorte", - 936: "Fuerte Zarpagris", - 956: "Los Verdegales", - 976: "Gadgetzan", - 977: "Puerto Bonvapor", - 978: "Zul'Farrak", - 979: "Vigía Penas de Arena", - 980: "Valle Cardizal", - 981: "Sima Abierta", - 982: "La Guarida Ponzoñosa", - 983: "Base Machacaduna", - 984: "Ruinas de Lunaeste", - 985: "Campo del Manantial", - 986: "Guarida de Zalashji", - 987: "Playa Finisterrae", - 988: "Playa Baile de las Olas", - 989: "Ruinas de Uldum", - 990: "Valle de los Vigías", - 991: "Puesto de Gunstan", - 992: "Ruinas de Lunasur", - 996: "Campamento de Render", - 997: "Valle de Render", - 998: "Roca de Render", - 999: "Torre de Petravista", - 1000: "Valle de Galardell", - 1001: "Camino del Lago", - 1002: "Tres Caminos", - 1016: "Cerro Fraguaferoz", - 1017: "Colina del Raptor", - 1018: "Pantano del Canal Negro", - 1019: "La Franja Verde", - 1020: "Pantano Pellejomusgo", - 1021: "Roca Thelgen", - 1022: "Pantano Branquiazul", - 1023: "Cañada Salobre", - 1024: "Pantano del Ocaso", - 1025: "La Franja Verde", - 1036: "Campamento Dentellada", - 1037: "Grim Batol", - 1038: "Puertas Faucedraco", - 1039: "La Flota Perdida", - 1056: "Colinas de Darrow", - 1057: "Muralla de Thoradin", - 1076: "Senda de las Tejedoras", - 1097: "La Ribera Silente", - 1098: "Mansión Mantoniebla", - 1099: "Campamento Mojache", - 1100: "Dominios Tótem Siniestro", - 1101: "Las Galerías Retorcidas", - 1102: "Lago Ventosalvaje", - 1103: "Avanzada Gordunni", - 1104: "Mok'Gordun", - 1105: "Vega Cicatriz Feral", - 1106: "Tierras Altas de Plumavieja", - 1107: "Lago Soplo", - 1108: "La Costa Olvidada", - 1109: "Pilar Este", - 1110: "Pilar Oeste", - 1111: "Rama Oniria", - 1112: "Lago Jademir", - 1113: "Oneiros", - 1114: "Ruinas de Viento Azabache", - 1115: "Dominios de los Cicatriz de Rabia", - 1116: "Ruinas de Plumaluna", - 1117: "Ruinas de Solarsal", - 1118: "Lower Wilds", - 1119: "Los Dos Colosos", - 1120: "Isla de Sardor", - 1121: "Isla del Terror", - 1136: "Altas Tierras Salvajes", - 1137: "Bajas Tierras Salvajes", - 1156: "Los Baldíos del Sur", - 1157: "Camino del Oro Sur", - 1176: "Zul'Farrak", - 1196: "Pináculo de Utgarde", - 1216: "Bastión Faucenegra", - 1217: "Campamento Vanndir", - 1218: "TESTAzshara", - 1219: "Campamento Legashi", - 1220: "Campamento Base Darnassiano", - 1221: "Ruinas de Eldarath", - 1222: "Guarida de Hetaera", - 1223: "Templo de Zin-Malor", - 1224: "Cabeza de Oso", - 1225: "Ursolan", - 1226: "Ruinas de Arkkoran", - 1227: "Bahía de la Tempestad", - 1228: "La Playa Arrasada", - 1229: "Torre de Eldara", - 1230: "Arrecife Dentado", - 1231: "Playa del Arrecife Sur", - 1232: "Monumento Cresta Cuervo", - 1233: "Loma Desolada", - 1234: "Lago Mennar", - 1235: "Santuario Cantosombrío", - 1236: "Campamento Haldarr", - 1237: "Valormok", - 1256: "Las Ruinas", - 1276: "Valle del Espolón", - 1277: "El Paso del Espolón", - 1296: "Granja Rocamuela", - 1297: "La Pocilga", - 1316: "Zahúrda Rajacieno", - 1336: "Cala del Aparejo Perdido", - 1337: "Uldaman", - 1338: "Lago Lordamere", - 1339: "Lago Lordamere", - 1357: "Camino de la Horca", - 1377: "Silithus", - 1397: "Bosque Esmeralda", - 1417: "Templo Sumergido", - 1437: "Bastión Machacamiedo", - 1438: "Castillo de Nethergarde", - 1439: "Puesto Machacamiedo", - 1440: "Serpiente Enroscada", - 1441: "Altar de la Tempestad", - 1442: "Cresta Vigía", - 1443: "La Fosa de la Escoria", - 1444: "El Mar de las Cenizas", - 1445: "Montaña Roca Negra", - 1446: "Puesto del Torio", - 1457: "Minas de Nethergarde", - 1477: "El Templo de Atal'Hakkar", - 1497: "Entrañas", - 1517: "Uldaman", - 1518: "Not Used Deadmines", - 1519: "Ciudad de Ventormenta", - 1537: "Forjaz", - 1557: "Campamento Pezuña Quebrada", - 1577: "El Cabo de Tuercespina", - 1578: "Costa Salvaje del Sur", - 1579: "The Deadmines 002", - 1580: "Ironclad Cove 003", - 1581: "Las Minas de la Muerte", - 1582: "Cala del Acorazado", - 1583: "Cumbre de Roca Negra", - 1584: "Profundidades de Roca Negra", - 1597: "Raptor Grounds", - 1598: "Grol'dom Farm", - 1599: "Campamento de Mor'shan", - 1600: "Honor's Stand", - 1601: "Blackthorn Ridge", - 1602: "Bramblescar", - 1603: "Agama'gor", - 1617: "Valle de los Héroes", - 1637: "Orgrimmar", - 1638: "Cima del Trueno", - 1639: "Alto de los Ancestros", - 1640: "Alto de los Espíritus", - 1641: "Alto de los Cazadores", - 1657: "Darnassus", - 1658: "Enclave Cenarion", - 1659: "Bancal del Artesano", - 1660: "Bancal del Guerrero", - 1661: "Los Jardines del Templo", - 1662: "Bancal de los Mercaderes", - 1677: "Saliente de Gavin", - 1678: "Saliente de Sofera", - 1679: "Daga de Corrahn", - 1680: "La Punta", - 1681: "Costa de la Neblina", - 1682: "Redil de Dandred", - 1683: "Caverna Estrecha", - 1684: "Alto del Orvallo", - 1697: "Tierras de los Raptores", - 1698: "Rajazarza", - 1699: "Colina Espinosa", - 1700: "Agama'gor", - 1701: "Loma Espina Negra", - 1702: "El Alto del Honor", - 1703: "La Empalizada de Mor'shan", - 1704: "Granja de Grol'dom", - 1717: "Horado Rajacieno", - 1718: "El Gran Elevador", - 1737: "Valle del Velo de Bruma", - 1738: "Manantial Nek'mani", - 1739: "Escondrijo Malallave", - 1740: "Base de Ventura y Cía.", - 1741: "Arena Gurubashi", - 1742: "Cubil del Espíritu", - 1757: "El Velo Carmesí", - 1758: "Las Mareas Vivas", - 1759: "La Damisela Afortunada", - 1760: "Centro de Operaciones de Ventura y Cía.", - 1761: "Aldea Muertobosque", - 1762: "Poblado Zarpavil", - 1763: "Jaedenar", - 1764: "Río del Veneno", - 1765: "Cascadas del Veneno", - 1766: "Cañada Gran Cicatriz", - 1767: "Bosque de Troncoferro", - 1768: "Caverna de Troncoferro", - 1769: "Bastión Fauces de Madera", - 1770: "Guarida Sombría", - 1771: "Santuario del Impostor", - 1777: "Cueva de Manantial", - 1778: "Tinieblas de las Penas", - 1779: "Aldea Draenil'dur", - 1780: "Cruce Lanzarrota", - 1797: "Stagalbog", - 1798: "Lodo Traicionero", - 1817: "Cueva Stagalbog", - 1837: "Cuevas Secacorteza", - 1857: "Muralla de Thoradin", - 1858: "Roca'gor", - 1877: "Valle de los Colmillos", - 1878: "Terraseca", - 1879: "Explanada del Espejismo", - 1880: "Cobertizo de Barbapluma", - 1881: "Campamento Machacacanillas", - 1882: "Barranco Bruma Enferma", - 1883: "Lago Ventobravo", - 1884: "Agol'watha", - 1885: "Estación de Investigación de Hiri'watha", - 1886: "Las Ruinas Abyectas", - 1887: "Arrecife de Bogen", - 1897: "Bancal del Hacedor", - 1898: "Barranco Viento Seco", - 1917: "Shaol'watha", - 1937: "Ruinas Sombrasol", - 1938: "Pilar Partido", - 1939: "Arenas Abisales", - 1940: "Tierras del Sur", - 1941: "Cavernas del Tiempo", - 1942: "Los Pantanales", - 1943: "Meseta Roca de Hierro", - 1957: "Cueva Tizonegro", - 1958: "Base de Peleteros", - 1959: "Valle Polvofuego", - 1977: "Zul'Gurub", - 1978: "Granja Juncobruma", - 1997: "Puesto del Veneno", - 1998: "Claro Ramaespolón", - 2017: "Stratholme", - 2037: "Quel'Thalas", - 2057: "Scholomance", - 2077: "Vega Crepuscular", - 2078: "Orilla Crepuscular", - 2079: "Isla de Alcaz", - 2097: "Cumbre de la Nube Negra", - 2098: "Catacumbas del Bosque Aurora", - 2099: "Fuerte de Petravista", - 2100: "Maraudon", - 2101: "Pensión La Cebada", - 2102: "Destilería Cebatruenos", - 2103: "Castillo de Menethil", - 2104: "Mesón Aguahonda", - 2117: "Sepulcro Sombrío", - 2118: "Concejo de Rémol", - 2119: "Mesón La Horca", - 2137: "The Pools of Vision", - 2138: "Cubil Calígine", - 2157: "Fortaleza de Bael'dun", - 2158: "Cubil de Brasaliza", - 2159: "Guarida de Onyxia", - 2160: "Mina Cortaviento", - 2161: "Condena de Roland", - 2177: "Liza", - 2197: "Pozas de las Visiones", - 2198: "Barranco Rompesombras", - 2217: "Aldea Lanza Partida", - 2237: "Campamento del Tramo Blanco", - 2238: "Gornia", - 2239: "Cráter del Ojo de Zane", - 2240: "Circuito del Espejismo", - 2241: "Roca Sable de Hielo", - 2242: "La Arboleda Oculta", - 2243: "Puesto de los Fauces de Madera", - 2244: "Poblado Nevada", - 2245: "Mazthoril", - 2246: "Baños de Fuego de Escarcha", - 2247: "Colinas Cardo Nevado", - 2248: "Dun Mandarr", - 2249: "Cañón Levescarcha", - 2250: "Matorral del Ala del Búho", - 2251: "Lago Kel'Theril", - 2252: "Las Ruinas de Kel'Theril", - 2253: "Aldea Estrella Fugaz", - 2254: "Túmulo de Ban'Thallow", - 2255: "Vista Eterna", - 2256: "Garganta Negro Rumor", - 2257: "Tranvía Subterráneo", - 2258: "Cuenca Fungal", - 2259: "The Marris Stead", - 2260: "Hacienda de Marris", - 2261: "La Subgranja", - 2262: "Villa Darrow", - 2263: "Torre de la Corona", - 2264: "Cruce de Corin", - 2265: "Campamento Cultor de la Muerte", - 2266: "Mano de Tyr", - 2267: "La Basílica Escarlata", - 2268: "Capilla de la Esperanza de la Luz", - 2269: "Molino Cejifrente", - 2270: "El Claro Ponzoñoso", - 2271: "Torre de la Muralla del Este", - 2272: "Vallenorte", - 2273: "Zul'Mashar", - 2274: "Mazra'Alor", - 2275: "Torre del Paso Norte", - 2276: "Refugio Quel'Lithien", - 2277: "Bosque de la Peste", - 2278: "Fuerte de la Plaga", - 2279: "Stratholme", - 2280: "DO NOT USE", - 2297: "Lago Darrowmere", - 2298: "Castel Darrow", - 2299: "Lago Darrowmere", - 2300: "Cavernas del Tiempo", - 2301: "Poblado Piel de Cardo", - 2302: "El Lodazal", - 2303: "Cañón Rompevientos", - 2317: "Mares del Sur", - 2318: "Mare Magnum", - 2319: "Mare Magnum", - 2320: "Mare Magnum", - 2321: "Mare Magnum", - 2322: "Mar de la Bruma", - 2323: "Mar de la Bruma", - 2324: "Mar de la Bruma", - 2325: "Mar de la Bruma", - 2326: "Mar de la Bruma", - 2337: "Cuartel de Cerrotajo", - 2338: "Mares del Sur", - 2339: "Mare Magnum", - 2357: "Asentamiento Sangradientes", - 2358: "Canción del Bosque", - 2359: "Poblado Zarpaverde", - 2360: "Avanzada Ala de Plata", - 2361: "Amparo de la Noche", - 2362: "Santuario de Remulos", - 2363: "Túmulo de Tempestira", - 2364: "Mare Magnum", - 2365: "Mare Magnum", - 2366: "La Ciénaga Negra", - 2367: "Antiguas Laderas de Trabalomas", - 2368: "Molino Tarren", - 2369: "Costasur", - 2370: "Castillo de Durnholde", - 2371: "Dun Garok", - 2372: "Campos de Trabalomas", - 2373: "Playa del Este", - 2374: "Granja Nethander", - 2375: "Colinas de Darrow", - 2376: "Torre Austral", - 2377: "Muralla de Thoradin", - 2378: "Playa del Oeste", - 2379: "Mina Azur", - 2397: "Mare Magnum", - 2398: "Mare Magnum", - 2399: "Mare Magnum", - 2400: "Mar Adusto", - 2401: "Mar Adusto", - 2402: "Mar Adusto", - 2403: "Mar Adusto", - 2404: "Tethris Aran", - 2405: "Ethel Rethor", - 2406: "Isla Ranazjar", - 2407: "Cabaña de Kormek", - 2408: "Aldea Cazasombras", - 2417: "Desfiladero de Roca Negra", - 2418: "Vigilia de Morgan", - 2419: "Roca Desliz", - 2420: "Senda del Ala del Terror", - 2421: "Draco'dar", - 2437: "Sima Ígnea", - 2457: "Bosque Arrullanoche", - 2477: "Mar de la Bruma", - 2478: "Morlos'Aran", - 2479: "Santuario Esmeralda", - 2480: "Cañada Fuego de Jade", - 2481: "Ruinas de Constellas", - 2497: "Costa Amarga", - 2517: "Alto de los Rapiñadores", - 2518: "Pabellón de Lariss", - 2519: "Colinas Zarpaleña", - 2520: "Guarida de los Zarpaleña", - 2521: "Río Verdantis", - 2522: "Ruinas de Isildien", - 2537: "Poblado Tótem Siniestro", - 2538: "Campamento Aparaje", - 2539: "Malaka'jin", - 2540: "Barranco del Alud", - 2541: "Cañón Sishir", - 2557: "La Masacre", - 2558: "Barranco de la Muerte", - 2559: "Río Diamante", - 2560: "Campamento de Ariden", - 2561: "El Vicio", - 2562: "Karazhan", - 2563: "Terreno de Morgan", - 2577: "La Masacre", - 2597: "Valle de Alterac", - 2617: "Campamento de los Mezclatornillos", - 2618: "Camino Fuego de Jade", - 2619: "Río Thondroril", - 2620: "Río Thondroril", - 2621: "Lago Mereldar", - 2622: "Cicatriz Pestilente", - 2623: "La Cicatriz Purulenta", - 2624: "Lago del Bosque Negro", - 2625: "Puerta de la Muralla del Este", - 2626: "Túnel Terroarácnido", - 2627: "Valle del Terror", - 2637: "Fuerte de Kargathia", - 2657: "Valle de los Huesos", - 2677: "Guarida de Alanegra", - 2697: "Cruce de la Muerte", - 2717: "Núcleo de Magma", - 2737: "El Muro del Escarabajo", - 2738: "Aldea del Viento del Sur", - 2739: "Campamento Crepuscular", - 2740: "Vega de Cristal", - 2741: "Estrado del Escarabajo", - 2742: "Colmen'Ashi", - 2743: "Colmen'Zora", - 2744: "Colmen'Regal", - 2757: "Santuario del Guerrero Caído", - 2777: "Alterac Valley", - 2797: "Cavernas de Brazanegra", - 2817: "Bosque Canto de Cristal", - 2837: "El Sótano del Maestro", - 2838: "Paso de Fraguapiedra", - 2839: "Valle de Alterac", - 2857: "La Jaula del Rugido", - 2877: "Chunk Test", - 2897: "Avanzada de Zoram'gar", - 2917: "Sala de las Leyendas", - 2918: "Sala de los Campeones", - 2937: "Dominios Grosh'gok", - 2938: "Desfiladero del Letargo", - 2957: "Mina Ferrohondo", - 2958: "Avanzada Piedrahogar", - 2959: "Dun Baldar", - 2960: "Paso de Ala Gélida", - 2961: "Aldea Lobo Gélido", - 2962: "Torre de la Punta", - 2963: "Mina Dentefrío", - 2964: "Fuerte Hacha Invernal", - 2977: "Baluarte Sangrehielo", - 2978: "Bastión Lobo Gélido", - 2979: "Granja Tor'kren", - 3017: "Paso de la Daga Escarcha", - 3037: "Campamento Roca de Hierro", - 3038: "Cráter de la Comadreja", - 3039: "Ruinas de Tahonda", - 3057: "Tierra de Disputa", - 3058: "Cueva Ala Gélida", - 3077: "Sosiego del Valor", - 3097: "El Pilar de la Ascensión", - 3098: "Puesto Crepuscular", - 3099: "Avanzada Crepuscular", - 3100: "Campamento Crepuscular Devastado", - 3117: "Guarida de Shalzaru", - 3137: "Punta Talrendis", - 3138: "Sagrario de Rethress", - 3139: "Cubil del Horror de la Luna", - 3140: "Cueva de Barbaescamas", - 3157: "Cueva del Alud", - 3177: "Campo de trabajos forzados Grito de Guerra", - 3197: "Campamento del Orvallo", - 3217: "La Marra", - 3237: "The Maul", - 3257: "Huesos de Grakkarond", - 3277: "Garganta Grito de Guerra", - 3297: "Cementerio Lobo Gélido", - 3298: "Paso Lobo Gélido", - 3299: "Paso de Dun Baldar", - 3300: "Cementerio Sangrehielo", - 3301: "Cementerio Avalancha", - 3302: "Cementerio Piedrahogar", - 3303: "Cementerio Pico Tormenta", - 3304: "Búnker Ala Gélida", - 3305: "Búnker Piedrahogar", - 3306: "Risco Zarpa Salvaje", - 3317: "Poblado Sañadiente", - 3318: "Roca de Durotan", - 3319: "Claro Ala de Plata", - 3320: "Serrería Grito de Guerra", - 3321: "Bastión Ala de Plata", - 3337: "Caverna Zarpa Salvaje", - 3338: "La Grieta Velada", - 3357: "Isla Yojamba", - 3358: "Cuenca de Arathi", - 3377: "El Serpenteo", - 3378: "Dominios de Mandokir", - 3380: "Altar del Dios de la Sangre", - 3382: "Templo de Bethekk", - 3384: "El Altar Demoniaco", - 3398: "El Extremo de la Locura", - 3417: "Bastión de Aterratrols", - 3418: "Guarida de los Rapiñadores", - 3420: "Granja", - 3421: "Herrería", - 3422: "Aserradero", - 3423: "Mina de oro", - 3424: "Establos", - 3425: "Fuerte Cenarion", - 3426: "Punta de Corzocelada", - 3427: "Campamento Barbabronce", - 3428: "Ahn'Qiraj", - 3429: "Ruinas de Ahn'Qiraj", - 3430: "Bosque Canción Eterna", - 3431: "Isla del Caminante del Sol", - 3432: "Santuario de Dath'Remar", - 3433: "Tierras Fantasma", - 3434: "Bancal del Escarabajo", - 3435: "Bancal del General", - 3436: "La Presa", - 3437: "El Criadero", - 3438: "El Panal", - 3439: "Bancal de los Oteadores", - 3440: "Bancal del Escarabajo", - 3441: "Bancal del General", - 3442: "La Presa", - 3443: "El Criadero", - 3444: "El Panal", - 3445: "Bancal de los Oteadores", - 3446: "Paseo Crepúsculo", - 3447: "Guarida de Ortell", - 3448: "Bancal del Escarabajo", - 3449: "Bancal del General", - 3450: "La Presa", - 3451: "El Criadero", - 3452: "El Panal", - 3453: "Bancal de los Oteadores", - 3454: "Ruinas de Ahn'Qiraj", - 3455: "El Mar del Norte", - 3456: "Naxxramas", - 3457: "Karazhan", - 3459: "Ciudad", - 3460: "La Ensenada Dorada", - 3461: "Fondeadero Vela del Sol", - 3462: "Aldea Brisa Pura", - 3463: "Puerta Magister", - 3464: "El Retiro del Errante", - 3465: "Sagrario del Norte", - 3466: "Sagrario del Oeste", - 3467: "Sagrario del Este", - 3468: "Refugio de Saltheril", - 3469: "Caballería de Thuron", - 3470: "Charca Plácido Susurro", - 3471: "El Bosque Viviente", - 3472: "Costa Bris Azur", - 3473: "Lago Elrendar", - 3474: "La Arboleda Agostada", - 3475: "Zeb'Watha", - 3476: "Tor'Watha", - 3477: "Azjol-Nerub", - 3478: "Puerta de Ahn'Qiraj", - 3479: "Mar de la Bruma", - 3480: "Tierras Ocaso Marchito", - 3481: "Aguja Ocaso Marchito", - 3482: "La Cicatriz Muerta", - 3483: "Península del Fuego Infernal", - 3484: "La Aguja del Sol", - 3485: "Academia Falthrien", - 3486: "Mansión Ravenholdt", - 3487: "Ciudad de Lunargenta", - 3488: "Tranquillien", - 3489: "Aldea Corona del Sol", - 3490: "Aldea Bruma Dorada", - 3491: "Aldea Brisaveloz", - 3492: "Aguja Brisaveloz", - 3493: "Sagrario del Sol", - 3494: "Sagrario de la Luna", - 3495: "Aguja de la Estrella del Alba", - 3496: "Enclave del Errante", - 3497: "An'daroth", - 3498: "An'telas", - 3499: "An'owyn", - 3500: "Ciudad de la Muerte", - 3501: "Zigurat Sangrante", - 3502: "Zigurat Aullante", - 3503: "Isla Shalandis", - 3504: "Los Dominios Toryl", - 3505: "Minas Sondaluz", - 3506: "Dominios Andilien", - 3507: "Colinas Hacha", - 3508: "Paso de Amani", - 3509: "Cima Rasguño de Sol", - 3510: "Catacumbas Amani", - 3511: "Torre de los Condenados", - 3512: "Zeb'Sora", - 3513: "Lago Elrendar", - 3514: "La Cicatriz Muerta", - 3515: "Río Elrendar", - 3516: "Zeb'Tela", - 3517: "Zeb'Nowa", - 3518: "Nagrand", - 3519: "Bosque de Terokkar", - 3520: "Valle Sombraluna", - 3521: "Marisma de Zangar", - 3522: "Montañas Filospada", - 3523: "Tormenta Abisal", - 3524: "Isla Bruma Azur", - 3525: "Isla Bruma de Sangre", - 3526: "Valle Ammen", - 3527: "Lugar del Accidente", - 3528: "Lago Lineargenta", - 3529: "Matorral Cubrebosque", - 3530: "Cresta de las Sombras", - 3531: "El Frontal de la Muerte", - 3532: "Calle del Alba", - 3533: "Ruinas de Lunargenta", - 3534: "Camino de Feth", - 3536: "Thrallmar", - 3537: "Tundra Boreal", - 3538: "Bastión del Honor", - 3539: "Los Peldaños del Destino", - 3540: "El Vacío Abisal", - 3541: "Campamento Forja: Mageddon", - 3542: "El Camino a la Gloria", - 3543: "La Gran Fisura", - 3544: "Llanura de los Fragmentos", - 3545: "Ciudadela del Fuego Infernal", - 3546: "Armería de Expedición", - 3547: "Trono de Kil'jaeden", - 3548: "Campamento Forja: Ira", - 3549: "Punto de Invasión: Aniquilador", - 3550: "Ruinas Borune", - 3551: "Ruinas de Sha'naar", - 3552: "Templo de Telhamat", - 3553: "Pozas de Aggonar", - 3554: "Avanzada del Halcón", - 3555: "Puesto Mag'har", - 3556: "Cubil de Haal'esh", - 3557: "El Exodar", - 3558: "Cascadas Elrendar", - 3559: "Colinas Cubrebosque", - 3560: "Campos Ammen", - 3561: "La Arboleda Sagrada", - 3562: "Murallas del Fuego Infernal", - 3563: "Ciudadela del Fuego Infernal", - 3564: "El Claro de Ascuas", - 3565: "Refugio Cenarion", - 3566: "Túmulo Lunala", - 3567: "La Maraña de Cápsulas", - 3568: "El Cementerio de Cápsulas", - 3569: "Hoya de la Marea", - 3570: "Punto Escama de Cólera", - 3571: "Aldea Brazolanudo", - 3572: "Bastión Semprepino", - 3573: "Desembarco de Odesyus", - 3574: "Atracadero de Valaar", - 3575: "La Costa de Cieno", - 3576: "Avanzada Azur", - 3577: "Campamento de Geezle", - 3578: "Restos del Arca", - 3579: "Cala del Traidor", - 3580: "Cima Ventosalvaje", - 3581: "Senda Ventosalvaje", - 3582: "Zeth'Gor", - 3583: "Costa de Berilo", - 3584: "Avanzada de Sangre", - 3585: "Bosquespada", - 3586: "La Espiral Vectorial", - 3587: "El Pistón de Distorsión", - 3588: "El Crionúcleo", - 3589: "El Tramo Carmesí", - 3590: "Guarida Escama de Cólera", - 3591: "Ruinas de Loreth'Aran", - 3592: "Nazzivian", - 3593: "Axxarien", - 3594: "Costa Cienonegro", - 3595: "La Poza del Hediondo", - 3596: "El Arrecife Oculto", - 3597: "Paso de Redámbar", - 3598: "Isla Cicatriz de Vermis", - 3599: "Alto de la Garra", - 3600: "Enclave Brazolanudo", - 3601: "Loma Plumira", - 3602: "Encrucijada de Kessel", - 3603: "Campamento de Tel'athion", - 3604: "El Arrecife Sangre Maldita", - 3605: "El Pasado Hyjal", - 3606: "La Cima Hyjal", - 3607: "Caverna Santuario Serpiente", - 3608: "El Reposo del Vindicador", - 3609: "Unused3", - 3610: "Ruinas Filo Ardiente", - 3611: "Avanzada del Clan", - 3612: "Isla Sangre Maldita", - 3613: "Garadar", - 3614: "Lago Son Celeste", - 3615: "El Trono de los Elementos", - 3616: "Ruinas Riecráneos", - 3617: "Colina Mazo de Guerra", - 3618: "Guarida de Gruul", - 3619: "Cresta Auren", - 3620: "Cascadas Auren", - 3621: "Lago Primasol", - 3622: "Puesto Primasol", - 3623: "Desembarco Aeris", - 3624: "Campamento Forja: Miedo", - 3625: "Campamento Forja: Odio", - 3626: "Telaar", - 3627: "Grieta del Viento Norte", - 3628: "Halaa", - 3629: "Grieta del Viento Sur", - 3630: "Oshu'gun", - 3631: "Campos de Espíritus", - 3632: "Shamanar", - 3633: "Tierras Ancestrales", - 3634: "Aldea Junco Alabeado", - 3635: "Unused2", - 3636: "Meseta Elemental", - 3637: "Fortaleza Mata'penas", - 3638: "El Círculo de los Retos", - 3639: "Isla Bruma de Plata", - 3640: "Aldea Dagapantano", - 3641: "Aldea Umbropantano", - 3642: "Aldea Feropantano", - 3643: "Enclave Escamas de Sangre", - 3644: "Telredor", - 3645: "Zabra'jin", - 3646: "Cresta Quagg", - 3647: "La Cañada Emergente", - 3648: "El Lodo Muerto", - 3649: "Esporaggar", - 3650: "Dominios Ango'rosh", - 3651: "Bastión Ango'rosh", - 3652: "Caverna Funggor", - 3653: "Lago Serpiente", - 3654: "El Vaciado", - 3655: "Lago Umbropantano", - 3656: "Lago Luz Pantanosa", - 3657: "Portal del Claro", - 3658: "Lago Espora Volante", - 3659: "La Laguna", - 3660: "Camino de las Espadas", - 3661: "Cañón Dientespada", - 3662: "Cámara del Pueblo", - 3663: "Mansión Derelicta", - 3664: "Cazadora del Sol", - 3665: "Plaza Alalcón", - 3666: "Cuenca Halaani", - 3667: "Ciénaga Talada", - 3668: "Ruinas Boha'mu", - 3669: "El Estadium", - 3670: "El Mirador", - 3671: "Cerro Tábido", - 3672: "Procesión Mag'hari", - 3673: "Safari Nesingwary", - 3674: "Matorral Cenarion", - 3675: "Tuurem", - 3676: "Velo Shienor", - 3677: "Velo Skith", - 3678: "Velo Shalas", - 3679: "Skettis", - 3680: "Valle Vientonegro", - 3681: "Alto Ala de Fuego", - 3682: "Poblado Grangol'var", - 3683: "Bastión Rompepedras", - 3684: "Bastión Allerian", - 3685: "Ruinas Mascahuesos", - 3686: "Velo Lítico", - 3687: "Olembas", - 3688: "Auchindoun", - 3689: "Velo Reskk", - 3690: "Lago Vientonegro", - 3691: "Lago Ere'Noru", - 3692: "Lago Jorune", - 3693: "Montañas Skethyl", - 3694: "Cresta Brumosa", - 3695: "Las Colinas Quebradas", - 3696: "Las Colinas Barrera", - 3697: "El Vertedero de Huesos", - 3698: "Arena de Nagrand", - 3699: "Patio Riecráneos", - 3700: "El Círculo de Sangre", - 3701: "El Suelo de la Arena", - 3702: "Arena Filospada", - 3703: "Ciudad de Shattrath", - 3704: "La Puerta del Pastor", - 3705: "Cuenca Telaari", - 3706: "El Portal Oscuro", - 3707: "Base de la Alianza", - 3708: "Campamento de la Horda", - 3709: "Villa Elfo de la Noche", - 3710: "Nordrassil", - 3711: "Cuenca de Sholazar", - 3712: "Área 52", - 3713: "El Horno de Sangre", - 3714: "Las Salas Arrasadas", - 3715: "La Cámara de Vapor", - 3716: "La Sotiénaga", - 3717: "Recinto de los Esclavos", - 3718: "Avanzada Rata del Pantano", - 3719: "Ruinas Foso Sangrante", - 3720: "Ruinas de las Agujas Gemelas", - 3721: "Las Ruinas Desmoronadas", - 3722: "Forja de Maná Ara", - 3723: "Ruinas Arklon", - 3724: "Cosmotirón", - 3725: "Ruinas de Enkaat", - 3726: "Forja de Maná B'naar", - 3727: "El Campo de Sobras", - 3728: "Los Campos del Vórtice", - 3729: "La Pila", - 3730: "Forja de Maná Coruu", - 3731: "La Falla de la Tempestad", - 3732: "Poblado Kirin'Var", - 3733: "Torre Violeta", - 3734: "Forja de Maná Duro", - 3735: "Meseta del Viento del Vacío", - 3736: "Forja de Maná Ultris", - 3737: "Cresta Celestial", - 3738: "Flecha de la Tormenta", - 3739: "Base Forja: Olvido", - 3740: "Base Forja: Gehenna", - 3741: "Ruinas de Farahlon", - 3742: "Trono de Socrethar", - 3743: "Bastión de la Legión", - 3744: "Aldea Sombraluna", - 3745: "Bastión Martillo Salvaje", - 3746: "La Mano de Gul'dan", - 3747: "Las Fosas Viles", - 3748: "La Forja Muerta", - 3749: "Cisterna Cicatriz Espiral", - 3750: "Alto Cicatriz Espiral", - 3751: "Punta del Fuego Solar", - 3752: "Alto Illidari", - 3753: "Ruinas de Baa'ri", - 3754: "Altar de Sha'tar", - 3755: "La Escalera Maldita", - 3756: "Ruinas de Karabor", - 3757: "Bancal de Ata'mal", - 3758: "Campos del Ala Abisal", - 3759: "Arrecife del Ala Abisal", - 3760: "Las Colinas Barrera", - 3761: "El Paso Elevado", - 3762: "Paso Junco Alabeado", - 3763: "Loma de Zangar", - 3764: "La Cresta del Crepúsculo", - 3765: "Senda Rajaespina", - 3766: "Puerto Orebor", - 3767: "Camino de las Espadas", - 3768: "Cresta Dentada", - 3769: "Bastión Señor del Trueno", - 3770: "Cañón Dientespada", - 3771: "La Arboleda Viviente", - 3772: "Sylvanaar", - 3773: "Bastión Aguja del Filo", - 3774: "Guarida de Gruul", - 3775: "Anillo de Sangre", - 3776: "Avanzada Machacasangre", - 3777: "Campamento Machacasangre", - 3778: "Mina Draenetista", - 3779: "La Llamada de Trogma", - 3780: "Aquelarre Alanegra", - 3781: "Grishnath", - 3782: "Velo Lashh", - 3783: "Velo Vekh", - 3784: "Campamento Forja: Terror", - 3785: "Campamento Forja: Cólera", - 3786: "Ogri'la", - 3787: "Campamento Forja: Inquina", - 3788: "El Paso Bajo", - 3789: "Laberinto de las Sombras", - 3790: "Criptas Auchenai", - 3791: "Salas Sethekk", - 3792: "Tumbas de Maná", - 3793: "Barranco Llama Infernal", - 3794: "Valle de los Huesos", - 3795: "Ruinas Sha'naari", - 3796: "Los Campos Alabeados", - 3797: "Cresta Cielo Estrellado", - 3798: "Garganta Haal'eshi", - 3799: "Cañón de la Muralla", - 3800: "Colina Colmillespinado", - 3801: "Dominios Mag'har", - 3802: "Cresta del Vacío", - 3803: "La Plataforma Abisal", - 3804: "La Avanzadilla de la Legión", - 3805: "Zul'Aman", - 3806: "Caravana de Provisiones", - 3807: "Caída del Atracador Vil", - 3808: "Puesto Cenarion", - 3809: "Muralla Sur", - 3810: "Muralla Norte", - 3811: "Avanzada Gor'gaz", - 3812: "Avanzada Rompeloma", - 3813: "El Camino del Tormento", - 3814: "Caravana de Provisiones del Este", - 3815: "Punta de Expedición", - 3816: "Zepelín Caído", - 3817: "Testeo", - 3818: "Tierras Escamas de Sangre", - 3819: "Enclave Cresta Oscura", - 3820: "Ojo de la Tormenta", - 3821: "Jaula de la Guardiana", - 3822: "Punta Eclipse", - 3823: "Isla de las Tribulaciones", - 3824: "Barranco Machacasangre", - 3825: "Cabo del Dragón", - 3826: "Cañón Faucedaga", - 3827: "Alto Vekhaar", - 3828: "Foresta Ruuan", - 3829: "Velo Ruuan", - 3830: "Bosque del Cuervo", - 3831: "Puerta de la Muerte", - 3832: "Cumbre del Vórtice", - 3833: "Loma Tajo", - 3834: "Cresta de la Locura", - 3835: "Barranco Pluma Polvorienta", - 3836: "Guarida de Magtheridon", - 3837: "Bastión Furia del Sol", - 3838: "Montañas Rompeloma", - 3839: "Armería Abandonada", - 3840: "El Templo Oscuro", - 3841: "Playa Crestanegra", - 3842: "El Castillo de la Tempestad", - 3844: "Aldea Mok'Nathal", - 3845: "El Castillo de la Tempestad", - 3846: "El Arcatraz", - 3847: "El Invernáculo", - 3848: "El Arcatraz", - 3849: "El Mechanar", - 3850: "Piedra Abisal", - 3851: "Puesto de la Tierra Media", - 3852: "Alto de Tuluman", - 3854: "Avanzada del Protectorado", - 3855: "Arena del Anillo de Sangre", - 3856: "Cruce Elrendar", - 3857: "Vado Ammen", - 3858: "Saliente Rajaespina", - 3859: "Lago Silmyr", - 3860: "Claro Raastok", - 3861: "Desfiladero Thalassiano", - 3862: "Garganta Bulliciosa", - 3863: "Landas Tábidas", - 3864: "Alto Bash'ir", - 3865: "Espina de Cristal", - 3866: "Skald", - 3867: "Barranco del Filo", - 3868: "Puente Girolámina", - 3869: "Torre de los Magos", - 3870: "Torre de los Elfos de Sangre", - 3871: "Ruinas Draenei", - 3872: "Ruinas del Atracador Vil", - 3873: "El Terreno de Pruebas", - 3874: "Ecodomo Campolejano", - 3875: "Ecodomo Altocielo", - 3876: "Ecodomo Sutheron", - 3877: "Ecodomo de la Tierra Media", - 3878: "Punto de Escala de El Etereum", - 3879: "Patio de la Capilla", - 3880: "Eje de Acceso Zeon", - 3881: "Mina Trelleum", - 3882: "Punto de Invasión: Destructor", - 3883: "Campamento de Bum", - 3884: "Desfiladero Rompeloma", - 3885: "Cresta Red Abisal", - 3886: "Caravana Derelicta", - 3887: "Caravana de Refugiados", - 3888: "Tumba Umbría", - 3889: "Velo Rhaze", - 3890: "Tumba de las Luces", - 3891: "Colina Carroña", - 3892: "Alcor Tortuoso", - 3893: "Círculo de la Observancia", - 3894: "Tierras Auchenai", - 3895: "Puesto de Vigilancia Cenarion", - 3896: "Alto Aldor", - 3897: "Bancal de la Luz", - 3898: "Grada del Arúspice", - 3899: "Bajo Arrabal", - 3900: "Punto de Invasión: Señor Supremo", - 3901: "Puesto Allerian", - 3902: "Campamento Rompepedras", - 3903: "Peña'mok", - 3904: "Hoya Maldita", - 3905: "Reserva Colmillo Torcido", - 3906: "La Playa de Sangre", - 3907: "Punta Veridiana", - 3908: "Mediavega", - 3909: "El Aprisco Perdido", - 3910: "Bosque Bruma", - 3911: "Orilla Tranquila", - 3912: "Desfiladero Ramadorada", - 3913: "Piedra Rúnica Falithas", - 3914: "Piedra Rúnica Shan'dor", - 3915: "Playa Puentegrato", - 3916: "Bosque Pasto Lunar", - 3917: "Auchindoun", - 3918: "Estación de Toshley", - 3919: "Cresta Canto", - 3920: "Puesto Devastación", - 3921: "Cresta Arklonis", - 3922: "Avanzada Aguja del Filo", - 3923: "Guarida de Gruul", - 3924: "Torre Quiebranorte", - 3925: "Torre Quiebrasur", - 3926: "Llanuras Devastadas", - 3927: "Granja de Oronok", - 3928: "El Altar de Condenación", - 3929: "El Sendero de la Conquista", - 3930: "Campos Eclipsianos", - 3931: "Dominios Aguja del Filo", - 3932: "Campamento Sketh'lon", - 3933: "Ruinas de Sketh'lon", - 3934: "Plaza de la Ciudad", - 3935: "Pasaje del Zahorí", - 3936: "Torre de la Forja Muerta", - 3937: "Vigía de la Escoria", - 3938: "Sagrario de las Estrellas", - 3939: "Fortaleza Faucedraco", - 3940: "La Poza Fétida", - 3941: "Test", - 3942: "Zona de Aterrizaje Razaan", - 3943: "Punto de Invasión: Cataclismo", - 3944: "El Altar de las Sombras", - 3945: "Desfiladero del Ala Abisal", - 3946: "Refugio de Wayne", - 3947: "Las Pozas Escaldantes", - 3948: "Brian and Pat Test", - 3949: "Campos de Magma", - 3950: "Atalaya Carmesí", - 3951: "Soto Eterno", - 3952: "Puente Calavermis", - 3953: "Plataforma Alaescama", - 3954: "Túnel Calavermis", - 3955: "Cuenca del Fuego Infernal", - 3956: "La Escalera Umbría", - 3957: "Avanzada Sha'tari", - 3958: "Campamento Sha'tari", - 3959: "Templo Oscuro", - 3960: "Túmulo de Moledor de Almas", - 3961: "Punta Alapenas", - 3962: "Anillo de Vim'gol", - 3963: "Cresta Espinazo de Dragón", - 3964: "Avanzada de la Guardia del Cielo", - 3965: "Minas del Ala Abisal", - 3966: "Campamento Faucedraco", - 3967: "Ruta Aérea Faucedraco", - 3968: "Ruinas de Lordaeron", - 3969: "Nido de Desgarro Oscuro", - 3970: "Nido de Obsidia", - 3971: "Nido de Insidion", - 3972: "Nido de Alafuria", - 3973: "Alto de los Vientonegro", - 3974: "Velo Harr'ik", - 3975: "Sosiego de Terokk", - 3976: "Velo Ala'rak", - 3977: "Velo Shil'ak Alto", - 3978: "Velo Shil'ak Bajo", - 3979: "El Mar Gélido", - 3980: "Bahía Cubredaga", - 3981: "Valgarde", - 3982: "Poblado Calavermis", - 3983: "Fortaleza de Utgarde", - 3984: "Nafsavar", - 3985: "Cataratas de Ymiron", - 3986: "Risco del Eco", - 3987: "La Isla de las Lanzas", - 3988: "Komawa", - 3989: "Arrecife de Garvan", - 3990: "Cabo Pillastre", - 3991: "Nuevo Agamand", - 3992: "El Antiguo Elevador", - 3993: "Torreta de la Guardia Oeste", - 3994: "Haltorboll", - 3995: "La Playa Rompeolas", - 3996: "Excavación de Baelgun", - 3997: "Avanzada de la Liga de Expedicionarios", - 3998: "Fortaleza de la Guardia Oeste", - 3999: "Las Puertas de Acero", - 4000: "Campo Venganza", - 4001: "Baleheim", - 4002: "Skorn", - 4003: "Fuerte Vildervar", - 4004: "Poblado Presavil", - 4005: "Ruinas de Ivald", - 4006: "Gjalerbron", - 4007: "Tumba de los Reyes Perdidos", - 4008: "Transportador de Shartuul", - 4009: "Campo de Entrenamiento Illidari", - 4010: "Piñón de Barro", - 4018: "Campamento Pezuña Invernal", - 4019: "Tierra de Desarrollo", - 4020: "Cantera Piedra de Poderío", - 4021: "Llanuras Sanguiespora", - 4022: "Gammoth", - 4023: "El Saliente Ámbar", - 4024: "Gelidar", - 4025: "La Falla Oeste", - 4026: "La Escalera de Tránsito", - 4027: "Costa de los Ecos", - 4028: "Litoral Tralladón", - 4029: "Ruinas Tralladón", - 4030: "Costa de los Ídolos", - 4031: "Pal'ea", - 4032: "Fortaleza Denuedo", - 4033: "Poblado Aleta Invernal", - 4034: "La Muralla Boreal", - 4035: "Los Campos de Géiseres", - 4036: "Estación de Bombeo de Palanqueta", - 4037: "Poblado Taunka'le", - 4038: "Cavernas Magnamoth", - 4039: "Cantera Frioescollo", - 4040: "Bahía Aliento de Njord", - 4041: "Kashala", - 4042: "Transborea", - 4043: "Llanuras Anegadas", - 4046: "Puesto Cuernoatroz", - 4047: "Embarcadero de Nat", - 4048: "Encierro Ámbar", - 4049: "Granja de Tabetha", - 4050: "Playa Derelicta", - 4051: "El Claro Helado", - 4052: "El Claro Vibrante", - 4053: "El Claro Retorcido", - 4054: "El Bosque Hendido", - 4055: "Lago Caldemere", - 4056: "Catacumbas de Utgarde", - 4057: "Colina Escudo", - 4058: "Lago Caldros", - 4059: "Isla Caldros", - 4060: "Vega Sangrante", - 4061: "El Paso del Gigante", - 4062: "Campamento de los Boticarios", - 4063: "Torre Lanza Ámbar", - 4064: "Estrecho Devastado", - 4065: "Gjalercorn", - 4066: "Pico Filoescarcha", - 4067: "Torre del Bosque de la Peste", - 4068: "Torre Lanza del Oeste", - 4069: "Torre Lanza del Norte", - 4070: "Costafría", - 4071: "Garganta Susurro", - 4072: "Subzona", - 4073: "Bancal del Invierno", - 4074: "Las Salas del Despertar", - 4075: "Meseta de La Fuente del Sol", - 4076: "Reuse Me 7", - 4077: "Playa de Sorlof", - 4078: "Alto Rajaespina", - 4079: "Paso Filoescarcha", - 4080: "Isla de Quel'Danas", - 4081: "El Cazador del Albor", - 4082: "El Sin'loren", - 4083: "Orgullo de Lunargenta", - 4084: "El Juramento de Sangre", - 4085: "Zona de Escala de Sol Devastado", - 4086: "Sagrario de Tramo del Sol", - 4087: "Puerto de Tramo del Sol", - 4088: "Arsenal de Tramo del Sol", - 4089: "Poblado Estrella del Alba", - 4090: "La Plaza Crepuscular", - 4091: "Costa Branquia Verde", - 4092: "La Cicatriz Muerta", - 4093: "La Forja del Sol", - 4094: "Meseta de La Fuente del Sol", - 4095: "Bancal del Magister", - 4096: "Claytön's WoWEdit Land", - 4097: "Cavernas Aleta Invernal", - 4098: "Bahía Titileo", - 4099: "Refugio Aleta Invernal", - 4100: "La Matanza de Stratholme", - 4101: "Arenas de Nasam", - 4102: "Puerto de Krom", - 4103: "Garfa de Nasam", - 4104: "Cala del Eco", - 4105: "Alto de Berilo", - 4106: "Desembarco de Garrosh", - 4107: "Malecón Grito de Guerra", - 4108: "Pista de Aterrizaje de Palanqueta", - 4109: "Lago Kum'uya", - 4110: "Campos de Lindeallá", - 4111: "Lindeallá", - 4112: "Faro de Lindeallá", - 4113: "Unu'pe", - 4114: "Confín de la Muerte", - 4115: "El Tramo Abandonado", - 4116: "Pozas Escaldantes", - 4117: "Manantiales de Vapor", - 4118: "Talramas", - 4119: "Pozas Purulentas", - 4120: "El Nexo", - 4121: "Escudo de Tránsito", - 4122: "Avanzada Bor'gorok", - 4123: "Magmoth", - 4124: "Los Cubiles de los Moribundos", - 4125: "Ciudad Templo de En'kilah", - 4126: "El Zigurat de los Lamentos", - 4127: "Caravana de Quijacero", - 4128: "Naxxanar", - 4129: "Bastión Grito de Guerra", - 4130: "Llanuras de Nasam", - 4131: "Bancal del Magister", - 4132: "Ruinas de Eldra'nath", - 4133: "Alto Carbonizado", - 4134: "Poza Virulenta", - 4135: "Aguja de Sangre", - 4136: "Aguja de Putrefacción", - 4137: "Aguja de Dolor", - 4138: "Tramo Helado", - 4139: "Plaza del Parhelio", - 4140: "La Cicatriz Muerta", - 4141: "Granja de Torp", - 4142: "Granero Grito de Guerra", - 4143: "Matadero Grito de Guerra", - 4144: "Avanzada de las Granjas Grito de Guerra", - 4145: "Estación de la Punta Oeste", - 4146: "Estación de la Punta Norte", - 4147: "Estación de la Punta Central", - 4148: "Estación de la Punta Sur", - 4149: "Campamento DEHTA", - 4150: "Percha de Kaw", - 4151: "Campo de Refugiados de Viento Oeste", - 4152: "Puerto Moa'ki", - 4153: "Poblado Indu'le", - 4154: "Claro Avalancha", - 4155: "La Media Concha", - 4156: "Aguja de Flujo", - 4157: "Jardines Reposo Lunar", - 4158: "Reposo Estelar", - 4159: "Campamento de la Brigada de los Páramos de Poniente", - 4160: "Bosque Lothalor", - 4161: "Templo del Reposo del Dragón", - 4162: "Cataratas Bruma de Hielo", - 4163: "Poblado Bruma de Hielo", - 4164: "El Foso de Narjun", - 4165: "Martillo de Agmar", - 4166: "Lago Indu'le", - 4167: "Santuario de Dragones Obsidiana", - 4168: "Santuario de Dragones Rubí", - 4169: "Bastión de Fordragón", - 4170: "Vanguardia Kor'kron", - 4171: "La Corte de las Calaveras", - 4172: "Angrathar la Puerta de Cólera", - 4173: "Reposo de Galakrond", - 4174: "La Espiral Maldita", - 4175: "Santuario de Dragones Bronce", - 4176: "El Espejo del Alba", - 4177: "Fortaleza de Hibergarde", - 4178: "Mina de Hibergarde", - 4179: "Santuario de Dragones Esmeralda", - 4180: "Nueva Vega del Amparo", - 4181: "El Tramo del Cruzado", - 4182: "Locura del Pecador", - 4183: "Santuario de Dragones Azur", - 4184: "Senda de los Titanes", - 4185: "La Orilla Olvidada", - 4186: "Rencor Venenoso", - 4187: "Vicio de Cristal", - 4188: "Los Campos de Carroña", - 4189: "Campamento del Embate", - 4190: "Puesto de Thorson", - 4191: "Confianza de la Luz", - 4192: "Caverna Agonía de Escarcha", - 4193: "Punta Escarlata", - 4194: "Jintha'kalar", - 4195: "Caverna Corazón de Hielo", - 4196: "Fortaleza de Drak'Tharon", - 4197: "Conquista del Invierno", - 4198: "Atolón de Kili'ua", - 4199: "Arroyoplata", - 4200: "Corazón de Vordrassil", - 4201: "Lágrimas de Vordrassil", - 4202: "Lágrimas de Vordrassil", - 4203: "Extremidad de Vordrassil", - 4204: "Refugio Pino Ámbar", - 4205: "Poblado Solsticio", - 4206: "Bastión de la Conquista", - 4207: "Runavold", - 4208: "Manantial de Granito", - 4209: "Zeb'Halak", - 4210: "Fortaleza de Drak'Tharon", - 4211: "Campamento Oneqwah", - 4212: "Playa Viento Este", - 4213: "Riscos Quebrados", - 4214: "Colinas Pedrusco", - 4215: "Santuario Colmillo Iracundo", - 4216: "Ruinas de Drakil'jin", - 4217: "Aserradero Río Negro", - 4218: "Santuario Sangre de Corazón", - 4219: "Mina Piedrahueca", - 4220: "Dun Argol", - 4221: "Thor Modan", - 4222: "Aserradero Cielo Azul", - 4223: "Fauces de Neltharion", - 4224: "El Pináculo Salobre", - 4225: "Orilla Resplandeciente", - 4226: "Iskaal", - 4227: "Caída del Dragón", - 4228: "El Oculus", - 4229: "Altozano del Prospector", - 4230: "Altos Viento Helado", - 4231: "Puesto de Venta de Madera Roja", - 4232: "Paso Venganza", - 4233: "Tramo del Alba", - 4234: "Naxxramas", - 4235: "Puesto de Venta de Duramen", - 4236: "Puesto de Venta de Siempreverde", - 4237: "Puesto del Altozano de Píceas", - 4238: "Puesto de Venta de Pino Blanco", - 4239: "Puesto de la Alameda", - 4240: "Puesto Fronterizo del Bosque", - 4241: "Cumbres Fantasmales", - 4242: "Bahía Ventura", - 4243: "Cripta de Hibergarde", - 4244: "Isla Luna de Sangre", - 4245: "Torre de Colmillo Oscuro", - 4246: "Mausoleo de Hibergarde", - 4247: "Cubil Aúllaocaso", - 4248: "El Foso de la Conquista", - 4249: "La Senda de Hierro", - 4250: "Ruinas de Tethys", - 4251: "Colinas de Arroyoplata", - 4252: "Riscos Quebrados", - 4253: "Frente de la Séptima Legión", - 4254: "Baldío del Dragón", - 4255: "Ruinas de Drak'Zin", - 4256: "Lago Drak'Mar", - 4257: "Afluente del Espinazo del Dragón", - 4258: "El Mar del Norte", - 4259: "Drak'ural", - 4260: "Campamento de Thorvald", - 4261: "Puesto del Filo Fantasmal", - 4262: "Puesto Fresno", - 4263: "Emboscada de Lydell", - 4264: "Cámaras de Piedra", - 4265: "El Nexo", - 4266: "Campamento de Harkor", - 4267: "Paso de Vordrassil", - 4268: "Campamento de Ruuna", - 4269: "Santuario de Escamas", - 4270: "Pasaje de Drak'atal", - 4271: "Pináculo de Utgarde", - 4272: "Cámaras de Relámpagos", - 4273: "Ulduar", - 4275: "El Confín Argenta", - 4276: "Altar de Sseratus", - 4277: "Azjol-Nerub", - 4278: "Campos Drak'Sotra", - 4279: "Drak'Sotra", - 4280: "Drak'Agal", - 4281: "Acherus: El Bastión de Ébano", - 4282: "La Avalancha", - 4283: "Las Tierras Perdidas", - 4284: "Campamento Base de Nesingwary", - 4285: "El Flujo de la Brecha del Mar", - 4286: "Los Huesos de Nozronn", - 4287: "Bastión de Kartak", - 4288: "Retiro Pavesa", - 4289: "La Senda del Guardián de Vida", - 4290: "Corazón del Río", - 4291: "Canope de Hablalluvia", - 4292: "Colina Corazón Frenético", - 4293: "Manglar Silvestre", - 4294: "Heb'Valok", - 4295: "El Fragmento Hendido", - 4296: "El Pilar Sangrevida", - 4297: "Poblado Caminamoho", - 4298: "Tierras de la Peste: El Enclave Escarlata", - 4299: "Kolramas", - 4300: "Puerta", - 4302: "El Pilar del Trecho Celestial", - 4303: "Claro Callonudillo", - 4304: "Enjambre Zafiro", - 4306: "Refugio Susurraneblina", - 4307: "El Pilar Iluminado", - 4308: "Campamento Lanzonato", - 4309: "Fortaleza de Drak'Tharon", - 4310: "Zeramas", - 4311: "Relicario de Agonía", - 4312: "Puesto de Vigilancia de Ébano", - 4313: "Fin de Thrym", - 4314: "Voltarus", - 4315: "Relicario de Dolor", - 4316: "Guarida de Garrafuria", - 4317: "Brecha de la Luz", - 4318: "Pozas de Zha'Jin", - 4319: "Zim'Abwa", - 4320: "Anfiteatro de la Angustia", - 4321: "Altar de Rhunok", - 4322: "Altar de Har'koa", - 4323: "Zim'Torga", - 4324: "Pozas de Jin'Alai", - 4325: "Altar de Quetz'lun", - 4326: "Heb'Drakkar", - 4327: "Drak'Mabwa", - 4328: "Zim'Rhuk", - 4329: "Altar de Mam'toth", - 4342: "Acherus: El Bastión de Ébano", - 4343: "Nuevo Avalon", - 4344: "Campos de Nuevo Avalon", - 4345: "Huerto de Nuevo Avalon", - 4346: "Concejo de Nuevo Avalon", - 4347: "Villa Refugio", - 4348: "Granjas de Villa Refugio", - 4349: "Serrería de Villa Refugio", - 4350: "Establos de Villa Refugio", - 4351: "El Bastión Escarlata", - 4352: "Capilla de la Llama Carmesí", - 4353: "Torre de la Punta de la Luz", - 4354: "Punta de la Luz", - 4355: "Cripta de los Recuerdos", - 4356: "Brecha de la Muerte", - 4357: "El Claro Ponzoñoso", - 4358: "Mano de Tyr", - 4359: "Puerto del Rey", - 4360: "Mirador Escarlata", - 4361: "Capilla de la Esperanza de la Luz", - 4362: "Locura del Pecador", - 4363: "Cicatriz Pestilente", - 4364: "Molino Cejifrente", - 4365: "Mina de Villa Refugio", - 4366: "El Cubil de Ursoc", - 4367: "La Línea de Añublo", - 4368: "Los Campos de Huesos", - 4369: "Avanzada de Dorian", - 4371: "Cráter de Mam'toth", - 4372: "Bastión de Zol'Maz", - 4373: "Zol'Heb", - 4374: "Lago de Garrafuria", - 4375: "Gundrak", - 4376: "El Matorral Silvestre", - 4377: "Forja de Nuevo Avalon", - 4378: "Arena de Dalaran", - 4379: "Valgarde", - 4380: "Posada de la Guardia Oeste", - 4381: "Puerta", - 4382: "El Bancal del Creador", - 4383: "Pista del Lago", - 4384: "Playa de los Ancestros", - 4385: "Lago Olamarga", - 4386: "Rápidos de Hablalluvia", - 4387: "Río Corazón Frenético", - 4388: "Río Conquista del Invierno", - 4389: "El Pilar Toquesol", - 4390: "Brecha Gélida", - 4391: "Excavación de Timomueca", - 4392: "La Plataforma del Tormentoso", - 4393: "Campamento Mano de la Muerte", - 4394: "Taberna Escarlata", - 4395: "Dalaran", - 4396: "Puesto Boquilla Oxidada", - 4399: "Mina de Lindeallá", - 4400: "El Pilar Musgoluz", - 4401: "Alto de Saragosa", - 4402: "Elevador de Venganza", - 4403: "Avanzada Balejar", - 4404: "Posada de Nuevo Agamand", - 4405: "Pasaje de los Malignos Perdidos", - 4406: "El Círculo del Valor", - 4407: "Cámara de los Lobo Gélido", - 4408: "Cámara de los Pico Tormenta", - 4411: "Puerto de Ventormenta", - 4412: "El Mirador de los Creadores", - 4413: "El Pedestal de los Creadores", - 4414: "Torre Escarlata", - 4415: "El Bastión Violeta", - 4416: "Gundrak", - 4417: "Puerto del Embate", - 4418: "K3", - 4419: "Colinas Velonieve", - 4420: "Bancal Velonieve", - 4421: "Garm", - 4422: "Poblado Brunnhildar", - 4423: "Poblado Sifreldar", - 4424: "Valkyrion", - 4425: "La Mina Desolada", - 4426: "Río del Aliento de Bor", - 4427: "Vanguardia Argenta", - 4428: "Fuerte Escarcha", - 4429: "Lugar del accidente de Grom'arsh", - 4430: "Templo de las Tormentas", - 4431: "Motor de los Creadores", - 4432: "Las Estepas Inferiores", - 4433: "Cumbres Espinazo de Dragón", - 4434: "Nidavelir", - 4435: "Cuna de Narvir", - 4436: "Llanuras Ventisquero", - 4437: "Valle de los Viejos Inviernos", - 4438: "Dun Niffelem", - 4439: "Lago Campo de Escarcha", - 4440: "Truenotoño", - 4441: "Campamento Tunka'lo", - 4442: "Campamento base de Brann", - 4443: "Puerta de los Ecos", - 4444: "Llanura de los Ecos", - 4445: "Ulduar", - 4446: "Bancal de los Creadores", - 4447: "Puerta de los Rayos", - 4448: "Senda de los Titanes", - 4449: "Uldis", - 4450: "Acuerdo de Loken", - 4451: "Caída de Bor", - 4452: "Aliento de Bor", - 4453: "Paso de Rohemdal", - 4454: "La Fundición de la Tormenta", - 4455: "Caverna Hibernal", - 4456: "Morada Runavold", - 4457: "Reposo de Torseg", - 4458: "Campo de Minas Encajebujía", - 4459: "Locura de Ricket", - 4460: "Pesadilla de Garm", - 4461: "Alto de Garm", - 4462: "Caverna Red de Cristal", - 4463: "Templo de la Vida", - 4464: "Templo del Orden", - 4465: "Templo del Invierno", - 4466: "Templo de la Invención", - 4467: "Ascenso de la Muerte", - 4468: "Los Campos Muertos", - 4469: "Óbito de Dargath", - 4470: "La Hondonada Oculta", - 4471: "El Maravilloso Mundo de Bernau", - 4472: "Hondonada Puño Helado", - 4473: "La Tumba Gélida", - 4474: "Las Playas Gemelas", - 4475: "Guarida de Zim'bo", - 4476: "Campamento Abandonado", - 4477: "La Cámara de las Sombras", - 4478: "Pasaje Viento Helado", - 4479: "Lago Aliento Invernal", - 4480: "El Mirador Olvidado", - 4481: "Pasaje de Jintha'kalar", - 4482: "Pasarela de Arriga", - 4483: "El Pasaje Perdido", - 4484: "Refugio de Pedruscón", - 4485: "La Biblioteca del Inventor", - 4486: "La Mina Gélida", - 4487: "Hondonada Témpano Gélido", - 4488: "La Hondonada Aullante", - 4489: "Puesto de Avanzada de los Cruzados", - 4490: "Crestormenta", - 4491: "Campamento Cascahueso", - 4492: "Sala de Ufrang", - 4493: "El Sagrario Obsidiana", - 4494: "Ahn'kahet: El Antiguo Reino", - 4495: "Yunque de Fjorn", - 4496: "Jotunheim", - 4497: "Cornisa Salvaje", - 4498: "Salas de los Ancestros", - 4499: "La Poza Contagiada", - 4500: "El Ojo de la Eternidad", - 4501: "Vanguardia Argenta", - 4502: "Taller de Mimir", - 4503: "Presa del Muro de Hierro", - 4504: "Valle de los Ecos", - 4505: "La Brecha", - 4506: "Ciudad de la Plaga", - 4507: "El Frente Roto", - 4508: "Mord'rethar: La Puerta de la Muerte", - 4509: "El Bombardeo", - 4510: "Aldur'thar: La Puerta de la Desolación", - 4511: "El Rompecielos", - 4512: "Martillo de Orgrim", - 4513: "Ymirheim", - 4514: "Minas de Saronita", - 4515: "La Conflagración", - 4516: "Fortificación del Muro de Hierro", - 4517: "Cantera Llorosa", - 4518: "Corp'rethar: La Puerta del Horror", - 4519: "El Patio de los Huesos", - 4520: "Malykriss: El Bastión Inmundo", - 4521: "Catedral de la Oscuridad", - 4522: "Ciudadela de la Corona de Hielo", - 4523: "Glaciar Corona de Hielo", - 4524: "Valhalas", - 4525: "Las Cámaras Subterráneas", - 4526: "Poblado Njorndar", - 4527: "Fortaleza de Balargarde", - 4528: "Fortaleza de Kul'galar", - 4529: "La Catedral Carmesí", - 4530: "Sagrario de Reanimación", - 4531: "La Factoría de Carne", - 4532: "Posada de Campo Venganza", - 4533: "La Caída de Sindragosa", - 4534: "Mina de Vildervar", - 4535: "El Foso del Colmillo", - 4536: "Caverna Aúllaescarcha", - 4537: "El Valle de la Esperanza Perdida", - 4538: "El Anillo Sumergido", - 4539: "El Templo Quebrado", - 4540: "El Valle de los Héroes Caídos", - 4541: "Enfermería de la Vanguardia", - 4542: "Sala del Creador", - 4543: "Templo de Sabiduría", - 4544: "Brecha de la Muerte", - 4545: "Mina Abandonada", - 4546: "Ruinas de El Enclave Escarlata", - 4547: "Cámaras de Piedra", - 4548: "Cámaras de Relámpagos", - 4549: "El Gran Árbol", - 4550: "El Espejo del Crepúsculo", - 4551: "El Riachuelo Crepuscular", - 4552: "La Corriente Decrépita", - 4553: "El Bosque Desolado", - 4554: "Ruinas de Shandaral", - 4555: "El Frente Azur", - 4556: "El Confín Violeta", - 4557: "El Matorral Desatado", - 4558: "Dominio de los Atracasol", - 4559: "Mirador Brisaveloz", - 4560: "Los Bajos Fondos", - 4564: "Alto de Krasus", - 4567: "El Bastión Violeta", - 4568: "El Manto de la Noche", - 4569: "Tubería de Salida de las Cloacas", - 4570: "Círculo de Voluntades", - 4571: "Sala de la Bandera de Brisa de Plata", - 4572: "Sala de la Bandera Grito de Guerra", - 4575: "Fortaleza de Conquista del Invierno", - 4576: "Puente Central", - 4577: "Puente Oriental", - 4578: "Puente Occidental", - 4579: "Dubra'Jin", - 4580: "Pináculo de los Cruzados", - 4581: "Torre de la Guardia en Llamas", - 4582: "Torre Filoinvierno", - 4583: "Torre de la Vista de las Sombras", - 4584: "El Caldero de Llamas", - 4585: "Cascadas Glaciales", - 4586: "Riscos Ventosos", - 4587: "El Bosque de las Sombras", - 4588: "La Guardia Negra", - 4589: "El Cenagal Escalofrío", - 4590: "Las Estepas de la Vida", - 4591: "Vigía Silencioso", - 4592: "Guarida de Gimorak", - 4593: "El Foso de los Mefistos", - 4594: "Cumbre Marca de Guerra", - 4595: "Cámara de los Horrores", - 4596: "El Círculo de Sufrimiento", - 4597: "Alto del Sufrimiento", - 4598: "Alto de Krasus", - 4599: "Tubería de Salida de las Cloacas", - 4601: "Isla de Dalaran", - 4602: "Fuerza Interior", - 4603: "La Cámara de Archavon", - 4604: "Puerta del Sol Rojo", - 4605: "Puerta del Zafiro Azul", - 4606: "Puerta de la Esmeralda Verde", - 4607: "Puerta de la Amatista Púrpura", - 4608: "Puerta de la Luna Amarilla", - 4609: "Patio de los Ancestros", - 4610: "Playa de Desembarco", - 4611: "Taller Chispa Occidental", - 4612: "Taller Chispa Oriental", - 4613: "Ciudad de Dalaran", - 4614: "Aguja de La Ciudadela Violeta", - 4615: "Naz'anak: Las Profundidades Olvidadas", - 4616: "Santuario Atracasol", - 4617: "Elevador", - 4618: "Monumento de Antonidas", - 4619: "La Ciudadela Violeta", - 4620: "Mercado de Magos", - 4621: "UNUSED", - 4622: "Puesto de Avanzada de la Primera Legión", - 4623: "Cámara de los Reyes Conquistados", - 4624: "Bancal Infecto", - 4625: "El Altar Profanado", - 4626: "Ciénaga Bruñida", - 4627: "Templo Caído de Ahn'kahet", - 4628: "Sala de Vínculos", - 4629: "Corazón del Invierno", - 4630: "El Mar del Norte", - 4631: "El Nido de la Madre de Linaje", - 4632: "Rocas Flotantes de Dalaran", - 4633: "Cercado de Raptores", - 4635: "Fortaleza de Drak'Tharon", - 4636: "El Paso Ponzoñoso", - 4637: "Reposo de Vargoth", - 4638: "Balcón de la Ciudadela Violeta", - 4639: "Anillo de Discrepancia", - 4640: "Anillo de Aceleración", - 4641: "Anillo de Transmutación", - 4642: "Anillo de Alineación", - 4644: "Refugio de Orendil", - 4645: "Campamento Brazanegra", - 4646: "Lago Fresno", - 4647: "Isla de Ruuzel", - 4648: "Campamento de Refugiados de Auberdine", - 4650: "Explanada de Hierro", - 4652: "Campo de Formación", - 4653: "Nidal de Tajoescama", - 4654: "La Forja Colosal", - 4655: "La Chatarrería", - 4656: "El Invernadero de Vida", - 4657: "El Archivum", - 4658: "Campos del Torneo Argenta", - 4659: "Lor'danel", - 4660: "Ruinas de Lornesta", - 4661: "Cuenca Fonroca", - 4662: "Campamento de Guerra Rompelanzas", - 4663: "Fauces del Vacío", - 4664: "Valle Rompelanzas", - 4665: "Campamento Base de la Expedición", - 4666: "Pabellón Atracasol", - 4667: "Pabellón de El Pacto de Plata", - 4668: "La Residencia Cooper", - 4669: "La Liza de los Campeones", - 4670: "La Liza de los Aspirantes", - 4671: "La Liza de los Valerosos Argenta", - 4672: "La Liza de los Valerosos de la Alianza", - 4673: "La Liza de los Valerosos de la Horda", - 4674: "Pabellón Argenta", - 4675: "Arboleda Marchita", - 4676: "Pabellón Atracasol", - 4677: "Pabellón de El Pacto de Plata", - 4679: "La Caverna Abandonada", - 4688: "claytonio test area", - 4690: "Pico del Trueno", - 4691: "Avanzada Grito Infernal", - 4692: "Reposo de Quel'Delar", - 4693: "Mina del Hachazo", - 4694: "Bastión Piel de Cardo", - 4695: "El Ojo del Vórtice", - 4696: "Campamento del Bosque Negro", - 4697: "La Playa Llameante", - 4698: "Cubil Lunadón", - 4702: "Paso Rompelanzas", - 4705: "Torre de la Algaba", - 4706: "Ruinas de Gilneas", - 4707: "Las Islas Perdidas", - 4708: "Caverna Rompetierra", - 4709: "Los Baldíos del Sur", - 4710: "Isla de la Conquista", - 4711: "Aguja del Polvo Estelar", - 4712: "Poza de la Luna de Purificación", - 4713: "Cabaña de Krolg", - 4714: "Gilneas", - 4715: "La Central Secreta", - 4716: "Campamento de Bolyun", - 4717: "Casa de Edune", - 4718: "Cobertizo Primigenio", - 4719: "Familiares", - 4720: "Las Islas Perdidas", - 4721: "Costa del Naufragio", - 4722: "Prueba del Cruzado", - 4723: "Prueba del Campeón", - 4724: "Descenso del Túmulo de Ban'ethil", - 4725: "Los Cabos", - 4726: "Puerto Quilla", - 4727: "Reposo de Aderic", - 4728: "Cabos del Norte", - 4729: "Bosque de la Puerta del Norte", - 4730: "El Monte Negro", - 4731: "Poblado Valletormenta", - 4732: "Mina Piedra Ígnea", - 4733: "Faro Velavento", - 4734: "Aldea Piedra Ígnea", - 4735: "Brecha de la Puerta del Norte", - 4736: "La Dentellada", - 4737: "Kezan", - 4738: "Fauce Abisal", - 4739: "Plaza Tejerruna", - 4740: "El Enclave de Plata", - 4741: "Tierra de Nadie de Isla de la Conquista", - 4742: "Desembarco de Hrothgar", - 4743: "Avanzada del Portavoz de la Muerte", - 4744: "Cantera de la Ladera", - 4745: "Puerta Trasera de Orgrimmar", - 4747: "Taller", - 4748: "Cantera", - 4749: "Muelles", - 4750: "Hangar", - 4751: "Refinería", - 4752: "Fortaleza de la Horda", - 4753: "Fortaleza de la Alianza", - 4755: "Ciudad de Gilneas", - 4756: "Plaza de los Mercaderes", - 4757: "Distrito Militar", - 4758: "Patio de Cringrís", - 4759: "Barrio de la Catedral", - 4760: "La Travesía del Atracamar", - 4761: "Catedral del Alba", - 4762: "Prisión Guardapétrea", - 4763: "Transporte: Nave de Guerra de la Alianza", - 4764: "Transporte: Nave de Guerra de la Horda", - 4765: "Central SCK", - 4766: "Kaja'mina", - 4767: "Puerto Pantoque", - 4768: "Chalet de Gallywix", - 4769: "Desembarco de Hrothgar", - 4770: "Primer Banco de Kezan", - 4771: "La Barriada", - 4772: "El Escoplo", - 4773: "Calle del Timo", - 4774: "Gilneas", - 4778: "Caverna de Kaja'mita", - 4779: "Campamento Base de la Horda", - 4780: "El Valle Atroz", - 4781: "Mirador Salvaje", - 4782: "La Cañada Salvaje", - 4783: "Desembarco de la Alianza", - 4784: "Rastro Venganza", - 4785: "Mare Magnum", - 4786: "Refugio del Ocaso", - 4787: "Lugar del Accidente de la Diligencia", - 4788: "Tramo de la Tempestad", - 4789: "La Muralla de Cringrís", - 4790: "Río de la Puerta del Norte", - 4791: "Pantano Bosque del Pedrisco", - 4792: "Hacienda de Allen", - 4793: "Hacienda de Hammond", - 4794: "Cubil de Koroth", - 4795: "Costa Filozante", - 4796: "Costa Filozante", - 4797: "Territorio Magram", - 4798: "Campo de Thargad", - 4799: "Prueba del Fuego", - 4800: "Prueba de las Sombras", - 4801: "Pináculo Arcano", - 4802: "Furia de Draka", - 4803: "Puesto de Furien", - 4804: "Tierras Vírgenes Cenarion", - 4805: "Claro de Karnum", - 4806: "Cabaña de Wahl", - 4807: "Huerto de Crowley", - 4808: "Caladero Hayward", - 4809: "La Forja de Almas", - 4810: "Puerta de la Reina", - 4811: "Puerta del Rey", - 4812: "Ciudadela de la Corona de Hielo", - 4813: "Foso de Saron", - 4814: "Cumbre Cebellina", - 4815: "Bosque Kelp'thar", - 4816: "Estrecho Temible", - 4817: "Mansión de Cringrís", - 4818: "Tierras del Establo de Crowley", - 4819: "La Antigua Arboleda", - 4820: "Cámaras de Reflexión", - 4821: "Muelle Pantoque", - 4822: "Campo Kajaro", - 4823: "Mares del Sur", - 4824: "Palacete de Gallywix", - 4825: "Intercambiador de la Cohetepista del Norte", - 4826: "Acantilados de la Tormenta", - 4827: "Acantilados de la Tormenta", - 4828: "Terminal de la Cohetepista del Sur", - 4829: "El Laboratorio Secreto", - 4830: "Intercambiador de la Cohetepista de Orgrimmar", - 4832: "Transporte: Nave de Guerra de la Alianza", - 4833: "Transporte: Nave de Guerra de la Horda", - 4834: "Ruinas de Nordressa", - 4835: "Campamento Rasguño de Sol", - 4836: "Paso del Espolón", - 4837: "Campamento de Barbablanca", - 4838: "Yate de Gallywix", - 4839: "Colina de Vistalonga", - 4840: "La Tubería", - 4841: "Tal'doren", - 4842: "Avanzada de la Caballería", - 4843: "El Alto del Honor", - 4844: "Cerro del Cazador", - 4845: "La Hojarasca", - 4846: "Campamento Una'fe", - 4847: "Fuerte del Norte", - 4848: "Expedición de Teegan", - 4849: "Mando de Vanguardia", - 4850: "Campos de Sangre", - 4851: "Ruinas de Taurajo", - 4852: "La Gran Puerta", - 4853: "Bastión de la Desolación", - 4854: "Fuerte Triunfo", - 4855: "Marca de Guerra", - 4856: "Bael Modan", - 4857: "Punta Piedra de Fuego", - 4858: "Guarida de Rygna", - 4859: "El Trono Helado", - 4860: "Campamento Hojarasca", - 4861: "El Brote", - 4862: "Las Cámaras Heladas", - 4863: "Costa Garrasangre", - 4864: "Caída de Zalazane", - 4865: "Campo de Entrenamiento Lanza Negra", - 4866: "Bastión Lanza Negra", - 4867: "Cala Escama Maliciosa", - 4868: "Molino Sotogrande", - 4869: "Cabaña de la Espesura", - 4870: "Lugar del Desembarco", - 4871: "Ciudad de Bolsillo", - 4872: "Fortaleza de Bael'dun", - 4873: "Alto de Raptores", - 4874: "Quebrada del Llanto", - 4875: "Isla Lanza Negra", - 4876: "Ruinas de Vashj'elan", - 4877: "Poza Emergente Vashj'elan", - 4878: "El Gruñospina", - 4879: "El Cercado de Jabaguerreros", - 4880: "La Guarida de Mantospina", - 4881: "Paso del Espolón", - 4882: "Alto de Marshal", - 4883: "Pilote Musgoso", - 4884: "Cuenca de los Gritos", - 4885: "Los Jardines Turbados", - 4886: "Poblado Oomlot", - 4887: "Paraje Pesadilla", - 4888: "Paraje Pesadilla", - 4889: "La Guarida de la Reina de Escarcha", - 4890: "Laboratorio Horrores y Risas Alquímicas de Putricidio", - 4891: "El Sagrario de Sangre", - 4892: "La Sala Carmesí", - 4893: "La Guarida de la Reina de Escarcha", - 4894: "Laboratorio Horrores y Risas Alquímicas de Putricidio", - 4895: "La Sala Carmesí", - 4896: "El Trono Helado", - 4897: "El Sagrario de Sangre", - 4898: "Agonía de Escarcha", - 4899: "Pico Perdido", - 4900: "Oostan Nord", - 4901: "Oostan Sor", - 4902: "Oostan Ost", - 4903: "Oostan", - 4904: "El Trayecto Oscuro", - 4905: "Dominio del Señor de la Plaga", - 4906: "El Trono de las Sombras", - 4908: "El Pasaje Oculto", - 4909: "Caldera Perdida", - 4910: "Agonía de Escarcha", - 4911: "Guarida de Volcanoth", - 4912: "Atalaya del Jefe de Guerra", - 4913: "Caverna Escama Maliciosa", - 4914: "Costa Tranquila", - 4915: "Campamento Pudrecerebro", - 4916: "Dominios de Calston", - 4917: "Acantilado Escarlata", - 4918: "Campamento Escarlata", - 4919: "Apeadero del Ocelo de la Muerte", - 4920: "Senda del Cruzado", - 4921: "Torre de vigilancia Escarlata", - 4922: "Tierras Altas Crepusculares", - 4923: "Cascadas del Cielo", - 4924: "Mina de Trabajos Forzados de Gallywix", - 4925: "Morada de Thunk", - 4926: "Cavernas Roca Negra", - 4927: "El Redil", - 4928: "Desfiladero del Espolón", - 4929: "Bastión Cortaviento", - 4930: "Cuenca de las Tejedoras", - 4931: "Ruinas de Eldre'thar", - 4932: "La Hondura Insondable", - 4933: "Fortaleza Krom'gar", - 4934: "Los Fangados", - 4935: "Puesto Mirkfallon", - 4936: "Puesto Caminarrisco", - 4937: "Mirador Thal'darah", - 4938: "Valle Marca de Guerra", - 4939: "Cañada del Vigiaconfines", - 4940: "Campamento Base de la Expedición del Fuerte del Norte", - 4941: "Campos Desenterrados", - 4942: "Altos Cortaviento", - 4943: "Paso de la Daga", - 4944: "Arboleda de Thal'darah", - 4945: "Cámaras de los Orígenes", - 4946: "Excavación de Bael Modan", - 4947: "Patrulla de Trenzado", - 4948: "Filón Catacroquer", - 4949: "Punta de Lanza", - 4950: "Grim Batol", - 4951: "Almacén de Madera Pantoque", - 4952: "Plataforma Petrolera SCK", - 4953: "Bosque Kelp'thar", - 4954: "Muelles de Gallywix", - 4955: "Las Costas Extintas", - 4956: "Los Pozos de Esclavos", - 4958: "Tierras Altas Verdes", - 4961: "Zanja de Marea Argenta", - 4962: "Nespirah", - 4963: "Barranco Brillohondo", - 4966: "Cresta de Biel'aran", - 4967: "Ruinas de Vashj'ir", - 4968: "Bancal de Quel'Dormir", - 4969: "Estela Tranquila", - 4970: "Reposo de la Legión", - 4971: "L'ghorek", - 4972: "Celosía Termal", - 4973: "La Dominancia", - 4974: "Cañón Sondaluz", - 4975: "Caverna Tenebrosa", - 4976: "Cala Brechascura", - 4977: "Fin de Korthun", - 4978: "Senda del Lobo", - 4979: "Santuario de Goldrinn", - 4980: "Fauces de Lycanthoth", - 4981: "Cuenca Furia del Sur", - 4982: "El Bloqueo Dranosh'ar", - 4983: "Cruce Crepuscular", - 4984: "La Vereda Crepuscular", - 4985: "La Carretera", - 4986: "Punta Vendetta", - 4987: "El Sagrario Rubí", - 4988: "Casucha de Gar'gol", - 4989: "Arboleda de Aessina", - 4990: "Santuario de Aviana", - 4991: "Garganta Negro Rumor", - 4992: "Ágora", - 4993: "Paso Negro Rumor", - 4994: "La Forja de las Súplicas", - 4995: "Trono de los Elegidos", - 4996: "Vigilia de la Fatalidad", - 4998: "Puertas de Sothann", - 4999: "Saliente de Relámpagos", - 5000: "Río Verrall", - 5002: "Campamento Ataya", - 5003: "Bastión Quebrantarrocas", - 5004: "Trono de las Mareas", - 5005: "Cuenca de Marea Argenta", - 5006: "Caverna Luzúmida", - 5007: "Ruinas del Templo Thelserai", - 5008: "Gruta Bruñida", - 5009: "Rama Sombría", - 5010: "Nueva Thalanaar", - 5011: "Cima Tramo Oeste", - 5012: "El Cúter Salobre", - 5013: "Penitencia de Cuerno Negro", - 5014: "El Averno", - 5015: "Pesar de Leyara", - 5016: "Arboleda Sibilante", - 5017: "La Llanura Agostada", - 5018: "Lago Cinéreo", - 5019: "Santuario de Malorne", - 5020: "La Estela Ardiente", - 5021: "La Puerta de Fuego", - 5022: "Límite del Mundo", - 5023: "La Cumbre Esmeralda", - 5024: "Reposo de la Soñadora", - 5025: "El Piélago de Sal", - 5026: "Excavación Oximelena", - 5027: "Cumbres Pezuña Quebrada", - 5028: "Sargazo de los Mares del Sur", - 5029: "La Guarida del Almirante", - 5030: "Confín del Bajío", - 5031: "Cumbres Gemelas", - 5032: "Túmulos de Hyjal", - 5033: "Puesto de Mando Crepuscular", - 5034: "Uldum", - 5035: "La Cumbre del Vórtice", - 5036: "Ruinas Niebla Negra", - 5037: "Lago Edunel", - 5038: "Nordrassil", - 5039: "Aguja de Sulfuron", - 5040: "El Trono de la Llama", - 5041: "Barcódromo de Fizzle y Pozzik", - 5042: "Infralar", - 5044: "El Bosque Corrupto", - 5045: "Alto del Ascendiente", - 5046: "Abismo del Espejismo", - 5047: "Brecha Abisal", - 5048: "Ruinas del Circuito", - 5049: "Bar del Barcódromo", - 5050: "El Criadero", - 5051: "Tumba del Navegante", - 5052: "Destino de la Legión", - 5053: "Saliente de Guraturdido", - 5054: "Arrecife de Huesos", - 5055: "Fosa de Bocados", - 5056: "La Espiral Inmortal", - 5057: "Cicatriz del Contrabandista", - 5058: "Gruta Brumadensa", - 5059: "El Encierro", - 5060: "Estación Chispacero", - 5061: "Estación Chispacero", - 5062: "Avanzada del Contrabandista", - 5063: "Campo de Reclutamiento Machacaduna", - 5064: "El Yunque de la Llama", - 5065: "Torre de Estulan", - 5066: "Punta Alblanco", - 5067: "Aguja Interior", - 5068: "Tierra de Pattymack", - 5070: "Excavación de Budd", - 5071: "Lamento de Gorrok", - 5072: "Bastión Plumaluna", - 5073: "Posada de Merobarbecho", - 5074: "Peritaje de Tallalosas", - 5075: "Refugio de Tirth", - 5076: "Campamento de Suministros de Nethergarde", - 5077: "Costa Quebrada", - 5078: "Las Costas Rojas", - 5079: "Aldea Pozarroca", - 5080: "Fosos de lucha de La Playa de Sangre", - 5081: "Santuario de La Playa de Sangre", - 5082: "Cueva de La Playa de Sangre", - 5083: "Excursión Velosolar", - 5084: "Mechasur", - 5085: "Maldraz", - 5086: "Horno Machacamiedo", - 5087: "Nidal de Sethria", - 5088: "El Núcleo Pétreo", - 5089: "Jardines de Quel'Dormir", - 5090: "Bancal de Nar'shola", - 5092: "Zahúrda Rajacieno", - 5093: "Posada del Campamento Base de la Expedición del Fuerte del Norte", - 5094: "Descenso de Alanegra", - 5095: "Tol Barad", - 5096: "Mar Adusto", - 5097: "Frente de Peloescarcha", - 5098: "Refugio Peloescarcha", - 5099: "El Crisol", - 5100: "Alto del Promontorio", - 5101: "Cresta de Aleta Profunda", - 5102: "Profundidades de Pesadilla", - 5103: "La Sima Escaldante", - 5104: "La Concha Subterránea", - 5105: "Tenazario X2", - 5106: "El Verne", - 5107: "Frente de Sira'kess", - 5108: "La Batalla por Gilneas (Mapa de la antigua ciudad)", - 5109: "Barrio de la Catedral", - 5110: "Patio de Cringrís", - 5111: "Catedral del Alba", - 5112: "Prisión Guardapétrea", - 5113: "Plaza de los Mercaderes", - 5114: "Puesto de Bahrum", - 5115: "Base Aérea de Forjaz", - 5117: "Avanzada de Ollaboquilla", - 5118: "Valle Cortaviento", - 5121: "Caída de Galen", - 5124: "Templo de Quel'Dormir", - 5130: "Trono de la Vida", - 5134: "Malezamarina", - 5135: "Arrecife Abandonado", - 5136: "Puerto Faucedraco", - 5137: "La Krazzería", - 5138: "Garganta de Sangre", - 5139: "La Garganta", - 5140: "Bancalto", - 5141: "Patrulla de Barbafuego", - 5142: "Bramal", - 5143: "Kirthaven", - 5144: "Extensión Bruñida", - 5145: "Profundidades Abisales", - 5146: "Vashj'ir", - 5148: "Distrito de Mercaderes", - 5149: "Casco Antiguo", - 5150: "Distrito de los Enanos", - 5151: "Plaza de la Catedral", - 5152: "El Parque", - 5154: "Barrio de los Magos", - 5155: "Reducto Bermellón", - 5157: "El Parque", - 5166: "Circo de las Sombras", - 5167: "La Calle Mayor", - 5168: "Valle del Honor", - 5169: "Valle de los Espíritus", - 5170: "Valle de la Fuerza", - 5171: "Valle de la Sabiduría", - 5173: "Tramoduro", - 5174: "Colina de Hogger", - 5176: "Cubil de Dientes de Oro", - 5177: "Arsenal de Prensadiente", - 5178: "La Base Aérea Tóxica", - 5247: "El Rompemareas", - 5248: "Bastión de Voldrin", - 5249: "Recompensa Estigia", - 5250: "Remordimiento de Galerek", - 5251: "La Segadora de Hierro", - 5252: "El Intrépido", - 5287: "El Cabo de Tuercespina", - 5288: "Bancal de Ahn'Qiraj", - 5289: "Torre Mortoalerta", - 5290: "La Sima Enfurecida", - 5291: "Ruinas Dentadas", - 5292: "Tramo de Rozas", - 5293: "Profundidades Desmoronadas", - 5294: "Agujas Estremecedoras", - 5295: "El Nidal Pálido", - 5296: "Trono de Therazane", - 5297: "Egreso Próspero", - 5298: "Precipicio Crepuscular", - 5299: "Puerta de Lorthuna", - 5300: "Alto Verlok", - 5301: "Tumba de Grolluk", - 5302: "Extensión Carmesí", - 5303: "Templo de la Tierra", - 5304: "Estación Cambioveloz", - 5305: "Arboleda del Guardaverde", - 5306: "Casco del Rasgadversarios", - 5311: "Transporte - Ascensor Onyxia/Nefarian", - 5312: "Nazj'vel", - 5313: "Restos de la Furia de la Tormenta", - 5314: "Lago de Ventormenta", - 5315: "Juramento de Llane", - 5316: "Estanque de Olivia", - 5317: "Bambala", - 5318: "El Cataclismo", - 5320: "Fuerte Livingston", - 5321: "Cubil de Ululante", - 5322: "Cicatriz del Rompemundos", - 5323: "Avanzada Tempespluma", - 5324: "Cañón del Viento Calizo", - 5325: "Puesto de Keeshan", - 5326: "Campamento Sempiterno", - 5327: "Avanzada de la Torre", - 5328: "Marjal Argenta Superior", - 5329: "Marjal Argenta Inferior", - 5330: "Hogar Pétreo", - 5331: "Frente Fracturado", - 5332: "Embajada de Orgrimmar", - 5333: "Prueba de Escarcha", - 5334: "El Bastión del Crepúsculo", - 5335: "Cuenca de Cuarcita", - 5336: "Restos de la Flota", - 5337: "Sagrario de los Profetas", - 5338: "Profundidades Fungal", - 5339: "Vega de Tuercespina", - 5340: "Sala de Armas", - 5341: "Sala de los Expedicionarios", - 5342: "La Gran Fundición", - 5343: "La Sala Militar", - 5344: "La Sala Mística", - 5345: "Cima del Trueno", - 5346: "Cementerio de la Ciudad de Ventormenta", - 5347: "Cráter de Render", - 5348: "Barco mercenario de la Alianza a Vashj'ir", - 5349: "Fosa Rocaguja", - 5350: "Sima Rocaguja", - 5351: "Ruinas de la Ciudad de Gilneas", - 5352: "Saliente Alabastro", - 5353: "Delta del Verrall", - 5354: "Mirador Crepuscular", - 5355: "Guarida de Abismion", - 5356: "Fuerte Grommash", - 5357: "Los Campos Convulsos", - 5358: "Caída de Alamuerte", - 5367: "La Finca de los Ensalmadores", - 5368: "Cámara de Avalanchion", - 5369: "Alto Mando Renegado", - 5374: "Entrada de las Cavernas Roca Negra", - 5375: "Restos del Amanecer de Plata", - 5382: "Fauce Abisal", - 5383: "Cubil del Devorador", - 5385: "La Gran División", - 5386: "Puesto de Retaguardia Renegado", - 5387: "El Frente Renegado", - 5388: "Expedición Alzalba", - 5389: "Península de Tol Barad", - 5390: "Los Canales", - 5391: "Caverna de Alahierro", - 5392: "Avanzada Carbonizada", - 5394: "Puerta de los Maestros", - 5395: "Cámara del Descanso", - 5396: "Ciudad Perdida de los Tol'vir", - 5397: "La Finca de Wollerton", - 5398: "Afueras de la Ciudad de Ventormenta", - 5399: "Bastión de Baradin", - 5400: "Rescoldos", - 5401: "Cuartel del Acorazado", - 5402: "Vigilia del Celador", - 5403: "Fortín del Este", - 5404: "Fortín del Sur", - 5405: "Fortín del Oeste", - 5406: "Tramo Piedranegra", - 5408: "Excavación de la Liga de Expedicionarios", - 5409: "El Antiguo Cuartel", - 5410: "Caverna Luz de Plata", - 5411: "Avanzada de Malicia", - 5416: "La Vorágine", - 5418: "El Reguero de Sangre", - 5420: "La Puerta de los Ciclos Eternos", - 5421: "Valle Pinorrojo", - 5422: "Torre de Taelan", - 5423: "Obelisco de las Estrellas", - 5424: "Rompiente Obsidiana", - 5425: "Orilla Crepuscular", - 5426: "Senda de Uther", - 5427: "Cripta de la Colina de las Penas", - 5428: "El Santuario de la Tierra Oriental", - 5429: "El Santuario de la Tierra Occidental", - 5431: "Cámara de las Estrellas", - 5432: "Mando Avanzado Renegado", - 5433: "La Muralla de Cringrís", - 5434: "Mando Avanzado Renegado", - 5435: "Ciudad de Gilneas", - 5436: "Tramo de la Tempestad", - 5437: "Círculo de los Elementos", - 5438: "Las Fauces de la Locura", - 5439: "Ruinas de Drakgor", - 5440: "Cabos del Norte", - 5441: "Aldea Piedra Ígnea", - 5442: "Mina Piedra Ígnea", - 5443: "Avanzada Velaspanto", - 5444: "Guardia de Rutsak", - 5445: "Gloria", - 5446: "Grada de la Muerte", - 5447: "Bancal Rajado", - 5448: "Torre del Escudo de la Luz", - 5449: "La Batalla por Gilneas", - 5450: "Faro", - 5451: "Estación de Bombeo", - 5452: "Minas", - 5453: "Aterrizaje de la Horda", - 5454: "Fortaleza de Gilneas", - 5455: "Tumba de Khartut", - 5456: "El Frente de Batalla", - 5457: "La Dominancia", - 5458: "Chapaleos", - 5459: "Avanzada Marea Pantanosa", - 5460: "Fango de Sangre", - 5461: "Paso Faucedraco", - 5462: "Recodo de Vermis", - 5463: "Dominio de Ix'lar", - 5464: "Terrenos de caza de Villa Darrow", - 5465: "Orsis", - 5466: "Ramkahen", - 5467: "Nahom", - 5468: "Costa de Gilneas", - 5469: "Torno de Asedio", - 5470: "Paso del Invicto", - 5471: "Machacolpe", - 5472: "La Puerta Crepuscular", - 5473: "La Ciudadela Crepuscular", - 5474: "Profundidades de Elementium", - 5475: "Cubil de los Discípulos", - 5476: "Capilla Piroleña", - 5477: "Posada Piroleña", - 5478: "Concejo Piroleña", - 5479: "Cala Serpenteante", - 5480: "Campamento base del Frente de Liberación de Gilneas", - 5481: "Campamento base de la Séptima Legión", - 5483: "Ruinas Montocre", - 5484: "Resistencia Montocre", - 5485: "Cobertizo Montocre", - 5486: "Plaza Principal Montocre", - 5487: "Fila del Mercado Montocre", - 5488: "Campamento de Guerra Gorshak", - 5489: "El Osario", - 5493: "La Brecha Devoradora", - 5495: "Gnomeregan", - 5496: "El Diodo", - 5497: "El Diodo de Mar", - 5498: "Mar'at", - 5499: "Campos de Akhenet", - 5500: "Avanzada de la Legión Ramkahen", - 5501: "Santuario de los Elementales", - 5502: "Ruinas de Bramal", - 5503: "Altar de la Ascensión", - 5504: "Lago Dumont", - 5505: "El Foso de Escamas", - 5506: "La Capilla Profanada", - 5507: "El Cerebelo", - 5508: "Bancal Piedrasol", - 5510: "Entrada del Castillo de Colmillo Oscuro", - 5511: "Entrada del Monasterio Escarlata", - 5512: "Entrada de Scholomance", - 5513: "Entrada de Stratholme", - 5514: "Entrada de Servicio de Stratholme", - 5515: "Entrada de las Profundidades de Roca Negra", - 5516: "Entrada de la Cumbre de Roca Negra", - 5517: "Entrada de las Cavernas de Brazanegra", - 5518: "Entrada de Horado Rajacieno", - 5519: "Campamento de Rhea", - 5520: "Maraudon - Entrada de la Gruta Maldita", - 5521: "Maraudon: Entrada de la Gruta de la Espora Fétida", - 5522: "Maraudon: Entrada de las Cascadas del Canto de la Tierra", - 5523: "Entrada de Uldaman", - 5524: "Paso de Mirasangre", - 5525: "Excavación Viento Seco", - 5526: "Tumba de los Vigías", - 5527: "Crisol de Matanza", - 5529: "La Masacre: Entrada del Barrio Alabeo", - 5530: "La Masacre: Entrada del Ágora de Gordok", - 5531: "La Masacre: Entrada de los Jardines de la Capital", - 5532: "Entrada de Zahúrda Rajacieno", - 5533: "Entrada de Zul'Farrak", - 5534: "Cabo de la Esperanza Perdida", - 5535: "Astillero de Wellson", - 5536: "Colina del Olvido", - 5537: "Leñoscuro", - 5538: "Aldea Monrojo", - 5539: "Fortaleza de Farson", - 5540: "Alto de Largo", - 5541: "Ciudadela del Fuego Infernal: Entrada de las Murallas", - 5542: "El Frente Inquieto", - 5543: "Ciudadela del Fuego Infernal: Entrada de Las Salas Arrasadas", - 5544: "Ciudadela del Fuego Infernal: Entrada de El Horno de Sangre", - 5545: "Campamento de Baradin", - 5546: "Dominios Grito Infernal", - 5547: "Auchindoun - Entrada de las Criptas Auchenai", - 5548: "Auchindoun - Entrada de las Tumbas de Maná", - 5549: "Auchindoun - Entrada de las Salas Sethekk", - 5550: "Auchindoun - Entrada del Laberinto de las Sombras", - 5551: "Colmillo Torcido: Entrada de La Cámara de Vapor", - 5552: "Colmillo Torcido: Entrada de La Sotiénaga", - 5553: "Colmillo Torcido: Entrada del Recinto de los Esclavos", - 5554: "El Castillo de la Tempestad: Entrada de El Mechanar", - 5555: "El Castillo de la Tempestad: Entrada de El Invernáculo", - 5556: "El Castillo de la Tempestad: Entrada de El Arcatraz", - 5557: "Entrada del Bancal del Magister", - 5558: "Entrada de La Fuga de Durnholde", - 5559: "Entrada de la Apertura del Portal Oscuro", - 5560: "Entrada de La Matanza de Stratholme", - 5561: "Entrada de El Nexo", - 5562: "Entrada de El Oculus", - 5563: "Loch Verrall", - 5564: "Boca del Dragón", - 5565: "Nuevo Kargath", - 5566: "Entrada de la Fortaleza de Utgarde", - 5567: "Entrada del Pináculo de Utgarde", - 5568: "Entrada de Azjol-Nerub", - 5569: "Ahn'kahet: Entrada de El Antiguo Reino", - 5570: "Entrada de la Fortaleza de Drak'Tharon", - 5571: "Entrada de El Bastión Violeta", - 5572: "Entrada de Gundrak", - 5573: "Entrada de Cámaras de Piedra", - 5574: "Entrada de Cámaras de Relámpagos", - 5575: "Entrada de la Prueba del Campeón", - 5576: "Entrada de La Forja de Almas", - 5577: "Entrada del Foso de Saron", - 5578: "Entrada de Cámaras de Reflexión", - 5579: "Entrada del Trono de las Mareas", - 5580: "Entrada de Grim Batol", - 5581: "Entrada de El Núcleo Pétreo", - 5582: "El Umbral", - 5583: "La Cumbre del Vórtice", - 5584: "Altar del Crepúsculo", - 5585: "Puerta de Hamatep", - 5586: "Río Vir'naal", - 5588: "Nidal Crepuscular", - 5589: "Quebranto Crepuscular", - 5590: "Baluarte Crepuscular", - 5591: "Excavación Sumergida", - 5592: "La Guarida Oculta", - 5593: "La Hemorragia", - 5594: "Bosque de las Tierras Altas", - 5595: "Bosque Obsidiana", - 5596: "Cuna de los Ancestros", - 5597: "Obelisco del Sol", - 5598: "Entrada del Templo Sumergido", - 5599: "Ciudad Perdida de los Tol'vir", - 5600: "Bastión de Baradin", - 5601: "Cámara del Sol", - 5602: "Embarcadero de Schnottz", - 5603: "Torre Oriental", - 5604: "Torre Guardalodo", - 5605: "Oasis Árido", - 5606: "Ruinas de Ammon", - 5609: "La Brecha Negra", - 5610: "Tumba del Terracundo", - 5611: "Cementerio Barbafuego", - 5612: "El Embarcadero Maldito", - 5613: "Los Pasos del Sino", - 5614: "La Senda de la Devastación", - 5615: "Llanos de Pirox", - 5622: "El Matorral Verde", - 5623: "El Círculo de las Cenizas", - 5624: "La Arboleda Tranquila", - 5625: "Ruinas de Lar'donir", - 5626: "Cresta Acechumbra", - 5627: "Nidal del Draco Negro", - 5628: "Cima de Hierro", - 5629: "Avance del Torio", - 5630: "La Vorágine", - 5631: "Oasis del Profeta Caído", - 5632: "Enclave del Trampero", - 5633: "Bancal de los Augurios", - 5634: "Sagrario del Viento del Sur", - 5635: "Cuartel del Señor de la Guerra", - 5637: "Posada Orgullo de León", - 5638: "Trono de los Cuatro Vientos", - 5639: "Foso Tripal", - 5640: "Conflagración de Humboldt", - 5641: "La Brecha Crepuscular", - 5642: "Tierras de Tahret", - 5644: "Sello del Rey del Sol", - 5645: "Arboleda Susurravientos", - 5646: "Ruinas de Sahket", - 5647: "Ruinas de Ahmtul", - 5649: "Alto de Corazón Salvaje", - 5650: "Las Colinas de los Vástagos", - 5651: "Puesto Llamaestrella", - 5652: "Cobertizo Diente Negro", - 5653: "Campos de Honor", - 5654: "Cincelada", - 5655: "Horno Pirontraña", - 5656: "El Laberinto de Calaveras", - 5657: "Bloque D", - 5658: "Profundidades Malditas", - 5659: "El Agujero", - 5660: "Guarida Obsidiana", - 5663: "Ciudad de Neferset", - 5664: "Iso'rath", - 5665: "Monte del Vigía del Sol", - 5666: "Presa Vir'naal", - 5667: "Infierno de Cannon", - 5668: "Obelisco de la Luna", - 5669: "Templo de Uldum", - 5670: "Oasis de Vir'sar", - 5671: "Ruinas de Khintaset", - 5672: "Cámara de la Luna", - 5673: "Puesto de Sullah", - 5674: "Ascenso del Hacedor", - 5675: "Aguja de Arikara", - 5676: "Claro de Troncoferro", - 5677: "Valle de las Cenizas", - 5678: "Alto del Morapecado", - 5679: "Afueras de la Ciudad de Neferset", - 5680: "Sala de la Bandera Martillo Salvaje", - 5681: "Sala de la Bandera Faucedraco", - 5682: "Monte del Rayo", - 5683: "Debajo del Arco Iris Doble", - 5684: "Trono de los Cuatro Vientos", - 5685: "Precipicio del Peregrino", - 5686: "Ocaso de Bashal'Aran", - 5687: "El Roble Quejumbroso", - 5688: "Monte Akher", - 5689: "Tumbas de los Precursores", - 5690: "Puesto Comercial de Arsad", - 5691: "Costa Lanza Negra", - 5692: "Las Tumbas del Camposanto", - 5693: "Caverna Negrasombra", - 5694: "Paso de Keset", - 5695: "Ahn'Qiraj: El Reino Caído", - 5696: "Avanzada de vigilantes", - 5697: "Otilius", - 5698: "Tumba del Rey del Sol", - 5699: "El Círculo de la Vida", - 5700: "Hostal de Schnottz", - 5701: "Entrada de las Cámaras de los Orígenes", - 5702: "Entrada de La Cumbre del Vórtice", - 5703: "Foso de Humo de Buenazampa", - 5704: "Las Catacumbas", - 5705: "Chalet Cubilnevado", - 5706: "Las Charcas Vaporosas", - 5707: "Egreso de Berilo", - 5708: "Entrada de la Ciudad Perdida de los Tol'vir", - 5709: "Tumba de Atulhet", - 5710: "Las Costas Extintas", - 5711: "Las Costas Extintas", - 5712: "El Monte Negro", - 5713: "Caladero Hayward", - 5714: "Poblado Valletormenta", - 5715: "Oasis Vir'naal", - 5716: "Lago Vir'naal", - 5717: "Delta Vir'naal", - 5718: "Puño de la Fortuna", - 5719: "Ágora", - 5720: "Costa Noctuniebla", - 5721: "Flota de Asalto Fuertemarea", - 5723: "Tierras de Fuego", - 5724: "Campamento Crepuscular", - 5726: "Casa Comunal del Campamento Mojache", - 5727: "Escalofrío de Scott", - 5728: "Posada Alalcón", - 5729: "Descenso de Alanegra", - 5730: "Posada de Nordrassil", - 5731: "Retiro de Whelgar", - 5732: "Cámaras de los Orígenes", - 5733: "Frente de Magma", - 5734: "Sarahlandia", - 5736: "La Isla Errante", - 5737: "Bosque Pei-Wu", - 5738: "Brecha de Malfurion", - 5739: "Campos Cinéreos", - 5740: "Fuentes de Magma", - 5741: "Profundidades Ígneas", - 5742: "Cumbre Inhóspita", - 5743: "La Guarida de la Viuda", - 5744: "Alto Punta Salvaje", - 5745: "El Horno", - 5746: "La Corriente de Magma", - 5747: "Rocas Desmoronadas", - 5748: "Cima Pluma de Fuego", - 5749: "Tramo de Ragnaros", - 5758: "Campamento Calígine", - 5763: "Meseta de Piroclasto", - 5764: "Guarida de Beth'tilac", - 5765: "Brecha Flama", - 5766: "Ruina Pétrea", - 5767: "Óbito del Mortal", - 5768: "Puente de Sulfuron", - 5769: "Patio de la Fortaleza de Sulfuron", - 5770: "Fortaleza de Sulfuron", - 5771: "Los Campos Fundidos", - 5775: "Bastión Faucedraco", - 5776: "Bastión Martillo Salvaje", - 5779: "Campo de tiro Thalassiano", - 5780: "Montañas Amani", - 5781: "Cresta Nordune", - 5782: "Avanzada de Darsok", - 5783: "Lugar Sombrío", - 5784: "Bancal Altonube", - 5785: "El Bosque de Jade", - 5786: "El Nexo", - 5787: "Fragata de Schnottz", - 5788: "Pozo de la Eternidad", - 5789: "Fin de los Días", - 5790: "Santuario de Dragones Rubí", - 5791: "La Cresta de la Llama Ancestral", - 5792: "Santuario de Dragones Obsidiana", - 5793: "Santuario de Dragones Azur", - 5794: "Santuario de Dragones Esmeralda", - 5795: "Santuario de Dragones Bronce", - 5796: "Umbral del Tiempo", - 5797: "Crestaserena", - 5798: "El Yunque de Conflagración", - 5799: "Ojo de la Tormenta", - 5800: "Torre de los Elfos de Sangre", - 5802: "Ruinas Draenei", - 5803: "Ruinas del Atracador Vil", - 5804: "Torre de los Magos", - 5805: "Valle de los Cuatro Vientos", - 5806: "Espinazo del Destructor", - 5807: "Guarida de Beth'tilac", - 5808: "Patio de la Fortaleza de Sulfuron", - 5809: "Fortaleza de Sulfuron", - 5810: "Yunque de Conflagración", - 5811: "Los Campos Fundidos", - 5812: "Óbito del Mortal", - 5813: "Puente de Sulfuron", - 5814: "Ruina Pétrea", - 5815: "Brecha Flama", - 5816: "La Cresta de la Llama Ancestral", - 5817: "Meseta de Piroclasto", - 5818: "Tierras de Fuego", - 5820: "Templo de los Cinco Albores", - 5821: "El Sendero de la Corrupción", - 5823: "Círculo de Espinas", - 5824: "Cima del Viento del Sur", - 5825: "El Valle del Amanecer", - 5826: "Las Pozas Cantarinas", - 5827: "El Labrantío", - 5828: "Aldea Mandori", - 5829: "Cresta Viento Risueño", - 5830: "Aldea Brisa Temprana", - 5831: "Poblado Fe-Feng", - 5832: "El Bosque de Bastones", - 5833: "Restos de El Buscacielos", - 5834: "Campos de Entrenamiento de Shang Xi", - 5835: "Aldea Wu-Song", - 5836: "Corazón del Bosque", - 5837: "El Puente del Amanecer", - 5838: "Bosque Susurrante", - 5839: "La Gran División", - 5840: "Valle de la Flor Eterna", - 5841: "Cima Kun-Lai", - 5842: "Estepas de Tong Long", - 5843: "Confín Sereno", - 5844: "Hora del Crepúsculo", - 5845: "Templo del Reposo del Dragón", - 5846: "Charca de Fu", - 5847: "La Escalera del Albor", - 5848: "Cueva de Meditación", - 5849: "Santuario de la Luz Interior", - 5850: "Costa de la Seda", - 5851: "Claro Zarpa'don", - 5852: "Restos de La Vanguardia", - 5853: "Puesto Garrosh'ar", - 5854: "Huerto Brisa Dulce", - 5855: "Monasterio Tian", - 5856: "Santuario del Recuerdo", - 5857: "Campamento Gormal", - 5859: "El Camino de los Ancestros", - 5860: "Retiro de Liang", - 5861: "Isla Luna Negra", - 5862: "Charca del Reflejo", - 5863: "Tierras del Festín", - 5865: "Restos del Puño de Grito Infernal", - 5866: "Ascenso de la Espiral de Viento", - 5867: "Pista de Aterrizaje Armafuerte", - 5868: "El Círculo del Enfoque Interno", - 5869: "Mare Magnum", - 5870: "La Feria de la Luna Negra", - 5871: "El Círculo del Equilibrio", - 5872: "Fosas Colafusta", - 5873: "Risco Niebla Marina", - 5874: "Bosque de Cera", - 5875: "Guarida de la Bruja de Jade", - 5876: "Corazón del Dragón", - 5877: "Combate a muerte de la Feria de la Luna Negra", - 5878: "Cuenca de la Orquídea", - 5879: "Bancal de los Diez Truenos", - 5880: "Lucha de Alamuerte en La Vorágine", - 5881: "La Granja de Dai-Lo", - 5882: "Gruta Mur", - 5883: "Madriguera Fecunda", - 5884: "Cervecería Ki-Han", - 5885: "Santuario del Risco Soñador", - 5886: "Cámara de los Susurros", - 5887: "Alameda Ramasombra", - 5890: "Arboleda del Zorro", - 5891: "Valle de Shang", - 5892: "Alma de Dragón", - 5893: "La Vorágine", - 5894: "Testing02", - 5895: "Testing SubZone A", - 5896: "The Orchid Pond - NOT USED", - 5897: "El Soto Fértil", - 5898: "Cataratas de la Serenidad", - 5899: "Camino de la Iluminación", - 5900: "Punta del Grito", - 5901: "Cueva de las Palabras", - 5902: "Senda de la Luna Negra", - 5904: "Paseo marítimo de la Luna Negra", - 5905: "Campamento Nooka Nooka", - 5906: "Aguja de Soledad", - 5907: "Lago Kittitata", - 5908: "Círculo de Escamas", - 5909: "Anillo de Sangre", - 5910: "Círculo de Huesos", - 5911: "Círculo de Piedras", - 5912: "Matorral Olavista", - 5913: "La Masacre: Norte", - 5914: "La Masacre: Este", - 5915: "La Masacre: Oeste", - 5916: "Stratholme: Entrada Principal", - 5917: "Stratholme: Entrada de Servicio", - 5918: "Monasterio del Shadopan", - 5919: "Mar Inexplorado", - 5920: "Ojo de la Eternidad", - 5921: "Arena de Catadores", - 5922: "Sobre El Mar Gélido", - 5923: "Baldío del Dragón", - 5924: "Santuario de Dragones Azur", - 5925: "Santuario de Dragones Esmeralda", - 5926: "Senda de los Titanes", - 5928: "Cima del Reposo del Dragón", - 5929: "El Sedal", - 5930: "Floralba", - 5931: "El Arboretum", - 5932: "Santuario del Alba", - 5933: "El Gong de la Esperanza", - 5934: "Lago de Nácar", - 5935: "Poblado Aleta de Nácar", - 5936: "Finca de Pang", - 5937: "Granjas Pie Atronador", - 5938: "Rancho Pie Atronador", - 5939: "Villoría Pie Atronador", - 5940: "Pabellón del Soñador", - 5941: "Mare Magnum", - 5942: "Bosque del Tigre", - 5943: "Ciénaga Brasacielo", - 5944: "Cabaña Abandonada", - 5945: "Poza de la Zarpa", - 5946: "Lago Brisa Temprana", - 5947: "Santuario de los Cinco Albores", - 5948: "Vega Escarpada", - 5949: "Granja de Shang", - 5950: "Cerro Makaku", - 5951: "Campo del Miko", - 5952: "Mirador Pintoresco", - 5953: "Ribera Turbia", - 5954: "Guarida del Pesar", - 5955: "Cantera Verdemar", - 5956: "Templo del Dragón de Jade", - 5957: "Hogar de Caolín", - 5958: "Prado Aguasol", - 5959: "La Gazapera Retuerta", - 5960: "Alamuerte", - 5961: "Arroyo Manduka", - 5962: "Augurio del Emperador", - 5963: "Cervecería del Trueno", - 5964: "Las Tierras de Bambú", - 5966: "Bosque de los Olvidados", - 5967: "Espesura Paoquan", - 5968: "Villarroca", - 5969: "Campos de Villarroca", - 5970: "Marjal Exótico", - 5972: "Pozas de la Pureza", - 5973: "Nueva Cifera", - 5974: "Tierras del Templo de Jade", - 5975: "Templo del Dragón de Jade", - 5976: "Puerta del Sol Poniente", - 5978: "Patio Ancestral", - 5979: "Mare Magnum", - 5980: "El Alcor", - 5981: "El Mercado del Alcor", - 5982: "El Granero Imperial", - 5983: "Llanura de los Ladrones", - 5984: "El Abanico Dorado", - 5985: "Acantilado del Céfiro", - 5986: "Campos de Seda", - 5987: "El Arroyo Áureo", - 5988: "Vicio de Cristal", - 5989: "Baldío del Dragón", - 5990: "Reposo de Galakrond", - 5991: "Senda de los Titanes", - 5992: "Los Cultivos Florecientes", - 5993: "Patio de las Luces", - 5994: "Orillas del Pozo", - 5995: "Palacio de Azshara", - 5996: "Gazapera Cola Saltarina", - 6000: "Atalaya de Zhu", - 6001: "Cervecería del Trueno", - 6002: "Callejón Sediento", - 6004: "La Jungla Salvaje", - 6005: "Provincia Zhu", - 6006: "La Escalera Velada", - 6007: "Grieta del Trueno", - 6008: "Ruinas de Dojan", - 6009: "La Incursión", - 6010: "Río Dojani", - 6011: "Pozas de la Juventud", - 6012: "Isla de Barlovento", - 6013: "Retiro del Cazador del Alba", - 6014: "Río Krasarang", - 6015: "Ruinas de Korja", - 6016: "Campamento Base de las Centinelas", - 6017: "Poblado Canción de Otoño", - 6018: "Cataratas Krasari", - 6019: "La Jungla Prohibida", - 6020: "La Muralla del Sur", - 6021: "Cataratas Huangtze", - 6022: "Mar Velo de Niebla", - 6023: "Puente Lanzaviento", - 6024: "La Espiral de Viento", - 6025: "Safari de Nesingwary", - 6026: "Las Ruinas Krasari", - 6027: "Nido Mur", - 6028: "Despeñadero Loma Agreste", - 6029: "La Fosa Torjari", - 6030: "Las Llanuras Salvajes", - 6031: "Aldea Bruma Otoñal", - 6032: "Ruinas de Guo-Lai", - 6033: "La Escalera Dorada", - 6034: "Lago Pétalo Níveo", - 6035: "Baluarte del Sol Poniente", - 6036: "La Pagoda Dorada", - 6037: "Cementerio Tu Shen", - 6038: "Guarida de la Bestia", - 6039: "Rancho Cantosol", - 6040: "Marjal Revolcafango", - 6041: "Mare Magnum", - 6042: "Isla Theramore", - 6043: "Marjal Revolcafango", - 6044: "Bahía Revolcafango", - 6045: "Isla de Alcaz", - 6046: "Ciudadela Garrida", - 6047: "El Lobo de Mar", - 6048: "Templo de la Grulla Roja", - 6049: "Refugio Ala de Grulla", - 6050: "Río Canto Espiritual", - 6051: "Templo de Kotmogu", - 6052: "Cámaras Escarlata", - 6053: "Alto de las Ruinas", - 6054: "Claro Brote Invernal", - 6055: "Las Cinco Hermanas", - 6058: "Comitiva Asaltada", - 6059: "Aldea Binan", - 6060: "Templo de Kotmogu", - 6061: "Templo de Kotmogu", - 6062: "Paso Kun-Lai", - 6063: "Retiro Agualuna", - 6064: "Firmamento", - 6065: "Poblado Kunzen", - 6066: "Scholomance", - 6067: "Veranda de la Primavera Eterna", - 6069: "Presa Branquias de Tinta", - 6071: "El Hozen Beodo", - 6072: "Lavadero de Yaks Lao e Hijo", - 6073: "Las Llanuras Otoñales", - 6074: "Salas de Guo-Lai", - 6075: "La Puerta Dorada", - 6076: "Cimas Cobrizas", - 6077: "Aldea Sri-La", - 6078: "Trono del Invocador", - 6079: "El Bazar Grúmel", - 6080: "Mirador del Dragón", - 6081: "Pico de la Serenidad", - 6082: "La Vereda Solitaria", - 6083: "Campamento Dienterroto", - 6084: "La Zanja", - 6085: "Barrilia", - 6086: "Avanzada Dienterroto", - 6087: "La Cúpula del Miko", - 6088: "Agujero Machakadedo", - 6089: "Campamento de Montaña Yeti", - 6090: "La Senda Arpillera", - 6091: "El Valle Arpillero", - 6092: "Barrio de Canteros de Verdemar", - 6093: "Campamento Bataar", - 6094: "Reposo Viento del Este", - 6095: "Reposo Viento del Oeste", - 6096: "Lago de las Eneas", - 6097: "Cueva Kunzen", - 6098: "Granja de Tallarín", - 6099: "Escondrijo de los Bribones", - 6100: "Rancho Zarpa Lanuda", - 6101: "Cervezas y Truenos", - 6102: "Refugio Zarpa Atronadora", - 6103: "Mirador Zarpa Atronadora", - 6104: "Refugio Zarpa Atronadora", - 6105: "Mirador Zarpa Atronadora", - 6106: "Muelle de los Pescadores", - 6107: "Franja Verdeante", - 6108: "Zanja de Narsong", - 6109: "Monasterio Escarlata", - 6110: "El Bosque del Trueno", - 6111: "La Estación Arpillera", - 6112: "CTF3", - 6113: "Pináculos de Narsong", - 6114: "Mogujia", - 6117: "Fuente de la Visión Eterna", - 6118: "El Sagrario del Escribiente", - 6119: "Aposento de los Dragones Gemelos", - 6120: "El Corazón de Jade", - 6121: "Campamento Ruqin", - 6122: "La Arboleda Florida", - 6123: "SM Cathedral/GY (TEMP)", - 6124: "Campamento de Ordo", - 6125: "Cámaras Mogu'shan", - 6126: "Minas Lonjaplata", - 6127: "El Santuario Silente", - 6128: "Retirada del Shadopan", - 6129: "Rincón Tallo Ardiente", - 6134: "Espesura Krasarang", - 6135: "Laguna Nayeli", - 6136: "Templo de Kotmogu", - 6137: "Cobertizo Peloescarcha", - 6138: "Desierto del Pavor", - 6140: "Mogu Ruins Bural Pit (TEMP)", - 6141: "Santuario de las Dos Lunas", - 6142: "Santuario de las Siete Estrellas", - 6143: "Palacio Mogu'shan", - 6144: "Camino Jin Yang", - 6145: "Los Campos Estivales", - 6146: "Vistamar", - 6147: "Kea Krak", - 6148: "Unga Ingoo", - 6149: "Alto Ancestral", - 6150: "La Puerta del Buey", - 6151: "Gruta Hálitovil", - 6152: "Campamento Yongqi", - 6153: "Pico de la Serenidad", - 6154: "Aldea Yinying", - 6155: "Cuna de Chi-Ji", - 6156: "Cumbre Kota", - 6157: "Campamento Base Kota", - 6158: "Arrecifes Arenáceos", - 6159: "Profundidades Sarjun", - 6160: "Pabellón de Angkhal", - 6161: "Pedestal de la Esperanza", - 6162: "Cúpula Balrissa", - 6163: "Campamento Osul", - 6164: "El Sumidero Inferior", - 6165: "El Sumidero Superior", - 6166: "Avanzada Longying", - 6168: "Playa Zouchin", - 6169: "Aldea Zouchin", - 6170: "Villanorte", - 6171: "Corrupción del Odio", - 6172: "Puesto de Jarroalto", - 6173: "Monasterio del Shadopan", - 6174: "Templo del Tigre Blanco", - 6175: "Perdición del Explorador", - 6176: "Valle de Crestanevada", - 6179: "Pozo de los deseos de Owen", - 6180: "Cumbre del Nieverest", - 6181: "Santuario del Orador del Agua", - 6182: "Palacio Mogu'shan", - 6184: "Refugio del Explorador", - 6185: "Monte Nieverest", - 6186: "Campamento Base Nieverest", - 6187: "Frente Gao-Ran", - 6188: "Paso de la Senda Lóbrega", - 6189: "La Hondonada Creciente", - 6190: "Madriguera Fangosa", - 6191: "Cuenca del Anochecer", - 6192: "Puente del Anochecer", - 6193: "Vigilancia de Rensai", - 6194: "Alto de Vigilancia", - 6195: "Sik'vess", - 6196: "Guarida Sik'vess", - 6197: "Cuartel del Shadopan", - 6198: "Valle de los Emperadores", - 6199: "Tumba de las Sombras", - 6200: "Tumba de los Secretos", - 6201: "Tumba de los Conquistadores", - 6202: "Santuario del Inclemente", - 6203: "Descanso del Emperador Rikktik", - 6204: "Aldea Verdemar", - 6205: "Kri'vess", - 6206: "Alto de la Nube Susurrante", - 6207: "La Vanguardia Zandalari", - 6208: "Cripta de los Reyes Olvidados", - 6209: "El Bosque de Jade", - 6210: "Aldea Verdemar", - 6211: "Barrio de Canteros de Verdemar", - 6212: "Incursión Sra'thik", - 6213: "Templo de Niuzao", - 6214: "Asedio del Templo de Niuzao", - 6215: "Gesta: Valle del Poder", - 6216: "Valle del Poder", - 6218: "El Foso Oscuro", - 6219: "Arena de la Aniquilación", - 6220: "Campos de Niuzao", - 6234: "Extensión Tremebunda", - 6292: "Castillo de Ventormenta", - 6293: "Lago de los Astros", - 6294: "Cuenca Shado-Li", - 6295: "Trono del Saber", - 6296: "Arena Tol'viron", - 6297: "Corazón del Miedo", - 6298: "Arena Liza'gar", - 6299: "El Resguardo Ámbar", - 6300: "Kypari Zar", - 6301: "Campo de Entrenamiento", - 6302: "Claustro del Cazador", - 6303: "El Patio", - 6304: "Templo del Buey Negro", - 6306: "Bancal Gurthan", - 6307: "Shan'ze Dao", - 6308: "Posada de Sri-La", - 6309: "Unga Ingoo", - 6310: "Unga Ingoo", - 6311: "Catacumbas de Niuzao", - 6312: "Campamento Gai-Cho", - 6313: "Gesta: Templo del Buey Negro", - 6314: "Sra'vess", - 6315: "Klaxxi'vess", - 6316: "Ik'vess", - 6317: "Klik'vess", - 6318: "Set'vess", - 6319: "Gesta: Ruinas Mogu", - 6328: "Asalto a Zan'vess", - 6329: "Kor'vess", - 6331: "Maroeste", - 6332: "La Posada del Mirador", - 6333: "Tasca de Paur", - 6334: "Jardín de la Cebada Crepuscular", - 6335: "La Marcha Espectral", - 6336: "Aldea Rikkitun", - 6337: "Arrecifes Gokk'lok", - 6338: "El Invernadero Ámbar", - 6339: "Rikkilea", - 6340: "Paso Kun-Lai", - 6341: "Aldea Binan", - 6342: "Marjal Ámbar", - 6343: "Kypari Vor", - 6345: "Caverna de Konk", - 6346: "Cámara del Mañana", - 6347: "La Cámara de Ámbar", - 6348: "Meseta del Espiritista", - 6349: "El Dragón Danzante", - 6354: "La Naba Perezosa", - 6355: "Posada Pie Atronador", - 6356: "Taberna Jarra Pétrea", - 6358: "Posada Confín Salvaje", - 6359: "Mesón de Binan", - 6360: "La Fortuna del Viajero", - 6361: "Cervecería Dos Puños", - 6362: "Taberna Vientonorte", - 6363: "La Rosa Áurea", - 6364: "Taberna en la Niebla", - 6365: "El Cubo de Cebo", - 6366: "Pesca y brebajes de Vistamar", - 6367: "Posada de Verdemar", - 6368: "Lance de Aguado", - 6369: "Cala Krasarang", - 6370: "Playa de la Tortuga", - 6371: "Las Islas del Sur", - 6372: "Puerta de la Primavera Eterna", - 6373: "El Nidal Secreto", - 6375: "Taberna en la Niebla", - 6376: "El Pasaje Antiguo", - 6377: "Madriguera Escama Odiosa", - 6378: "Tierra de Korja", - 6379: "Dojo de Verdemar", - 6380: "Camino de los Cien Pasos", - 6381: "La Senda Florida", - 6382: "Locura del Albañil", - 6383: "Pétalo Glacial", - 6384: "Claustro Abandonado", - 6385: "Capilla de los Cruzados", - 6386: "Jardines de la Capilla", - 6387: "Bancal del Fragor", - 6388: "Descenso de Zhu", - 6389: "Caverna Viento Aullante", - 6390: "Frente Gai-Cho", - 6391: "La Mugre Salobre", - 6392: "Ramabaja", - 6393: "Espinazo del Dragón", - 6394: "Espinazo del Dragón", - 6395: "Espinazo del Dragón", - 6396: "Puerta del Sol Poniente", - 6397: "La Campana Clarín", - 6398: "Santuario de los Dragones Gemelos", - 6401: "Plataforma de Mazu", - 6402: "Restos de El Saltanieblas", - 6403: "Gruta Mugrescama", - 6404: "Arrecifes Mugrescama", - 6405: "Provincia Zouchin", - 6406: "Isla de la Venganza", - 6407: "Llamada del Trueno", - 6408: "Campamento de Asalto", - 6409: "Área de Retaguardia", - 6410: "Árbol Hueco", - 6411: "Campamento de Asalto", - 6412: "Área de Retaguardia", - 6413: "Árbol Hueco", - 6414: "La Arboleda Florida", - 6415: "El Avance Yaungol", - 6417: "Río Yan-Zhe", - 6418: "Saliente Venenoso", - 6419: "Cima Kun-Lai", - 6420: "Las Fosas del Ágape", - 6421: "Campamento de Guerra Kzzok", - 6422: "Puerto del Enjambre Sra'thik", - 6423: "Cresta Ikz'ka", - 6424: "Cámara de Raíces Sra'vess", - 6425: "Pantano Olvidado", - 6426: "Festival de la Cerveza Lunar", - 6427: "Presa Branquias de Tinta", - 6428: "El Pasaje Antiguo", - 6429: "Costa del Gorjeo", - 6430: "Expedición de los Pescadores", - 6431: "Cala Marejada", - 6432: "Cuenca del Resplandor Ámbar", - 6433: "Cala Solitaria", - 6434: "El Paso del Trueno", - 6435: "Corazón del Miedo", - 6436: "Guarida del Terror", - 6437: "Guarida del Miedo", - 6438: "Guarida del Horror", - 6439: "Guarida del Pánico", - 6440: "Kypari Ik", - 6441: "Fangal Raíz Negra", - 6442: "La Nidada de Shek'zeer", - 6443: "Bosquetormento", - 6444: "Zan'vess", - 6445: "Rocas Susurrantes", - 6446: "Cantera de Ámbar", - 6447: "La Senda del Aguijón", - 6448: "Mirador de Mazu", - 6449: "Templo de Kotmogu", - 6450: "Cañada Umbría", - 6451: "Valle de los Retos", - 6452: "Campamento Narache", - 6453: "Islas del Eco", - 6454: "Camposanto", - 6455: "Isla del Caminante del Sol", - 6456: "Valle Ammen", - 6457: "Nueva Ciudad Manitas", - 6458: "Cantera de Kyparita", - 6459: "Fosas Picaveneno", - 6460: "La Mugre Salobre", - 6461: "Rocas Susurrantes", - 6462: "Sendero de los Conquistadores", - 6463: "Marjal Revolcafango", - 6466: "Cueva del Eco Infinito", - 6467: "Tribuna Mogu'shan", - 6468: "El Tramo del Emperador", - 6469: "Senda Viento Aullante", - 6470: "El Vestíbulo Dorado", - 6471: "La Sala de la Asamblea Carmesí", - 6472: "Pasadizo Olvidado", - 6473: "Cámara de los Reyes Inmemoriales", - 6474: "Trono de los Antiguos Conquistadores", - 6475: "Refugio del Señor de las Espadas", - 6476: "Santuario de Nala'shi", - 6477: "Mares del Sur", - 6478: "Mares del Sur", - 6479: "Mares del Sur", - 6480: "Mares del Sur", - 6481: "Mares del Sur", - 6482: "El Bancal del Verano", - 6483: "El Paso del Emperador", - 6484: "El Farol Áureo", - 6485: "Cámara de la Iluminación", - 6486: "La Cámara Celestial", - 6487: "Galería Etérea", - 6488: "El Bazar de las Estrellas", - 6489: "El Intercambio Imperial", - 6490: "Salón del Ruiseñor", - 6492: "Cámara del Reflejo", - 6494: "Santuario de Avenencia", - 6495: "La Estancia Enlosada", - 6496: "Cámara Ritual de Guo-Lai", - 6497: "La Estancia de las Estatuas", - 6498: "Puerta de los Augustos Celestiales", - 6499: "La Sala del Dragón", - 6500: "Marjal Revolcafango", - 6501: "El lady Mehley", - 6502: "Isla Theramore", - 6503: "Cámara de Guo-Lai", - 6504: "La Vía del Emperador", - 6505: "La Guarida Hojarrío", - 6506: "Foso de la Semilla Madre", - 6507: "Isla del Trueno", - 6508: "Risco Cola Saltarina", - 6509: "Interior de la Cervecería del Trueno", - 6510: "Las Minas de la Muerte", - 6511: "Cuevas de los Lamentos", - 6512: "El Lamento de la Viuda", - 6513: "Oona Kagu", - 6514: "Maraudon", - 6515: "Veranda de la Primavera Eterna", - 6516: "Aldea Zarpa'don", - 6517: "Fortaleza de las Agujas Gemelas", - 6518: "Camino de la Serenidad", - 6519: "Reposo Estival", - 6520: "Gruta de Gokk'lok", - 6521: "Poblado Almíbar", - 6522: "Claro Almíbar", - 6523: "Granja Almíbar", - 6524: "Bastión del Trueno", - 6525: "Puño de Grito Infernal", - 6526: "Campo de Entrenamiento", - 6527: "Bancal del Tigre", - 6528: "Espinazo del Dragón", - 6529: "Puño de Grito Infernal", - 6530: "Espinazo del Dragón", - 6533: "El Abrasacielos", - 6534: "Interior de la Fortaleza de las Agujas Gemelas", - 6535: "Guarida del Desafío", - 6538: "Risco Sombra Otoñal", - 6539: "Colina Flor de Sol", - 6540: "Reposo Canto Espiritual", - 6541: "Taberna Manzana Jugosa", - 6542: "Bancal Gurthan", - 6543: "Guarida Hojaniebla", - 6544: "Madriguera Muerdefrío", - 6545: "Mansión de Cringrís", - 6546: "Mina Piedra Ígnea", - 6547: "La Jungla Salvaje", - 6548: "Saliente Venenoso", - 6549: "Sra'vess", - 6550: "Campos de Niuzao", - 6551: "Estepas de Tong Long", - 6552: "Cima Kun-Lai", - 6553: "Santuario de las Siete Estrellas", - 6554: "Santuario de las Dos Lunas", - 6555: "Montañas de Elwynn del Norte", - 6556: "Reposo Audaz", - 6557: "Provincia Zouchin", - 6558: "La Vanguardia Zandalari", - 6560: "La Terraza Áurea", - 6561: "Punta de Kril'mandar", - 6562: "Las Islas del Sur", - 6563: "Cala Krasarang", - 6565: "Una Daga en la Oscuridad", - 6566: "Punto de Dominio", - 6567: "Batalla en Alta Mar", - 6572: "Mina de Tronafritz", - 6573: "Defensa de la Taberna", - 6574: "Espesura Krasarang", - 6575: "Espesura Krasarang", - 6576: "Malandanza", - 6577: "La Aguja del Trueno", - 6578: "El Patio de los Huesos", - 6579: "Za'Tual", - 6580: "Risco Ihgaluk", - 6581: "Zeb'tula", - 6582: "Shaol'mara", - 6583: "El Alto Violeta", - 6584: "Promontorio Buscalba", - 6585: "Puerta del Emperador", - 6586: "El Cerco de las Bestias", - 6587: "Platea del Conquistador", - 6588: "Cruce Sangriento", - 6589: "Mina del Relámpago", - 6590: "Las Forjas del Trueno", - 6591: "Muelle Aguaturbia", - 6592: "La Cripta Solemne", - 6593: "Atrio de Lei Shen", - 6594: "Cámara de los Difamados", - 6595: "El Abrasacielos", - 6596: "Desembarco del León", - 6597: "Avanzada de Chispamisil", - 6600: "Aliviadero Gravanegra", - 6601: "Playa Pantoque", - 6602: "El Crustáceo Hirviente", - 6603: "Excavación de Picamena", - 6604: "Aserradero de Hachaveloz", - 6605: "Corazón de Zan'vess", - 6606: "Misión de Clase", - 6608: "Orgullo de Heyman", - 6609: "Ruinas de Ogudei", - 6610: "El Pasaje Antiguo", - 6611: "Dalaran", - 6612: "Lugar del Desembarco de la Horda", - 6613: "Templo Oscuro", - 6614: "Campo de Entrenamiento Illidari", - 6615: "Espesura Krasarang", - 6616: "Templanza", - 6617: "Avance Garrosh'ar", - 6618: "Club de Lucha de Bizmo", - 6619: "Ruinas de Korune", - 6620: "Guarida de Skiggit", - 6621: "Templo de la Grulla Roja", - 6622: "Solio del Trueno", - 6623: "Monumento de Antonidas", - 6624: "Círculo de Voluntades", - 6625: "Alto de Krasus", - 6626: "Alto de Krasus", - 6627: "El Enclave de Plata", - 6628: "Ciudad de Dalaran", - 6629: "La Ciudadela Violeta", - 6630: "Aguja de La Ciudadela Violeta", - 6631: "Tubería de Salida de las Cloacas", - 6632: "Tubería de Salida de las Cloacas", - 6633: "Rocas Flotantes de Dalaran", - 6634: "Santuario Atracasol", - 6635: "Isla de Dalaran", - 6636: "El Bastión Violeta", - 6637: "Los Bajos Fondos", - 6638: "El Manto de la Noche", - 6639: "Mercado de Magos", - 6640: "Reposo de Vargoth", - 6641: "Balcón de la Ciudadela Violeta", - 6642: "Plaza Tejerruna", - 6643: "Desembarco del León", - 6644: "Punto de Dominio", - 6645: "Refugio Ala de Grulla", - 6646: "Poblado Canción de Otoño", - 6647: "El Bosque de Jade", - 6648: "La Senda Florida", - 6649: "Valle de los Cuatro Vientos", - 6650: "Cima Kun-Lai", - 6651: "Valle de la Flor Eterna", - 6652: "La Escalera Velada", - 6653: "El Abrasacielos", - 6654: "Guarida del Depredador", - 6661: "Isla de los Gigantes", - 6662: "Talador", - 6665: "Cañón del Céfiro", - 6666: "Infiltración en el Astillero", - 6667: "Afueras del Campamento de Guerra Trol", - 6668: "Astillero", - 6669: "Buque de Guerra", - 6670: "Mar", - 6671: "Cámara de los Eminentes", - 6672: "Fosa Lúgubre", - 6673: "¡Hacia los Cielos!", - 6674: "Promontorio Buscalba", - 6675: "Isla del Trueno", - 6676: "Bahía de los Ecos", - 6677: "Isla del Trueno", - 6678: "Dun Morogh", - 6679: "Monte Luz", - 6680: "Provisiones del Frente", - 6681: "Mina del Relámpago", - 6682: "Selva de Tanaan", - 6683: "Costa de la Neblina", - 6686: "Malandanza", - 6687: "El Patio de los Huesos", - 6688: "Za'Tual", - 6689: "Risco Ihgaluk", - 6690: "El Alto Violeta", - 6691: "Promontorio Buscalba", - 6692: "Puerta del Emperador", - 6693: "El Cerco de las Bestias", - 6694: "Platea del Conquistador", - 6695: "Cruce Sangriento", - 6696: "Las Forjas del Trueno", - 6697: "Muelle Aguaturbia", - 6698: "Atrio de Lei Shen", - 6699: "Orgullo de Heyman", - 6700: "Bahía de los Ecos", - 6701: "Siniestro de Beeble", - 6702: "Siniestro de Bozzle", - 6703: "Ensenada de los Susurros", - 6704: "Ensenada de los Susurros", - 6705: "Ruinas Exteriores", - 6706: "Abismo de la Desidia", - 6707: "Ciudadela del Rey del Trueno", - 6708: "Viajero Carmesí", - 6709: "Salabria", - 6710: "Trotamares", - 6711: "Sietevelas", - 6712: "Rayo de los Mares", - 6713: "Barbas de Rhonin", - 6714: "Aposento del Gran Imperión", - 6716: "Ciudadela del Rey del Trueno", - 6717: "Zeb'tula", - 6718: "Shaol'mara", - 6719: "Valle Sombraluna", - 6720: "Cresta Fuego Glacial", - 6721: "Gorgrond", - 6722: "Cumbres de Arak", - 6723: "Selva de Tanaan", - 6724: "Bahía Partedorsales", - 6725: "Ría Brumagrís", - 6726: "Marjal de Atriplex", - 6727: "Matorral Maderasombra", - 6728: "Isla de los Gigantes", - 6729: "Isla del Trueno", - 6730: "Celda Oculta", - 6731: "Los Secretos de Sima Ígnea", - 6732: "La Cima del Tigre", - 6733: "Valle de la Flor Eterna", - 6734: "Excavación de la Gran Flor", - 6735: "Mina de la Gran Flor", - 6736: "Cañón del Céfiro", - 6737: "Cañón del Céfiro", - 6738: "Asedio de Orgrimmar", - 6739: "La Cripta Solemne", - 6742: "Risco Viento Gélido", - 6743: "Throm'var", - 6744: "Barranco Faucedaga", - 6745: "Gruta de Grulloc", - 6746: "Siege of Orgrimmar (OLD DO NOT USE)", - 6747: "Afueras de Cerrotajo", - 6748: "Afueras del Poblado Sen'jin", - 6749: "Cámara de Y'Shaarj", - 6750: "Torre de vigilancia de Cerrotajo", - 6751: "Excavación de la Gran Flor", - 6752: "Mina de la Gran Flor", - 6753: "Cañón del Céfiro", - 6754: "Cañón del Céfiro", - 6755: "Nagrand", - 6756: "Farahlon", - 6757: "Isla Intemporal", - 6758: "Cementerio Estigma de Hielo", - 6759: "Grieta Azotavientos", - 6760: "Castillo de Stromgarde", - 6761: "Gul'rok", - 6762: "Centro de la Ciudad de Shattrath", - 6763: "Caída de Theramore", - 6764: "Cuartel Panzadura", - 6765: "Campos de excavación", - 6766: "Los Fosos", - 6767: "Mina de Piedra de Añublo", - 6768: "La Torre de Hierro", - 6770: "Cala Olvidada del Guardanegro", - 6771: "Torneo Celestial", - 6772: "El Bosque Maldito", - 6773: "Viejo Pi'jiu", - 6774: "Llanuras Agrietadas", - 6775: "Óbito del Coloso", - 6776: "Los Despojos", - 6777: "Avanzada Colmillo Pétreo", - 6778: "Corrientes Vientohelado", - 6779: "Grom'gar", - 6780: "Caverna de los Espíritus Perdidos", - 6781: "Altar de Kron", - 6782: "Ascenso de Fuego Glacial", - 6783: "Foso del Rechazo", - 6784: "Magnarok", - 6785: "Bancal K'arani", - 6787: "Grommar", - 6790: "Bajaluna", - 6791: "Campos Fúnebres", - 6792: "Shaz'gul", - 6793: "Descenso de la Luz", - 6794: "Observatorio de Teluuna", - 6796: "Avanzada Estrella Fugaz", - 6797: "Gul'var", - 6798: "Valle Arrasado", - 6799: "Bahía de Garrafilada", - 6800: "Desembarco Dranosh'ar", - 6801: "La Ofensiva Lanza Negra", - 6802: "Ante las Puertas", - 6803: "Valle de la Fuerza", - 6804: "La Calle Mayor", - 6805: "Puertas de los Rompedores", - 6807: "Corazón Oscuro de Pandaria", - 6809: "Guarida del Vigía", - 6810: "Caída del Gigante", - 6811: "Claro Solitario", - 6812: "Aldea Verdemar", - 6813: "Desembarco del León", - 6814: "Sangre en la Nieve", - 6815: "Punto de Dominio", - 6816: "Ventormenta", - 6817: "Cerro Siniescarcha", - 6820: "[PH] - SMV - Alliance Garrison", - 6821: "Mares del Sur", - 6822: "Lago Rubí", - 6823: "Santuario de Ordos", - 6824: "Cuenca Sapobuche", - 6825: "Ruinas del Caminante del Fuego", - 6830: "Corte Celestial", - 6831: "Lago Musgoverde", - 6832: "Costa Intemporal", - 6833: "Desembarco Tushui", - 6834: "Desembarco Huojin", - 6835: "Ensenada Brumosa", - 6836: "Bancal de las Tres Brisas", - 6837: "Gruta de Kukuru", - 6838: "Guarida de Tsavo'ka", - 6839: "Guarida Misteriosa", - 6840: "Espesura Susurro de las Sombras", - 6841: "Paso de las Piedras Rojas", - 6842: "Senda de los Caminantes del Fuego", - 6843: "Monasterio de la Llama Negra", - 6844: "Senda Llameante", - 6846: "Aguas Perdidas en el Tiempo", - 6848: "Guarida de Turgall", - 6849: "Minas Tiznado", - 6850: "Campamento Minero Tiznado", - 6851: "Grommar", - 6852: "Terreno de Pruebas", - 6854: "Hidey-Hole", - 6855: "Arboleda Sombra Umbría", - 6857: "Alto de la Sombra", - 6861: "Guarida de Grulloc", - 6862: "El Mercado del Alcor", - 6863: "El Ingrediente Secreto", - 6864: "Ciudadela Aguja del Filo", - 6865: "zzOld - Explosion Town [NOT TEMP NAME]", - 6866: "Claro Crepuscular", - 6867: "Anillo de Sangre", - 6868: "Sala del Gran Cazador", - 6869: "Wor'gol", - 6870: "Gruta Sombra Umbría", - 6871: "Roca del Clarividente", - 6872: "Bancal de las Estrellas", - 6873: "Fortaleza de la Angustia", - 6874: "Minas Machacasangre", - 6875: "Trono Aguja del Filo", - 6876: "Alto del Bastión", - 6877: "Risayerta", - 6878: "Cuenca Azotacola", - 6879: "Espina Quebrada", - 6880: "Floración Sol Eterno", - 6881: "Avanzada de las Bestias", - 6882: "Cuevas Olvidadas", - 6883: "Tuberías Roca Negra", - 6884: "Lodazal de la Calígine", - 6885: "Caldera Plumarrisco", - 6886: "Caverna Draenatauro", - 6887: "Arena Quebrantarrocas", - 6888: "Tugurio de Kor'gall", - 6889: "Manantial de Azufre", - 6890: "zzOld - Gorgrond Garrison Area", - 6891: "Cañón de Gronns", - 6892: "El Foso", - 6893: "Espesura Iyun", - 6894: "Cueva de Gronns", - 6895: "Ascenso de Barlovento", - 6896: "Cuenca Elemental", - 6897: "Astillero Roca Negra", - 6898: "zzOld - Tankworks", - 6899: "Cresta de la Aflicción", - 6900: "Cuenca de Azufre", - 6901: "Terminal Malavía", - 6902: "Nido de Reeg'ak", - 6903: "Madriguera Deshelada", - 6905: "Torre Gormaul", - 6906: "Avanzada de Muro de Hielo", - 6907: "Torre de Mor'dul", - 6908: "[PH] Heroes Through Time", - 6909: "Alto de Socrethar", - 6910: "Nidal Marea Oscura", - 6911: "Foso del Devorador", - 6912: "Auchindoun", - 6913: "Paso de Gorgrond", - 6914: "Gran Raigambre", - 6915: "Tierra Fértil", - 6916: "Patio de las Almas", - 6917: "Avanzada de Liadrin", - 6918: "Hoya Telamuerte", - 6920: "Telmor", - 6921: "Llanuras de la Serenidad", - 6922: "Valle Flor Lunar", - 6923: "Aldea Embaari", - 6924: "Aruuna", - 6925: "Estadía del Anacoreta", - 6926: "Velo Shadar", - 6927: "Isla Caeocaso", - 6928: "Senda del Peregrino", - 6929: "Deriva del Jabalí Glacial", - 6930: "Karabor", - 6931: "Camino de la Luz", - 6932: "Cementerio de Sombraluna", - 6933: "Refugio de Terokkar", - 6934: "Paso de Raíces", - 6935: "Floración Afilada", - 6936: "Blasted Lands (DO NOT USE)", - 6937: "Lago Cornapunta", - 6938: "Llamada de Kuuro", - 6939: "Alto del Carnicero", - 6940: "La Trinchera del Vínculo", - 6941: "Ashran", - 6942: "Grieta Espinina", - 6943: "Jardines de la Eternidad", - 6944: "Patio de la Tranquilidad", - 6945: "El Coliseo de la Luz", - 6946: "Puerto de Karabor", - 6947: "Costa Orunai", - 6948: "Campo de Asedio Archenon", - 6949: "Tuurem", - 6950: "Aserradero Mor'gran", - 6951: "Puerto de Hierro", - 6952: "Refugio Súlfico", - 6953: "Cabeza de Playa Arrasada", - 6954: "Desembarco Arrasado", - 6955: "Gesta de la Aldea de los Huesos", - 6956: "Almacén de Acechagronns", - 6957: "Alto del Exiliado", - 6958: "Poza de las Visiones", - 6959: "Mina de Cristal de Aruuna", - 6960: "Cresta Fuego Glacial", - 6961: "Paso del Cañón", - 6962: "Barranco Aguja del Filo", - 6963: "Frente Arrasado", - 6964: "Risco Colmillo Pétreo", - 6965: "Paso del Trueno", - 6966: "Trinchera Helada", - 6967: "Fundición Roca Negra", - 6968: "Acantilados Rocafuria", - 6969: "Minas Machacasangre", - 6970: "Torre de las Sombras", - 6971: "Gruta Sombraluna", - 6972: "Fuerte de Grommar", - 6973: "Muelles de Grommar", - 6975: "[PH] Ashran [TRANSITION ZONE]", - 6976: "Cueva Sangrespina", - 6977: "La Marisma Verde", - 6978: "Reposo de la Luz", - 6979: "Tumba de las Almas", - 6980: "Ciudad de Shattrath", - 6982: "Trono de los Quebrantarrocas", - 6983: "Factoría de Hierro", - 6984: "Terminal Malavía", - 6985: "Gruta Aguafresca", - 6986: "Mina de Jorune", - 6987: "Creciente Menguante", - 6988: "Trecho Celestial", - 6989: "Barrio Inferior", - 6990: "Gran Cumbre", - 6991: "El Mirador", - 6993: "Pico Sauce Lunar", - 6994: "Huesos de Agurak", - 6995: "Caída de Agurak", - 6996: "Ogrópolis", - 6997: "Ruinas Ango'rosh", - 6998: "Cumbres de Arak", - 6999: "Ciudadela del almirante Taylor", - 7000: "Trono de la Depravación", - 7001: "Campamento Jurasol", - 7003: "El Cráter Fracturado", - 7004: "Muro de Hielo", - 7005: "Cuenca Avalancha", - 7006: "Cimas Algentes", - 7009: "Cubil Helado", - 7010: "Cuenca Rangari", - 7011: "Cuenca del Aterimiento", - 7012: "Guarida del Durmiente", - 7013: "Bastión Machacasangre", - 7014: "Paso del Trueno", - 7015: "Cumbres de Arak", - 7016: "Cumbres de Arak", - 7017: "Alto de Blook", - 7018: "Ruinas de Ata'gar", - 7019: "La Sima Tenebrosa", - 7020: "Punta Represalia", - 7021: "El Frente Ardiente", - 7022: "[PH] Test Zone", - 7023: "[PH] Test Zone 2", - 7024: "Madriguera Sabañón", - 7025: "Selva de Tanaan", - 7026: "Bastión Okril'lon", - 7028: "Terrenos de la Forja", - 7029: "Refugio del Exarca", - 7030: "Alto Machacamiedo", - 7031: "Skettis", - 7032: "Jardín de K'ure", - 7033: "Talud Lanza Negra", - 7034: "Pico Garra Temible", - 7035: "Marca del Cuervo", - 7036: "Velo Terokk", - 7037: "El Portal Oscuro", - 7038: "Espesura de Sangre Vital", - 7039: "Altar Sangrante", - 7040: "Terreno de Pruebas Kargathar", - 7041: "Sangre Vital", - 7042: "Salas Umbrías", - 7043: "Cantera Roca Negra", - 7044: "El Camino a la Gloria", - 7045: "Bastión de Hierro", - 7046: "Velo Perdido de Anzu", - 7047: "Glaciar Prohibido", - 7048: "Colina Sangrespina", - 7049: "Marisma de las Pesadillas", - 7051: "Hojarasca Dyvari", - 7052: "Faucesable", - 7053: "Restos de Xandros", - 7054: "Montaña Filospada", - 7055: "Riscos del Ostro", - 7056: "Atalaya Grito de Guerra", - 7057: "Gol'gor", - 7058: "La Montaña Sangrante", - 7059: "Wor'var", - 7060: "Campos de Buena Caza de Hemet", - 7061: "Avanzada de Yrel", - 7062: "El Glaciar Ardiente", - 7063: "Puesto Ribera", - 7064: "Rocas de Profecía", - 7065: "Lok-rath", - 7066: "Corral de Nagrand", - 7067: "Avanzada de Mar'gok", - 7068: "Grommashar", - 7069: "Puertas de Grommashar", - 7070: "Puerto Puño de Hierro", - 7071: "Cornisa Gruñizarpa", - 7072: "Tierras Ancestrales", - 7073: "El Círculo de los Retos", - 7074: "Telaar", - 7075: "Alto del Lobo", - 7076: "Apeadero de la Horda de Hierro", - 7078: "Bajaluna", - 7079: "Cueva Gélida", - 7080: "Torre Fulgor", - 7081: "Estación Telaari", - 7082: "Mina Sombraluna", - 7083: "Defensa de Karabor", - 7084: "Devastación de Aruuna", - 7085: "Avanzada de Player", - 7086: "Ciudadela de Player", - 7087: "Ciudadela de Player: nivel 4, Alianza", - 7088: "Círculo de Conquista", - 7089: "Tumba de las Luces", - 7090: "Caída de Zorkra", - 7091: "Bosque Fulgurante", - 7092: "El Suelo de la Arena", - 7093: "Paso de Terokkar", - 7094: "Garganta Enriscada", - 7095: "Caravana Destrozada", - 7096: "Avanzada de Player", - 7097: "Fuerte Wrynn", - 7098: "Orgullo de Vol'jin", - 7099: "Avanzada Lanza de Guerra", - 7100: "Bastión Escudo de Tormenta", - 7103: "Ciudadela de Player", - 7104: "Ciudadela de Player: nivel 4, Horda", - 7105: "Mirador Rangari", - 7107: "Laderas de Trabalomas (Costasur contra Molino Tarren)", - 7109: "El Vergel Eterno", - 7110: "La Broza", - 7111: "Recodo del Cuervo", - 7112: "Mirador Lobo Gélido", - 7113: "Alto del Lobo", - 7114: "Despeñadero Auchenai", - 7115: "Zangarra", - 7116: "Elodar", - 7117: "Campos de Elodar", - 7118: "Aterrizaje Encantado", - 7119: "Enclave Rangari", - 7120: "Altar de Sha'tar", - 7121: "Alto del Exarca", - 7122: "Cañada Estelar", - 7123: "Salvalor", - 7124: "La Caverna de los Maestros", - 7125: "Clarosombra", - 7126: "Campamento Marcaspina", - 7127: "Velo Akraz", - 7128: "Guarida del Asaltante", - 7129: "Puente de Tar'thog", - 7130: "Alto de Garrafilada", - 7131: "Fortaleza Gordal", - 7132: "Loma de la Redención", - 7133: "Asidero de Durotan", - 7134: "Cubil de Draka", - 7135: "Monte Volira", - 7136: "Taller de Draxen", - 7138: "Alto de Trak", - 7139: "Meseta Elemental", - 7140: "Valle Sangremelena", - 7141: "Arco de Sha'tar", - 7142: "Nido de los Oracuervos", - 7143: "Paso del Bosque Nudoso", - 7144: "Floración Lunacerada", - 7145: "Cuenca de Sethekk", - 7146: "Huesos de Sethe", - 7147: "La Aguja Quebrada", - 7148: "Incursión Rajadiente", - 7149: "Pantano Carmesí", - 7150: "Oshu'gun", - 7151: "Bosque de los Espíritus", - 7152: "Avanzada de Naielle", - 7153: "Manglar Boscaraz", - 7154: "Garganta de la Calina", - 7155: "Valle de la Destrucción", - 7156: "Paraje de la Angustia", - 7157: "Torre de Khadgar", - 7158: "Paso Alto", - 7159: "Frondalia", - 7160: "Fisura de la Furia", - 7161: "Draacorium", - 7162: "Taller Silbapellizco", - 7163: "Alto Silbapellizco", - 7164: "Restos de la Veta Madre", - 7165: "Caravana de Fiona", - 7166: "Aserradero de Paso Alto", - 7167: "Excavación de Elodar", - 7168: "Cueva del Terror", - 7169: "La Máquina de Almas", - 7170: "zzOld - Sundered Pass", - 7171: "Corazón del Pantano Carmesí", - 7172: "Cañón del Ocaso", - 7173: "Desembarco Manto de la Noche", - 7175: "Escondrijo de Ketya", - 7176: "Estribo Gordal", - 7177: "Madriguera de Saturador", - 7178: "Refugio de Rexxar", - 7179: "Trono del Señor Brujo", - 7180: "Atalaya de la Garra", - 7181: "Reposo de Lithic", - 7182: "Altar de la Garra", - 7183: "Corona del Rompedor", - 7184: "zzOld - Imperial Road", - 7185: "Dominios de Moira", - 7186: "Mok'gor", - 7187: "Kog'ah", - 7188: "Gra'ah", - 7189: "Santuario de Othaar", - 7190: "Óbito de Dionor", - 7191: "Talador", - 7192: "Los Bajos Fondos", - 7193: "Distrito del Mercado", - 7194: "Beneplácito del Imperador", - 7195: "Ensenada Goriana", - 7196: "Senda de los Vencedores", - 7197: "Hachazo", - 7198: "Puerto Sur", - 7199: "Excavación Apexis", - 7200: "Auchindoun", - 7201: "Alto del Tosco", - 7202: "Cenagal Sinuoso", - 7203: "Fosa Pálida", - 7204: "Sagrario de los Naaru", - 7205: "Rehoyo Grom'kar", - 7206: "Cataratas Solitarias", - 7207: "Cámara Apexis", - 7208: "Crustáceo Salobre", - 7209: "Puente Aguja del Filo", - 7210: "Fuerte de Player", - 7211: "Fuerte de Player", - 7212: "PattyMack Test Area", - 7213: "Plaza del Jardín", - 7214: "Plaza de la Fuente", - 7215: "Costa Luminiscente", - 7216: "Gran Árbol Valuundira", - 7217: "Puertas de Grommashar", - 7218: "Grommashar", - 7219: "Aguazal Esporulado", - 7220: "Orilla Olvidada", - 7221: "Isla de las Sombras", - 7222: "Granja del Almirante Taylor", - 7223: "Altar Negro de Xan'tish", - 7224: "Madreselva", - 7225: "Umbropantano", - 7226: "Barranco Viento del Este", - 7227: "Lago Son Celeste", - 7228: "Caverna Vil", - 7229: "Concejo", - 7230: "Gran Sala", - 7231: "Sendero Cienaluz", - 7232: "Cuenca Huesos Secos", - 7233: "Bahía Pescahielo", - 7234: "Costa Muerdefrío", - 7235: "Profundidades Colosales", - 7236: "Diente de Tor'goroth", - 7237: "Desembarco Machacasangre", - 7238: "Mar de Hierro", - 7239: "Mar de Zangar", - 7241: "Ensenada del Ostro", - 7242: "Cala Pálida", - 7243: "Punta del Jabalí Glacial", - 7244: "Profundidades del Aterimiento", - 7245: "Promontorio Ata'gar", - 7246: "Cala Sombra Umbría", - 7247: "Salto de Ozgor", - 7248: "Desembarco Throm'var", - 7249: "Cabo Sombraluna", - 7250: "Cabo Umbropantano", - 7251: "Bahía Orunai", - 7252: "Delta Orunai", - 7253: "Cima Sombraluna", - 7255: "Mar de Zangar", - 7256: "Estrecho Marea Oscura", - 7257: "Bahía Glaciar", - 7258: "Mar Evanescente", - 7259: "Mar de Hierro", - 7260: "Laguna Hielofrío", - 7261: "Muro del Aterimiento", - 7262: "Canal de Tanaan", - 7263: "Cicatriz Machacasangre", - 7264: "Costa Sombraluna", - 7265: "Canal de Tanaan", - 7266: "El Cilindro Oxidado", - 7267: "Cámara del Titán", - 7268: "Santuario de Paso de Terokkar", - 7269: "Aguja de la Luz", - 7270: "Risco Aullante", - 7271: "Campamento de Cuerolobo", - 7272: "Ciénaga Negra", - 7273: "Laguna del Silencio", - 7274: "Velo Zekk", - 7275: "Cantera de Magma", - 7276: "Delta Inhóspito", - 7277: "Tierras de la Manada Sangremelena", - 7278: "Guarida de Leorajh", - 7279: "Anfiteatro de la Aniquilación", - 7280: "El Trono de los Elementos", - 7281: "Club de Lucha Salvaje", - 7282: "Almacén de Madera de las Tierras Bajas", - 7283: "Arena de Entrenamiento de Paso Alto", - 7284: "Sagrario Arcano", - 7285: "Sagrario Arcano", - 7286: "Arsenal de Vol'jin", - 7287: "Torre de Artillería Wrynn", - 7288: "Guarida del Traficante", - 7289: "Guarida del Traficante", - 7290: "Taberna Lar de Fuego", - 7291: "Cervecería Stoktron", - 7292: "Corral de Wor'var", - 7293: "Corral Rangari", - 7294: "Tanquería de Wor'var", - 7295: "Tanquería Telaari", - 7296: "Puesto de Vigilancia Mok'gol", - 7297: "Ruinas de Na'gwa", - 7298: "Ruinas del Mirador", - 7299: "Ruinas del Primer Bastión", - 7300: "Mar Barrera", - 7301: "Bastión Mok'gol", - 7302: "Vigilia de la Sombra", - 7303: "Despeñadero Quebrado", - 7304: "Cima Vaporatriz", - 7305: "Bancal del Alba", - 7306: "Bancal de las Tierras Bajas Arrasadas", - 7307: "Cumbre de Roca Negra Superior", - 7308: "Fosas de Cosecha", - 7309: "Muelle de Hierro", - 7310: "Dominio de Puño Negro", - 7311: "Cementerio Machacabrasas", - 7312: "Cueva del Náufrago", - 7313: "Expedición Destruida", - 7314: "[Test] Sound Design", - 7315: "Alto de la Mano Destrozada", - 7316: "Skettis", - 7317: "Bastión Garrafilada", - 7318: "El Refugio Acogedor", - 7319: "Caída de Gorgorek", - 7320: "Fronda del Vergel", - 7321: "Mina Abandonada de Ogrópolis", - 7322: "Fin del Crujecolmillos", - 7323: "Cráter Cinéreo", - 7324: "Excavación de Bajaluna", - 7325: "Excavación de Bajaluna", - 7326: "Excavación de Bajaluna", - 7327: "Mina del Muro de Hielo", - 7328: "Mina del Muro de Hielo", - 7329: "Mina del Muro de Hielo", - 7330: "Ventormenta", - 7331: "Laguna Cerúlea", - 7332: "Escudo de Tormenta", - 7333: "Lanza de Guerra", - 7334: "Azsuna", - 7335: "Cárcel de Esclavos Espiral de Odio", - 7336: "Bahía del Contrabandista", - 7337: "La Puerta Ardiente", - 7338: "Antro de Runas", - 7339: "Templo de Zarkhenar", - 7340: "Reposo Alazur", - 7341: "Manantial Nor'Danil", - 7342: "La Represalia de la Reina", - 7343: "[UNUSED]", - 7344: "Arena del Naufragio", - 7345: "Mak'rana", - 7346: "Pasaje de Rhut'van", - 7348: "Cala de Oceanus", - 7349: "Faronaar", - 7351: "Campamento de la Legión: Ruina", - 7352: "Arrecifes El'dranil", - 7353: "Criadero de Kraklaa", - 7354: "Campamento de la Legión: Caos", - 7355: "Palacio Desmoronado", - 7356: "Puesto de la Victoria", - 7357: "Jardines Inundados", - 7358: "Academia de Nar'thalas", - 7359: "Criadero Alazur", - 7360: "Tierras Altas de Llothien", - 7361: "Ruinas Ley de Zarkhenar", - 7362: "Frente de Faronaar", - 7363: "Estanque Real", - 7364: "Avanzada Illidari", - 7365: "Campamento de Guerra Espiral de Odio", - 7366: "Ruinas de Nar'thalas", - 7367: "Ogrópolis", - 7368: "Alijo de Gurgthock", - 7369: "Torre Destruida", - 7370: "Avanzada Azur", - 7375: "Terreno de Pruebas Goriano", - 7376: "El Círculo de Sangre", - 7377: "Claro del Olvido", - 7378: "Bosque Oscuro", - 7379: "Cascadas Gorianas", - 7381: "Prueba de Fe", - 7382: "Pozas de Vida", - 7383: "Arboleda Verdeante", - 7384: "Risco Violeta", - 7385: "Costa de Zangar", - 7386: "Breña Escupehojas", - 7387: "Excavación de Brak", - 7389: "Dominio de Furiafilo", - 7390: "Puertas de Tanaan", - 7391: "El Camino a la Gloria", - 7392: "Pilares del Destino", - 7393: "Paso Verdantis", - 7394: "Caldera Contravapor", - 7395: "Coliseo", - 7396: "Cueva Aguijón Negro", - 7397: "Costa Luminiscente", - 7399: "Cabaña de Ishaal", - 7400: "Cabaña de Zellek", - 7401: "Caverna del Lamento", - 7402: "Alto Sol Eterno", - 7403: "Montón de Mantillo", - 7404: "Costa Lernea", - 7405: "Puerto de Ogrópolis", - 7406: "Acantilados de Ogrópolis", - 7407: "Mar del Sur", - 7408: "Bahía Vientorroc", - 7409: "La Orilla del Ocaso", - 7410: "Mar del Sur", - 7411: "Vereda de Hierro", - 7412: "Cala del Asesino", - 7413: "Cuenca Susurro del Espíritu", - 7414: "Cresta Cielomar", - 7415: "Vereda de Hierro", - 7416: "Cueva Escamazufre", - 7417: "Fondeadero Sha'tari", - 7418: "Autoridades del Puerto de Shattrath", - 7419: "Faro de Sha'tar", - 7420: "Ciudad de Shattrath", - 7421: "Ágora de Shattrath", - 7422: "Distrito del Mercado Sha'tari", - 7423: "Lugar del Accidente del Malavía", - 7424: "Guarida de la Bestia Fluvial", - 7425: "Posada del Farolero", - 7426: "Hogar Medialuna", - 7427: "La Almohada de Seda", - 7428: "Mar Barrera", - 7429: "Paso de la Horda de Hierro", - 7431: "El Puño Colosal", - 7432: "Meseta Elemental", - 7433: "Lugar de Trabajo Telaari", - 7434: "Lugar de Trabajo de Wor'var", - 7435: "Finca de Aarko", - 7436: "Mar del Sur", - 7437: "Paso Viento Marchito", - 7438: "Asiento de Kor'lok", - 7439: "Reposo de los Reyes", - 7440: "Saliente Equidneano", - 7441: "Campamento Lanza de Guerra", - 7442: "Campamento Escudo de Tormenta", - 7444: "Ashran", - 7445: "Mar Evanescente", - 7446: "Mar Barrera", - 7447: "Profundidades Colosales", - 7448: "Mar Barrera", - 7449: "Fosas Espinosas", - 7450: "El Peñasco Indómito", - 7451: "Poblado Cuerno Partido", - 7452: "Cala Maraciago", - 7453: "Estación de Tránsito Sha'tar", - 7454: "Meseta Célica Sha'tari", - 7455: "Distrito Residencial de Shattrath", - 7456: "Guarida Frondosa", - 7457: "Guarida de Sher'khaan", - 7459: "Casa de Carne de Carne Pútrida", - 7460: "Guarida de los Secretos", - 7461: "Garganta Sacudetierra", - 7462: "Coliseo", - 7463: "Coliseo", - 7464: "Senda de los Vencedores", - 7465: "Ensenada Goriana", - 7466: "Beneplácito del Imperador", - 7467: "Distrito del Mercado", - 7468: "Los Bajos Fondos", - 7469: "Campamento Oculto", - 7470: "Monte Kra'gor", - 7471: "Guarida de Dol'mak", - 7472: "Paso del Óbito", - 7473: "Colinas de Valuun", - 7474: "Taller de Thaelin", - 7475: "Posada de ciudadela de la Alianza", - 7476: "Avance de Volrath", - 7477: "El Cruce", - 7478: "Vanguardia de Tremblade", - 7479: "Avanzada de los Archimagos", - 7480: "Mirador de Shattrath", - 7481: "Cuchitril de Akeeta", - 7482: "Lugar de Trabajo del Fuerte Wrynn", - 7483: "Lugar de Trabajo del Orgullo de Vol'jin", - 7484: "Lugar de Trabajo de Puerto Sur", - 7485: "Lugar de Trabajo de Hachazo", - 7486: "Puerta de Ventormenta", - 7488: "Cámara Maligna", - 7489: "Caldera Plumarrisco", - 7490: "Ciudadela", - 7492: "Valle de Saraah", - 7493: "Factoría de Hierro", - 7494: "Huesos de Agurak", - 7495: "Costa de Tanaan", - 7496: "Valle de Tanaan", - 7497: "Mirador de la Traidora", - 7498: "Valle Sombraluna (Terrallende)", - 7499: "Nagrand (Terrallende)", - 7500: "Delta Orunai", - 7501: "Costa Orunai", - 7502: "Dalaran", - 7503: "Monte Alto", - 7504: "La Ciudadela Violeta", - 7505: "Alto de Krasus", - 7506: "Alto de Krasus", - 7508: "Enclave Dientegris", - 7510: "El Abismo Ardiente", - 7513: "Guarida de Devastadores", - 7514: "Zeth'Kur", - 7515: "Puerto Bastión de Hierro", - 7517: "La Vega Baldía", - 7519: "Filo de la Realidad", - 7520: "Puerto de Hierro", - 7521: "Vieja Senda de la Costa", - 7522: "Avanzada del León", - 7523: "Vol'mar", - 7524: "Ruinas de Kra'nak", - 7525: "Bancal de la Luz Lejana", - 7526: "Templo de Sha'naar", - 7527: "Frente de Hierro", - 7534: "Costa Abrupta", - 7535: "Bancal de Challiane", - 7536: "Puesto de Aktar", - 7537: "Cámara de la Tierra", - 7538: "Trono de Kil'jaeden", - 7539: "Zeth'Gol", - 7540: "Guarida del Derrocado", - 7541: "Tormenheim", - 7542: "Cimas Anegadas", - 7543: "Costa Abrupta", - 7544: "Cámara de Aggramar", - 7545: "Ciudadela del Fuego Infernal", - 7546: "Guarida de Neltharion", - 7547: "Ciudad Negra", - 7548: "Mina de Ashran", - 7549: "[TEMP] Stormshield Excavation", - 7550: "[TEMP] Warspear Excavation", - 7551: "[TEMP] The Hollow", - 7552: "[TEMP] The Ancient Vault", - 7553: "[TEMP] The Dark Below", - 7554: "Jardín de Elune", - 7555: "Campamento de Selección Abrasacielos", - 7556: "Farondale", - 7557: "Caverna Escama Tormentosa", - 7558: "Val'sharah", - 7559: "Valle Zarpacerada", - 7562: "Templo Perdido", - 7563: "Cala de Nashal", - 7564: "Posición de los Renegados", - 7567: "Manantial Nor'Danil", - 7568: "Tierras Altas de Llothien", - 7569: "Faronaar", - 7570: "Estanque Real", - 7571: "Velo Oliváceo", - 7572: "Thas'talah", - 7574: "Fin de la Esperanza", - 7575: "Desembarco de la Luz", - 7576: "El Vacío Abisal", - 7577: "Promontorio del Maestro", - 7578: "Ojo de Azshara", - 7579: "Cuenca Trombainferno", - 7580: "Monumento de Antonidas", - 7581: "Ciudad de Dalaran", - 7582: "Círculo de Voluntades", - 7583: "Aguja de La Ciudadela Violeta", - 7584: "Tubería de Salida de las Cloacas", - 7585: "Tubería de Salida de las Cloacas", - 7586: "Foso Sangrante", - 7587: "Rocas Flotantes de Dalaran", - 7588: "Helheim", - 7589: "Santuario Brisaveloz", - 7590: "Isla de Dalaran", - 7591: "Cámara de los Ojos", - 7592: "El Bastión Violeta", - 7593: "Plaza Tejerruna", - 7594: "Los Bajos Fondos", - 7595: "El Manto de la Noche", - 7596: "Mercado de Magos", - 7597: "Reposo de Vargoth", - 7598: "Balcón de la Ciudadela Violeta", - 7599: "Cubil de Shackle", - 7600: "Lorlathil", - 7601: "Arboleda de Cenarius", - 7602: "Enclave de la Sociedad Empírea", - 7603: "Mirador Pico Negro", - 7604: "Marisma de Zoram", - 7605: "Cuenca Desdichada", - 7606: "Cráter de Cazacalaveras", - 7607: "Ascenso Eterno", - 7608: "Hrydshal", - 7609: "[TEMP] JT (copy)", - 7610: "Grajero Alatormenta", - 7611: "Sendero Rompeciclón", - 7612: "Cima de Thorim", - 7615: "La Forja Vil", - 7616: "El Nidal Olvidado", - 7617: "Valle Zarpaluna", - 7618: "Ensenada Marchita", - 7620: "Senda Verde", - 7621: "Llothien", - 7622: "El Osario Profanado", - 7624: "Tumba de Sargeras", - 7625: "La Rosa Negra", - 7626: "El Clamañublo", - 7627: "El Brisaveloz", - 7628: "La Desolación de Sanguino", - 7629: "Trono de Helya", - 7630: "Campo Eterno de Batalla", - 7631: "Claro Oromiel", - 7632: "Bastión del Trabajante de Hierro", - 7633: "Sagrario en Ruinas", - 7634: "Feralas", - 7635: "[TEMP] Placeholder Area - Level Design Dev Land Only", - 7636: "El Refugio Corrupto", - 7637: "Suramar", - 7638: "Sagrario de la Luz", - 7639: "Lodazal de Espinas", - 7641: "Fang'rila", - 7642: "Arroyo Braden", - 7643: "Puertas del Valor", - 7646: "Acceso Llamarada Vil", - 7647: "Reposo Lunar", - 7649: "Cuenca Durmiente", - 7650: "Granja de Marl", - 7651: "Campo de los Reyes Caídos", - 7652: "Bosque de las Runas", - 7653: "Haustvald", - 7654: "Caverna Stygana", - 7655: "Refugio Rangari", - 7656: "Mare Magnum", - 7657: "Cataratas Relucientes", - 7658: "Cala de Nashal", - 7659: "Reposo de los Héroes", - 7661: "Corte del Cuervo", - 7662: "Enclave Cuerno Férreo", - 7663: "Selva de Tanaan", - 7664: "Corazón de Roble", - 7665: "Shala'nir", - 7666: "Marismas Sombrías", - 7667: "Jardín de la Luna", - 7668: "Cuenca de Thas'talah", - 7669: "Caverna Arboleda Oscura", - 7670: "Cueva del Traicionado", - 7671: "Cubil de Magula", - 7672: "Cámaras del Valor", - 7673: "Arboleda Corazón Oscuro", - 7674: "Mazmorra de Legion", - 7675: "Celdas Oscuras", - 7676: "La Perforación", - 7677: "Templo de Elune", - 7678: "Fundición Lok'goron", - 7679: "Acherus: El Bastión de Ébano", - 7680: "Bajíos Alalunar", - 7681: "Caída de Tyr", - 7682: "Caverna Colmillo de Sable", - 7683: "El Brisaveloz", - 7684: "Llanto del Alma en Pena", - 7685: "La Rosa Negra", - 7686: "La Eternidad", - 7687: "La Muerte Insidiosa", - 7688: "El Clamañublo", - 7689: "Bosque Torracuero", - 7690: "El Abrasacielos", - 7691: "Small Battleground D", - 7692: "Cimas Susurrantes", - 7693: "Templo de las Mil Luces", - 7694: "Skold-Ashil", - 7695: "Ciudadela de la Corona de Hielo", - 7696: "El Trono Helado", - 7697: "Hojas del Príncipe Caído", - 7698: "Desembarco Rastro del Pavor", - 7699: "Guardia Gris", - 7700: "Posición del Forestal", - 7701: "Mirador de Crowley", - 7702: "Raíz del Pavor", - 7703: "Astillero de Muro de Hielo", - 7704: "Refugio de Malorne", - 7705: "Mardum, el Abismo Devastado", - 7706: "Astillero de Bajaluna", - 7707: "El Baluarte de Hierro", - 7708: "Templo de Elune", - 7709: "Recodo del Río", - 7710: "Velo de Bruma", - 7711: "Alto de Huln", - 7712: "Posición Illidari", - 7713: "Pico Inferno", - 7714: "Enclave Roepiedra", - 7716: "Ciudadela del Fuego Infernal", - 7717: "Terrenos de caza Foso Sangrante", - 7718: "Costa de los Hundidores", - 7719: "Añublovil", - 7720: "Campamento de Guerra Gromdar", - 7721: "Pantano Colatrueno", - 7722: "La Muralla Quebrada", - 7723: "Avanzada Bastión de Hierro", - 7724: "Playa Aguablanca", - 7725: "Tierras Altas Cazasangre", - 7726: "Cuerno del Cielo", - 7727: "Fosos de Mannoroth", - 7728: "Tierra de las Brujas", - 7729: "Campos de An'she", - 7730: "La Puerta Negra", - 7731: "Tótem del Trueno", - 7732: "Cuenca Roca Escarpada", - 7733: "Retiro de Nesingwary", - 7734: "Ciudadela de la Corona de Hielo", - 7735: "Selva de Tanaan", - 7736: "Cámara de Eyir", - 7737: "Niskara", - 7739: "Arena de Desafíos de Colmillo Negro", - 7740: "Costa de Magma", - 7741: "Cresta de la Desesperación", - 7742: "Sede de Mando", - 7743: "Acherus: El Bastión de Ébano", - 7744: "Reposo del Escudo", - 7745: "La Vorágine", - 7746: "Ash'theran", - 7747: "Máquina de Almas", - 7748: "Extensión Vil", - 7749: "El Martillo Vil", - 7750: "Refugio Sha'naari", - 7751: "Atalaya de Malo", - 7752: "Mirador Ojo de la Tormenta", - 7753: "Corazón de Azeroth", - 7754: "Caverna Enigmática", - 7755: "Valle Pluma Oscura", - 7757: "Pico Desgarraviento", - 7760: "Bahía Manto de la Noche", - 7761: "Mausoleo Cresta Cuervo", - 7765: "Costa Viento Sur", - 7766: "Selva de Tanaan", - 7767: "Suramar", - 7768: "Bastión Alma Vil", - 7769: "Bosque Aojado", - 7771: "Invasión de Tanaan", - 7772: "Julie's Test Area", - 7773: "Ofensiva de Cringrís", - 7777: "El Bastión Violeta", - 7778: "Mansión Heathrow", - 7779: "Desembarco de Radcliffe", - 7780: "Torreón Grajo Negro", - 7782: "Filo del Acantilado", - 7783: "Gruta Piedra Lóbrega", - 7786: "Caverna Brote de Vida", - 7787: "Cámara de las Celadoras", - 7789: "Bosque Hojabronce", - 7790: "Sendero de Huln", - 7791: "Bonanza de la Madre Tierra", - 7794: "Bastión Vol'mar", - 7795: "El Cubil del León", - 7796: "Costa Abrupta", - 7797: "Frontera Devastada", - 7798: "Fin de la Esperanza", - 7799: "Templo Perdido", - 7800: "Mesa Velonieve", - 7801: "Petragrís", - 7803: "Valdisdall", - 7804: "Andu'talah", - 7805: "Torreón Grajo Negro", - 7806: "Cámara de Neltharion", - 7807: "Cueva de la Prueba de Sangre", - 7808: "Valle de la Espada", - 7809: "Túmulo de la Penumbra", - 7810: "Cima de Torok", - 7811: "Naglfar", - 7812: "Acantilados Boca Infernal", - 7813: "Bastión Celestial", - 7814: "Cámara de las Celadoras", - 7815: "Mirador Obsidiana", - 7816: "Arena del Torreón Grajo Negro", - 7817: "Sepulcro del Cielo", - 7818: "Roca Vela", - 7819: "Cámara de los Traidores", - 7820: "Forja de la Corrupción", - 7821: "La Fortaleza Maldita", - 7822: "Arena de Nagrand", - 7825: "Descenso de Los Bajos Fondos", - 7826: "El Vacío Abisal", - 7827: "Costasur contra Molino Tarren", - 7828: "Brecha Sotopiedra", - 7829: "Refugio de Navarrogg", - 7830: "Arrecifes Boca Infernal", - 7833: "Costa Roca Lejana", - 7834: "Templo de la Luz Abisal", - 7835: "El Vacío Abisal", - 7836: "Cubil Hocicofango", - 7837: "Pantano Ciego", - 7838: "Azshara", - 7839: "PH Ocean", - 7840: "Meredil", - 7841: "Tel'anor", - 7842: "Bastión de la Guardia Lunar", - 7843: "Falanaar", - 7844: "Bastión Alma Vil", - 7845: "Fisura de Cristal", - 7846: "Arboleda de los Sueños", - 7847: "Arrecifes Boca Infernal", - 7848: "Puerto Mareskorn", - 7849: "Puesto de Cullen", - 7850: "Avanzada de Lorna", - 7851: "Refugio de la Garra", - 7852: "Campamento de Guerra Vilskorn", - 7853: "Sima Reptapeñón", - 7855: "La Arquería", - 7856: "Frontera de Vuelo de la Selva de Tanaan", - 7857: "Reposo del Escudo", - 7859: "Cueva de los Inmortales", - 7860: "Alto de los Vinculadores de Tinta", - 7861: "Base Pezuña Presta", - 7862: "Morheim", - 7863: "Orilla Escama Estanca", - 7864: "Cámara de la Noche", - 7865: "Corte de la Celadora", - 7866: "Recinto Demoníaco", - 7867: "Cámara de Hielo", - 7868: "Cámara de los Espejos", - 7869: "Cámara de la Ley", - 7870: "Pasaje Caído", - 7871: "Tumba del Penitente", - 7872: "Sala Illidari", - 7873: "Cámara del Silencio", - 7874: "Puerto Mareskorn", - 7875: "Falla Cicatriz del Terror", - 7876: "Caverna del Vigilante", - 7877: "Refugio Alblanco", - 7878: "Cardumen Múrloc", - 7879: "Salón del Guardián", - 7881: "Dalaran", - 7882: "Alto de Krasus", - 7883: "Cala del Naufragio", - 7884: "El Enclave de Plata", - 7885: "Monumento de Antonidas", - 7886: "Ciudad de Dalaran", - 7887: "Círculo de Voluntades", - 7888: "La Ciudadela Violeta", - 7889: "Aguja de La Ciudadela Violeta", - 7890: "Tubería de Salida de las Cloacas", - 7891: "Tubería de Salida de las Cloacas", - 7892: "Rocas Flotantes de Dalaran", - 7893: "Santuario Atracasol", - 7894: "Isla de Dalaran", - 7895: "El Bastión Violeta", - 7896: "Plaza Tejerruna", - 7897: "Los Bajos Fondos", - 7898: "El Manto de la Noche", - 7899: "Mercado de Magos", - 7900: "Reposo de Vargoth", - 7901: "Balcón de la Ciudadela Violeta", - 7902: "La Isla Errante", - 7903: "Templo de los Cinco Albores", - 7904: "Santuario de los Cinco Albores", - 7905: "Aldea Mandori", - 7906: "Bosque Pei-Wu", - 7907: "Cresta Viento Risueño", - 7908: "El Valle del Amanecer", - 7909: "Las Pozas Cantarinas", - 7910: "Túmulo del Durmiente", - 7911: "La Grulla Sonriente", - 7912: "Nido Fétido", - 7913: "Valle Medialuna", - 7916: "Ruinas Sashj'tar", - 7917: "Bahía Vendaval Azur", - 7918: "Falla Cicatriz del Terror", - 7919: "Guarida de Jagganoth", - 7920: "Torre del Celador", - 7921: "Tormenheim", - 7922: "Claro Solescarcha", - 7923: "Puesto Pino de Hielo", - 7924: "Colinas Aullantes", - 7925: "Cueva Murkloc", - 7926: "Puesto de la Cima", - 7927: "Acantilados Boca Infernal", - 7928: "Shal'Aran", - 7929: "Sagrario Ardiente", - 7930: "La Sala Carmesí", - 7931: "La Aguja", - 7932: "Las Cámaras de Alaescarcha", - 7933: "Risco Estridente", - 7934: "La Muralla de las Calaveras", - 7935: "Alto del Libramorte", - 7936: "Avanzada Pezuña Helada", - 7937: "Avanzada Pezuña Solitaria", - 7938: "Banquete de Gurun", - 7939: "Cala Vendaval Azur", - 7940: "Jandvik", - 7941: "Ashran", - 7943: "Cuenca Ley", - 7944: "Estrecho de Hydrannon", - 7945: "Mardum, el Abismo Devastado", - 7946: "Felsoul Hold", - 7947: "Gruta Colahacha", - 7948: "Mirador de Oro", - 7949: "Avanzada Pezuña Pétrea", - 7950: "Avanzada Pezuña de Niebla", - 7951: "Templo del Tigre Blanco", - 7952: "Cima Kun-Lai", - 7953: "Colinas Pardas", - 7954: "Pesadilla Esmeralda", - 7955: "Infralar", - 7956: "Profundidades Desmoronadas", - 7960: "Muro Celeste", - 7961: "Aldea Crin de Nieve", - 7962: "Mando Alma Vil", - 7963: "Gran Paseo", - 7964: "El Cúmulo", - 7965: "Acantilados Viento Impetuoso", - 7966: "Taller de Oculeth", - 7967: "Boost Experience [TEMP NAME]", - 7968: "Kal'delar", - 7969: "Karazhan", - 7970: "Ágora Lunaeterna", - 7971: "Catacumbas de Karazhan", - 7972: "Cubil del Tuétano", - 7973: "Las Islas Veladas", - 7974: "Guarida de Ursoc", - 7975: "Claros de Tirisfal", - 7976: "Claros de Tirisfal", - 7977: "Bosque Susurrante", - 7978: "Reposo del Guardián", - 7979: "Camino del Sueño Esmeralda", - 7980: "Las Rutas Insondables", - 7981: "Enclave Petragrís", - 7982: "Cámaras Resonantes", - 7983: "Campos de la Cacería Eterna", - 7984: "Valle Ámbar", - 7987: "Cementerio Monte Alto", - 7988: "Cámara de Pruebas de Oculeth", - 7990: "Cavernas Arcanas", - 7991: "Mansión Koralune", - 7992: "Pozo Antiguo", - 7993: "Pasaje Olvidado", - 7994: "Fosa de Cristal", - 7995: "Sagrario Sombra Lunar", - 7996: "El Bastión Violeta", - 7997: "Subterráneos de Suramar", - 7998: "Costa Desmoronada", - 7999: "Tótem del Trueno", - 8000: "Azsuna", - 8001: "Bahía del Hombre Muerto", - 8002: "Filo del Horizonte", - 8003: "Templo de las Mil Luces", - 8005: "Veranda de la Primavera Eterna", - 8006: "[TEMP] Tech Test - Seamless World Transition A (JT)", - 8007: "[TEMP] Tech Test - Seamless World Transition B (JT)", - 8008: "Caída de Crinceniza", - 8009: "Gruta Sashj'tar", - 8010: "Cavernas Jandvik", - 8011: "Morada de las Sombras", - 8012: "Cámara de las Sombras", - 8013: "[PH]Mardum Treasures", - 8014: "Cima Viento de las Ánimas", - 8015: "Cubil de Forgalash", - 8017: "Arrecife del Ocaso", - 8018: "Estación Ley de Anora", - 8019: "Caverna Corona Pétrea", - 8020: "Foso Sashj'tar", - 8021: "Finca de Lunastre", - 8022: "Mardum, el Abismo Devastado", - 8023: "El Martillo Vil", - 8024: "Cuenca Vendaval Azur", - 8025: "Bastión Nocturno", - 8026: "Pesadilla Esmeralda", - 8027: "Arena de Dalaran", - 8028: "Lagar de Aegira", - 8029: "Refugio Cantoestelar", - 8031: "Ascenso Esmeralda", - 8032: "Valle Nemoroso", - 8033: "Cala Vendaval Azur", - 8034: "Cráter de Un'Goro", - 8035: "Alto de Thorim", - 8039: "Caverna Cienohondo", - 8040: "Ojo de Azshara", - 8041: "Sueño Esmeralda", - 8042: "Arrecifes Roca Lejana", - 8043: "Falla de Aln", - 8044: "Claros de Tirisfal", - 8045: "Galería de Aegwynn", - 8046: "La Vorágine", - 8047: "Mirador Ojo de la Tormenta", - 8049: "Arena Bruloscuro", - 8050: "Cascadas Nemorosas", - 8051: "Cúspide de Cordana", - 8052: "Una Copa Más", - 8053: "The Greater Sea (Don't Use)", - 8054: "Thal'dranath", - 8055: "Caverna Caninovil", - 8056: "Pico de la Reflexión", - 8057: "[TEMP] Dummy Area - Dev Test (JT)", - 8058: "Dev Area - A", - 8059: "Dev Area - B", - 8060: "Dev Area - C", - 8061: "Dev Area - D", - 8062: "Dev Area - E", - 8063: "Dev Area - F", - 8064: "VETA MADRE", - 8065: "El Retiro", - 8066: "La Carretera", - 8067: "Claro Canto Hendido", - 8074: "Guarida Tel'Andu", - 8075: "Oficina del Jefe de Correos", - 8076: "Círculo de los Espíritus", - 8077: "Warden Tower (PH)", - 8078: "Pedestal del Vigía de la Noche", - 8079: "Corte de las Estrellas", - 8081: "Campamento Espiral de Odio", - 8082: "Marisma de los Naufragios", - 8083: "Cala de Barbaprofunda", - 8084: "Sauces Arrulladores", - 8085: "Caverna de los Fauceparda", - 8086: "Nido de Ala'washte", - 8087: "Espesura Carmesí", - 8088: "Costa Tañemar", - 8089: "Acantilados Velonieve", - 8090: "Precipicio Nemoroso", - 8091: "Nordrassil", - 8092: "Claro de la Luna", - 8093: "La Cumbre del Vórtice", - 8094: "El Más Allá", - 8095: "Nastrondir", - 8096: "Enclave de los Esclavos", - 8097: "Madriguera Corrupta", - 8098: "Test Dungeon", - 8099: "Sagrario de G'Hanir", - 8100: "La Gruta Podrida", - 8101: "Cañón de Miasma", - 8102: "Corazón del Terror", - 8103: "Atalaya de Nashal", - 8104: "Trono de las Mareas", - 8105: "Niskara", - 8106: "Fauce Abisal", - 8107: "Cima Monte Alto", - 8108: "Cámara de los Traidores", - 8109: "Cámara del Silencio", - 8110: "Cámara de los Espejos", - 8111: "Cámara de la Ley", - 8112: "Cámara de Hielo", - 8113: "Tumba del Penitente", - 8114: "Corte de la Celadora", - 8115: "Recinto Demoníaco", - 8116: "Sala Illidari", - 8117: "Pasaje Caído", - 8118: "Cámara de la Noche", - 8119: "La Cámara del Nexo", - 8120: "Acceso a la Tumba", - 8121: "Puente Cuernoliso", - 8122: "Pico Monte Alto", - 8123: "NightmareDragonNexus [PH]", - 8124: "Espada del Alba", - 8125: "Tierras de Fuego", - 8126: "La Puerta Violeta", - 8127: "Óbito del Mortal", - 8128: "Guarida de Beth'tilac", - 8129: "Ruina Pétrea", - 8130: "La Cresta de la Llama Ancestral", - 8131: "Meseta de Piroclasto", - 8132: "El Yunque de Conflagración", - 8133: "Puente de Sulfuron", - 8134: "Patio de la Fortaleza de Sulfuron", - 8135: "Brecha Flama", - 8136: "El Sendero de la Corrupción", - 8137: "Círculo de Espinas", - 8138: "Fortaleza de Sulfuron", - 8139: "Los Campos Fundidos", - 8140: "Salón Juego de Manos", - 8141: "El Nexo", - 8142: "Ciudadela Sangrenegra", - 8143: "Isla Rocanegra", - 8144: "Escondrijo de Katterin", - 8145: "Trono Crepuscular", - 8146: "Abismo Crepuscular", - 8147: "Isla de los Vigías", - 8148: "Ciudad de Suramar", - 8149: "Viñedos Crepusculares", - 8150: "Corte de las Estrellas", - 8151: "Gloria del Cuervo", - 8152: "Sede de los Cazadores", - 8153: "Talloumbrío", - 8154: "Seradane", - 8155: "Arboleda del Crepúsculo", - 8156: "Rama Oniria", - 8157: "División de Rhut'van", - 8158: "Enclave Destruido", - 8159: "Tótem Vil", - 8160: "Arquería de Falanaar", - 8161: "Ulduar", - 8162: "El Archivum", - 8163: "Templo de Fal'adora", - 8164: "Caída de Crinceniza", - 8165: "Ciénaga Pleamar", - 8166: "Bahía del Hombre Muerto", - 8167: "Laderas Sañatormenta", - 8168: "Cima de la Tormenta", - 8169: "Paso del Rey Caído", - 8170: "La Chispa de la Imaginación", - 8171: "Los Pasillos del Ingenio", - 8172: "Meseta de Ámbar", - 8173: "Ruinas de Elune'eth", - 8174: "Bahía de Cuarzo", - 8175: "Cumbres Tormentosas", - 8176: "Templo de las Tormentas", - 8178: "Plataforma de Invocación", - 8179: "Shaladrassil", - 8180: "Pesadilla de Malorne", - 8181: "Desembarco Luz de Luna", - 8182: "Mercado Engalanado", - 8183: "Corte de la Medianoche", - 8184: "Hacienda Enjoyada", - 8185: "Guarida de Eksis", - 8186: "Senda de los Titanes", - 8187: "Reposo de Sotnar", - 8188: "Falanaar", - 8189: "Arquería de Falanaar", - 8190: "Camino del Sueño Esmeralda", - 8191: "Nido Ondavil", - 8192: "Fosos de la Desesperación", - 8193: "Altar del Fin de los Tiempos", - 8194: "Cubiles de Bestia Vil", - 8195: "Máquina de Almas: Apocalipsis", - 8196: "Tajada del Tramposo", - 8197: "Puesto Guardavil", - 8198: "Faronaar Superior", - 8201: "Ciénaga Pleamar", - 8202: "Caverna Vilescama", - 8203: "Altar Pluma Negra", - 8204: "Tótem del Trueno", - 8205: "Reino del Cazador de Magos", - 8206: "Faronaar", - 8207: "Altar del Fin de los Tiempos", - 8208: "Tajada del Tramposo", - 8209: "Nido Ondavil", - 8210: "Máquina de Almas: Apocalipsis", - 8211: "Fosos de la Desesperación", - 8212: "Faronaar", - 8213: "Puesto Guardavil", - 8214: "Cubiles de Bestia Vil", - 8215: "Cámaras de La Arquería", - 8217: "Bosque de Jotun", - 8218: "Sagrario de los Tejestrellas", - 8219: "Nidal Puestaluna", - 8220: "Avanzada Puestaluna", - 8221: "Jardines Inferiores", - 8222: "El Athenaeum", - 8223: "Veranda de la Alta Niebla", - 8224: "Crisol Lunar", - 8225: "Puerta de Ventormenta", - 8226: "Valle de los Héroes", - 8227: "Bosque de Elwynn", - 8228: "Distrito de Mercaderes", - 8229: "Los Canales", - 8230: "Refugio Bosqueférreo", - 8231: "Terrenos de Caza Cuellipluma", - 8232: "Cuenca Colorida", - 8233: "Quebrada Susurrolunar", - 8234: "Ciudad de Ventormenta", - 8235: "Casco Antiguo", - 8236: "Castillo de Ventormenta", - 8237: "Delta de Alluvanon", - 8238: "Dreyrgrot", - 8239: "Templo Oscuro", - 8242: "Barrio de los Magos", - 8243: "El Antiguo Cuartel", - 8244: "Plaza de la Catedral", - 8245: "Distrito de los Enanos", - 8246: "Vigilia de Absolon", - 8247: "Cuesta Pezuña de Heyman", - 8249: "Terrado de los Vigías", - 8250: "Rescate de Koltira", - 8251: "Campamento Relicario", - 8252: "El Oculus", - 8253: "Avanzada Cuernorrisco", - 8254: "Paso de An'she", - 8255: "Senda de los Ettin", - 8256: "Campamento Aterravil", - 8257: "Cima del Templo", - 8258: "Sagrario Sombra Lunar", - 8259: "Falanaar", - 8260: "Cámara Acorazada de Alodi", - 8261: "Campamento del Kirin Tor Abandonado", - 8262: "Templo del Dragón de Jade", - 8263: "Puente Lanzaviento", - 8264: "Tierras del Templo de Jade", - 8265: "Karazhan", - 8266: "Puerta de Crinceniza", - 8267: "Refugio de los Thorignir", - 8268: "Gruta Luz Perdida", - 8269: "Cauce Enterrado", - 8270: "Reposo de Margoss", - 8271: "Cubil del Viajero", - 8272: "Canción de Luz", - 8273: "Cabaña de Thornton", - 8274: "Arrecife Lithduin", - 8275: "Isla Bruma Azur", - 8276: "Mar de la Bruma", - 8277: "El Exodar", - 8278: "Forestagrís", - 8279: "Akstad", - 8280: "Campos de Entrenamiento Hacharruna", - 8281: "Cantera Guardián de Piedra", - 8282: "El Martillo Vil", - 8283: "Círculo de la Gloria", - 8284: "Reducto Illidari", - 8285: "Monasterio Escarlata", - 8286: "Claustro Abandonado", - 8287: "Jardines de la Capilla", - 8288: "Mare Magnum", - 8289: "Mirador de Suertesombría", - 8290: "Costa Abrupta", - 8291: "Aposento de los Dragones Gemelos", - 8292: "Templo del Dragón de Jade", - 8293: "El Sagrario del Escribiente", - 8294: "Aposento de los Dragones Gemelos", - 8295: "El Corazón de Jade", - 8296: "Foso de Malinoth", - 8297: "Marisma Gris", - 8298: "Mulgore", - 8299: "Barrio Real", - 8300: "Refugio de Theodric", - 8301: "Cuartel de Guardia Gris", - 8302: "El Olvido", - 8303: "Vega Aullante", - 8304: "Alto del Acechador de Estrellas", - 8306: "Aldea Brisa Temprana", - 8307: "Cámara de los Susurros", - 8308: "Valle de los Suspiros", - 8309: "Tol Barad", - 8310: "Bastión de Baradin", - 8311: "Fortín del Oeste", - 8312: "Cuartel del Acorazado", - 8313: "Fortín del Este", - 8314: "Fortín del Sur", - 8315: "Rescoldos", - 8316: "Vigilia del Celador", - 8317: "Claros de Tirisfal", - 8318: "Claros de Tirisfal", - 8319: "Bastión de Baradin", - 8320: "Cumbre Comitiva Oscura", - 8321: "Fuera de control", - 8322: "Cima Faucesable", - 8323: "Campo de Soñadores", - 8324: "Ciudadela Susurravientos", - 8325: "Camino de la Arboleda Salvaje", - 8326: "Marisma Hendida", - 8327: "Roca del Velador", - 8328: "Baluarte del Halcón Negro", - 8329: "Cuenca de Anora", - 8330: "[TEMP] Placeholder Area - Level Design Land - Wind Test", - 8331: "El Fin de Heyman", - 8332: "Bahía Esmeralda", - 8333: "Grieta Enmarañada", - 8334: "Mirador Puestaluna", - 8335: "Alto Miraespíritus", - 8336: "Tumba de Sargeras", - 8337: "Garganta Marcapiedra", - 8338: "Camino Perdido", - 8339: "Dientes Quebrados", - 8340: "Mercado de Piedra", - 8341: "Ruta Escamada", - 8342: "Senda del Gusano", - 8343: "Espinazo del Dragón", - 8344: "La Sala de Cristal", - 8345: "Villa de la Primera Arcanista", - 8346: "Tierras de la Peste del Este", - 8347: "Sagrario de la Luz", - 8348: "Veranda Shal'dorei", - 8349: "Nidal Garrarrisco", - 8350: "Cerro Illidari", - 8351: "Animalario", - 8352: "Guarida de Su'esh", - 8353: "Atrio del Destino", - 8354: "Tierras de la Peste del Este", - 8355: "El Eterium", - 8356: "Santuario de la Luz", - 8357: "Capilla de la Esperanza de la Luz", - 8358: "Molino Cejifrente", - 8359: "Torre de la Muralla del Este", - 8360: "Cicatriz Pestilente", - 8361: "IV:7", - 8362: "Vrekt", - 8363: "Arboleda Salvaje", - 8364: "Refugio de Cantespada", - 8365: "Costa Escama Sombría", - 8366: "El Libramorte", - 8367: "Colmillo del Rey", - 8368: "Arrecife del Ocaso", - 8369: "Yugo de la Luz", - 8370: "Nidal de los Vigías", - 8371: "Reducto de las Celadoras", - 8372: "Saliente Roca Lejana", - 8373: "Ojo de Azshara", - 8374: "La Puerta Insigne", - 8375: "Tierras Altas Rocosas", - 8376: "Bahía de Ashil", - 8378: "Desembarco Luz de Luna", - 8379: "Mercado Engalanado", - 8380: "Corte de la Medianoche", - 8381: "Hacienda Enjoyada", - 8382: "El Creciente Menguante", - 8383: "Galería del Artesano", - 8384: "Vigilia de la Sirena", - 8385: "Bazar Lunaeterna", - 8386: "Jardín Centelleante", - 8387: "Manantial Oculto", - 8388: "La Pasarela Devastada", - 8389: "Ascenso del Astromante", - 8390: "Suramar", - 8391: "Campamento Base Cumbrealta", - 8392: "Cloacas de Dalaran", - 8393: "Hafr Fjall", - 8394: "Cabaña de Carlisle", - 8395: "Puerto de los Astravar", - 8396: "Cueva de Jefe del Mundo", - 8397: "Veranda de la Iluminación", - 8398: "Paseo del Orden", - 8399: "Elor'shan", - 8400: "Kel'balor", - 8401: "Estación Ley de Puestaluna", - 8402: "Estación Ley de Aethenar", - 8403: "Cámaras del Eclipse", - 8404: "Profundidades de Falanaar", - 8405: "Teloth'aran", - 8406: "Torreón Grajo Negro", - 8407: "Corte del Cuervo", - 8408: "Gloria del Cuervo", - 8409: "Reposo de los Héroes", - 8411: "Reposo del León", - 8412: "Cornisa Pezuñalisa", - 8413: "Tanques para Todo", - 8414: "Pieles Legendarias", - 8415: "Telas Talismánicas", - 8416: "Cubil del Fuego Fatuo Taimado", - 8417: "Campamento de Nal'ryssa", - 8418: "Exilio de Lyndras", - 8419: "Cubil de los Dementes", - 8420: "Cripta Olvidada", - 8422: "Rugido de la Tempestad", - 8423: "La Arquería", - 8425: "Bahía Esmeralda", - 8426: "Ruinas de Nar'thalas", - 8427: "Arrecifes Roca Lejana", - 8428: "Arrecifes Boca Infernal", - 8429: "Tel'anor", - 8430: "Nidal Vientos de Cambio", - 8431: "Santuario del Orden", - 8432: "El Puente del Amanecer", - 8433: "Ashran", - 8434: "Retiro del Clamaestrellas", - 8435: "Arboleda Corazón Oscuro", - 8436: "Parque Ocaso", - 8437: "Costa Abrupta", - 8439: "Gran Oscuridad del Más Allá", - 8440: "Prueba del Valor", - 8441: "Calzada Rayo de Luna", - 8442: "Nashal, el Vigilante", - 8443: "Karazhan", - 8444: "Isla Rompemar", - 8445: "Mare Magnum", - 8448: "[TEMP] JT Zandalar Swamp", - 8449: "xxOLD - Zuldazar City", - 8451: "Frontera de Vuelo del Templo del Tigre Blanco", - 8452: "Tumba de Sargeras", - 8453: "Acceso a la Tumba", - 8454: "Ciudad Negra", - 8455: "Costa Abrupta", - 8456: "Ciudad de Ventormenta", - 8457: "Huida de la Cámara", - 8458: "Cima El'dranil", - 8460: "Frontera de Vuelo de la Campaña del Caballero de la Muerte", - 8461: "Sagrario de la Iluminación", - 8463: "Altar del Fin de los Tiempos", - 8464: "Loch Varmad", - 8468: "Posada Cerveza de Trueno", - 8469: "La Vorágine", - 8470: "VictorCortisLand", - 8471: "Santuario del Orden", - 8472: "Paseo del Orden", - 8473: "Mazmorra Bloqueada", - 8474: "Dalaran (Rasganorte)", - 8476: "Villavíbora", - 8477: "Bosque de Elwynn", - 8478: "Karazhan", - 8479: "Broken Shore DO NOT USE", - 8480: "8DesertCanyonTest", - 8481: "Avance Alma Vil", - 8482: "Arena Filospada", - 8483: "El Ojo de la Eternidad", - 8485: "Ashran", - 8486: "Taller de Nyell", - 8487: "Terraza Lunaeterna", - 8488: "Catacumbas Putrefactas", - 8489: "Islas", - 8490: "Winter AB", - 8491: "[TEMP] CV Kul Tiras Island", - 8492: "Costa Brumosa", - 8493: "Roca Gaviota", - 8494: "Risco Esmeralda", - 8495: "Torre de Xylem", - 8496: "Bahía de Suramar", - 8497: "Guarida de Onyxia", - 8498: "Trono de Helya", - 8499: "Zuldazar", - 8500: "Nazmir", - 8501: "Vol'dun", - 8502: "Mares del Sur", - 8503: "Avanzada Vilificada", - 8504: "Nidorrisco", - 8505: "Acantilados Espuma Marina", - 8506: "Paso Lumbrevil", - 8507: "Caverna Lumbrevil", - 8508: "Cuenca de Arathi", - 8509: "Senda Efímera", - 8510: "Cala Nidocuervo", - 8511: "Taller Abandonado", - 8512: "Campo Eterno de Batalla", - 8513: "Cueva Húmeda", - 8514: "Gnomeregan", - 8515: "Campos Pérfidos", - 8516: "Cuenca Brechavil", - 8517: "Puesto de Liberación", - 8518: "El Ojo de la Eternidad", - 8519: "Ruinas de la Medialuna", - 8520: "Cultivos Florecientes", - 8521: "Campo de los Reyes Caídos", - 8522: "Haustvald", - 8523: "El Templo de Atal'Hakkar", - 8524: "Tumba de Sargeras", - 8525: "Catacumbas Putrefactas", - 8526: "Cuenca de Arathi", - 8527: "Catedral de la Noche Eterna", - 8528: "Cuevas de los Lamentos", - 8529: "Foso de Saron", - 8530: "Quebranto Demoníaco", - 8531: "Cervecería del Trueno", - 8532: "El Banco de Dalaran", - 8534: "Guarida Extraordinaria", - 8535: "Dalaran (Paso de la Muerte)", - 8536: "Islote Muertobosque", - 8537: "Alma Temible", - 8538: "Vacío Abisal", - 8540: "Nave de Mando de la Legión", - 8541: "El Vacío Abisal", - 8542: "Ascenso Luz de Luna", - 8543: "Bancal Sollozante", - 8544: "Cobertizo del Desterrado", - 8545: "Gruta Insidiosa", - 8546: "Cueva de los Tótem Sangriento", - 8547: "Costa de la Angustia", - 8548: "Playa Iravil", - 8549: "La Vorágine", - 8550: "Portal Interceptado", - 8551: "Baluarte de la Vileza", - 8552: "Valle Escarpado", - 8553: "Caída de Wrynn", - 8554: "Puesto de Liberación", - 8555: "Acantilados de los Lamentos", - 8556: "Stratholme", - 8557: "Franja Orillavil", - 8558: "Caverna Nube Vil", - 8559: "Cavernas Tótem Vil", - 8560: "Alto de la Venganza", - 8561: "El Vacío Abisal", - 8562: "Broken Valley - Area 2 [PH]", - 8563: "Broken Valley - Area 3 [PH]", - 8564: "Broken Valley - Area 4 [PH]", - 8565: "Broken Valley - Area 5 [PH]", - 8566: "Mare Magnum", - 8567: "Estrecho de Tiragarde", - 8568: "Boralus", - 8569: "Cresta Carbón", - 8570: "Val'sharah", - 8571: "Abismo Desolado", - 8572: "Fauces de Corrupción", - 8573: "MSandersTest2016", - 8574: "Krokuun", - 8576: "Cala Dentada", - 8577: "Camino de Ceniza", - 8579: "Isla de la BlizzCon", - 8580: "Acantilado de los Carroñeros", - 8581: "Asalto a la Costa Abrupta", - 8582: "AI Test Map 8 - Arathi Basin", - 8583: "Torreón Grajo Negro", - 8584: "Ruina de Almas", - 8585: "Guarida de Malignos", - 8586: "Costa de la Melancolía", - 8587: "Desembarco Muertobosque", - 8588: "Foso de la Agonía", - 8589: "Dalaran", - 8590: "Tormento del Soldado", - 8591: "Taberna de Shwayder", - 8592: "Cámara de los Jurapenas", - 8593: "Fisura sombría", - 8594: "Cámaras del Valor", - 8595: "Forja de la Corrupción", - 8596: "Tormenheim", - 8597: "Batalla por la Montaña Roca Negra", - 8598: "Warden Tower TEMP", - 8600: "Mare Magnum", - 8601: "Parque de Animaciones", - 8602: "Estacada Oscura", - 8603: "Marisma Aleta de Piedra", - 8604: "Torreón Grajo Negro", - 8605: "Camino Infernal", - 8606: "Taberna del Fin del Mundo", - 8607: "El Yeti Risueño", - 8608: "El Exobar", - 8609: "Posada de Amparo de la Noche", - 8610: "El Guerrero del Camino", - 8611: "Posada del Oasis", - 8612: "Taberna de la Luciérnaga", - 8613: "Taberna El Cohete Oxidado", - 8614: "El Gato y el Chamán", - 8615: "Taberna del Grumete Frito", - 8616: "Taberna La Quilla Rota", - 8617: "El Cerdo Borracho", - 8618: "El Colmillo Partido", - 8619: "Posada de Ciudad de Lunargenta", - 8620: "La Bodega", - 8621: "La Cola del Dracoleón", - 8622: "Taberna Roca de Fuego", - 8623: "Mesón del Cuervo Escarlata", - 8624: "Enfrentamiento del Shadopan", - 8625: "Azsuna", - 8630: "Las Minas de la Muerte", - 8634: "[TEMP] Hackaton CV", - 8635: "El Trono Helado", - 8636: "[TEMP] Hackaton Race", - 8637: "[TEMP] Hackaton Race", - 8638: "Antorus, el Trono Ardiente", - 8639: "Test Dungeon", - 8640: "Valle de Crestanevada", - 8641: "Campos de la Cacería Eterna", - 8642: "Cornisa Vuelovil", - 8643: "Senda del Sufrimiento", - 8644: "Plataforma de Invocación", - 8645: "Cámaras del Valor", - 8646: "Monte Alto", - 8647: "Gnomeregan", - 8648: "Punto de Aalgen", - 8650: "Cuenca Abisal", - 8651: "Mardum, el Abismo Devastado", - 8652: "Empty Never Nowhere", - 8653: "Santuario de Aviana", - 8654: "Límite del Mundo", - 8656: "CharacterTestSpaceArea_ECannon", - 8657: "Salas de Comunión", - 8658: "Bastión de Aterratrols", - 8659: "Guarida de los Rapiñadores", - 8660: "Trono de los Cuatro Vientos", - 8661: "[PH] JT Test World", - 8662: "Cumbre de Rhut'van", - 8663: "Cumbre de Rhut'van", - 8664: "Muelle del Trueque", - 8665: "Puerto de Zandalar", - 8666: "Gran Bazar", - 8667: "Tal'farrak", - 8668: "Tal'aman", - 8669: "Embajada Gurubashi", - 8670: "Dazar'alor", - 8671: "Altar de Sanbwoni", - 8672: "El Sagrario Rubí", - 8673: "Recoveco Krokul", - 8674: "Vértice Caída Oscura", - 8675: "Salas Umbrías", - 8676: "Glaciar Perdido", - 8677: "Tramo Congelado", - 8678: "Reposo de Kyranastrasz", - 8679: "Plaza Degolladero", - 8681: "Elunaria", - 8682: "Fin del Triunvirato", - 8683: "Fangorrana", - 8684: "AI Test Map - Arathi Basin (Debug Version)", - 8685: "Garganta Grito de Guerra", - 8686: "Bastión Ala de Plata", - 8687: "Serrería Grito de Guerra", - 8688: "Ciénaga Fluvial", - 8689: "Ruinas de Zul'jan", - 8690: "Zul'Nazman", - 8691: "Avanzada Masacrada", - 8692: "Lavado de sangre", - 8693: "Rivera Quebrada", - 8694: "Trono de la Aniquilación", - 8695: "Fosos de la Aniquilación", - 8696: "Bosque Petrificado", - 8698: "Tonel y Yunque", - 8699: "Nazmir [DEVLAND COPY]", - 8700: "El Exodar", - 8701: "Eredath", - 8702: "Ruinas de Oronaar", - 8703: "Trono del Triunvirato", - 8704: "Punto de Éxodo", - 8705: "Derrumbe de Oronaar", - 8706: "Isolon", - 8707: "Fin del Litoral", - 8708: "Bastión de Aterratrols", - 8709: "Sombrero de Akazamzarak", - 8710: "Sagrario de Alturus", - 8711: "Cuenca Tenebrosa", - 8712: "Prueba de Estilo", - 8713: "Parque de Animaciones", - 8714: "El Vindicaar", - 8715: "Nido sangriento", - 8716: "Fuerte Valiente", - 8717: "Puerto de Boralus", - 8718: "Mercado Vientos Alisios", - 8719: "Redil Bestia de Guerra", - 8720: "Tumba de Sargeras", - 8721: "Drustvar", - 8722: "Puerta de Sangre", - 8723: "Frente Salvaje", - 8724: "Raz'lotec", - 8725: "Bancal de Guerra", - 8726: "Zanchul", - 8727: "Jardín de los Loa", - 8728: "Santuario de Pa'ku", - 8729: "Resquicio Frondoso", - 8730: "Meseta de Naz", - 8731: "Monte Mugamba", - 8732: "Templo del Profeta", - 8733: "Zeb'ahari", - 8734: "Templo de Rezan", - 8735: "Los Baldíos del Norte", - 8736: "Durotar", - 8737: "Playa Lanza Negra", - 8738: "Poblado Sen'jin", - 8739: "Afueras del Poblado Sen'jin", - 8740: "Cuencas de Umbra", - 8741: "Incursión de la Guardia de las Sombras", - 8742: "Ciudadela del Fuerte del Norte", - 8743: "La Costa Mercante", - 8744: "Islas del Eco", - 8746: "Caída de Zalazane", - 8747: "Mare Magnum", - 8748: "Costa Lanza Negra", - 8749: "Isla Lanza Negra", - 8750: "Costa Garrasangre", - 8751: "Valle de los Retos", - 8752: "Valle de los Retos", - 8753: "Roca de los Espíritus", - 8754: "Sendero Oculto", - 8755: "Isla de Batalla", - 8756: "Trinquete", - 8757: "El Escalón de la Marea", - 8758: "Río Furia del Sur", - 8759: "Río Furia del Sur", - 8760: "Los Baldíos del Sur", - 8761: "El Oasis Estancado", - 8762: "El Cruce", - 8763: "Oasis Aguaverde", - 8764: "Camino del Oro", - 8765: "La Hojarasca", - 8766: "La Gran División", - 8767: "Mulgore", - 8768: "Roca Roja", - 8769: "Cerro del Cazador", - 8770: "La Carretera", - 8771: "El Alto del Honor", - 8772: "Las Llanuras Doradas", - 8773: "Cresta Viento Furioso", - 8774: "Campamento Una'fe", - 8775: "Ruinas de Taurajo", - 8776: "La Gran Puerta", - 8777: "Campos de Sangre", - 8778: "Punta Vendetta", - 8779: "Las Llanuras Onduladas", - 8780: "Pozo Pezuña Invernal", - 8781: "Poblado Pezuña de Sangre", - 8782: "Lago Toro de Piedra", - 8783: "Caravana Devastada", - 8784: "Mina Ventura y Cía.", - 8785: "Campamento Narache", - 8786: "Barranco Cortazarza", - 8787: "Mesa de la Nube Roja", - 8788: "El Gruñospina", - 8789: "Feralas", - 8790: "Filón Catacroquer", - 8791: "Patrulla de Trenzado", - 8792: "Marca de Guerra", - 8793: "Bastión de la Desolación", - 8794: "Bajas Tierras Salvajes", - 8795: "Nueva Thalanaar", - 8796: "Las Mil Agujas", - 8797: "Cima Tramo Oeste", - 8798: "El Gran Elevador", - 8799: "Cumbre de la Nube Negra", - 8800: "Zahúrda Rajacieno", - 8801: "Dustwallow Marsh", - 8802: "El Pantano del Dragón", - 8803: "Excavación de Bael Modan", - 8804: "Bael Modan", - 8805: "Marjal Revolcafango", - 8806: "Posada Reposo Umbrío", - 8807: "Punta de Lanza", - 8808: "Punta Piedra de Fuego", - 8809: "Madriguera de los Cienorrojo", - 8810: "Fuerte Triunfo", - 8811: "Punta Perdida", - 8812: "Torre de la Punta Norte", - 8813: "Ciénaga Azul", - 8814: "Cueva Niebla Negra", - 8815: "Poblado Murohelecho", - 8816: "Cubil de la Llama", - 8817: "Ruinas Quebrantarrocas", - 8818: "Piñón de Barro", - 8819: "Ciénaga de Fuego", - 8820: "Siniestro de Beezil", - 8821: "Granja de Tabetha", - 8822: "Puesto Cuernoatroz", - 8823: "El Lodazal", - 8824: "Colina de las Brujas", - 8825: "Bahía Revolcafango", - 8826: "Cala Furiamarea", - 8827: "Paraje Pesadilla", - 8828: "Campamento Hojarasca", - 8829: "Mando de Vanguardia", - 8830: "Poblado Pezuñanegra", - 8831: "Fuerte del Norte", - 8832: "Expedición de Teegan", - 8833: "Mare Magnum", - 8834: "Isla de Alcaz", - 8835: "Playa Tenebruma", - 8836: "Mansión Cienaluz", - 8837: "Alto del Centinela", - 8838: "Embarcadero de Nat", - 8839: "Isla Theramore", - 8840: "Isla Bruma Azur", - 8841: "Conservatorio de la Tríada", - 8842: "El Exodar", - 8843: "El Enclave Umbral", - 8844: "Isla Media Luna", - 8845: "Guarida del Tesoro Secreta", - 8846: "Reducto de Tiragarde", - 8847: "Sototumba", - 8848: "Gol Inath", - 8849: "Fin del Destino", - 8850: "Tierras Salvajes", - 8851: "Kazai", - 8852: "Hacienda Valarroyo", - 8853: "Paso de los Prisioneros", - 8854: "Escondrijo Vulpera", - 8855: "Madrigueras Abandonadas", - 8856: "Páramo Rocoso", - 8857: "Cauce Salobre", - 8858: "Llanuras Sinuosas", - 8859: "Corte de Zak'rajan", - 8860: "Ruinas de Atul'Aman", - 8861: "Templo de Akunda", - 8862: "Páramos Abrasadores", - 8863: "Valle de la Desdicha", - 8865: "Refugio de Randall", - 8866: "Cala Selvamuerta", - 8867: "Campamento Gobernalle", - 8868: "Mirador del Trueno", - 8869: "Santuario de los Devotos", - 8870: "Marisma Olvidada", - 8871: "Cimas Rocosas", - 8872: "Sagrario de Vorrik", - 8873: "Enclave de los Exiliados", - 8874: "Barranco Sinuoso", - 8875: "Aguja de los Clamacielos", - 8876: "Grieta Rocosa", - 8877: "Paso del Hombre Muerto", - 8878: "Desfiladero del Trueno", - 8879: "Manantial Eterno", - 8880: "Bajíos Hundidos", - 8881: "Bazar de Luz Azur", - 8882: "Conservatorio de lo Arcano", - 8883: "El Pretorio", - 8884: "Jardines de Arinor", - 8885: "Bancal de Kil'jaeden", - 8886: "Reflejo del Profeta", - 8889: "Cementerio de Dragones", - 8890: "Tierras de la Peste del Oeste", - 8891: "Monte Hyjal", - 8892: "La Matanza de Stratholme", - 8893: "Pozo de la Eternidad", - 8896: "Claro Acechabestias", - 8897: "Necrópolis", - 8899: "Baldío Antoran", - 8900: "El Zócalo", - 8901: "El Caldero del Rock", - 8902: "Fundición Gobernalle", - 8903: "Estación Punto Este", - 8904: "Coto de Caza Valarroyo", - 8905: "Incursión de la Guardia de las Sombras", - 8906: "Cuencas de Umbra", - 8907: "Conservatorio de la Tríada", - 8908: "El Enclave Umbral", - 8909: "Trono del Triunvirato", - 8910: "Trono del Triunvirato", - 8911: "El Arcatraz", - 8913: "Ruinas de Zo'bal", - 8914: "Plaza de Luz Azur", - 8915: "El Vindicaar", - 8916: "El Vindicaar", - 8917: "Wellington", - 8918: "Puerto de Zem'lan", - 8919: "Oasis Silbaflor", - 8920: "Costa Escarpada", - 8921: "Tal'gurub", - 8922: "Koramar", - 8923: "Remanso Frondoso", - 8924: "Río Quijatajo", - 8925: "Travesía de Nesingwary", - 8926: "Pantano Podrerraíz", - 8927: "Reserva de Caza", - 8928: "El Escape", - 8929: "Temple of Kimbul - DO NOT USE", - 8930: "Bancal del Colmillo", - 8931: "Marisma Selva Umbría", - 8932: "Fangorrana Superior", - 8933: "Campos Devastados", - 8934: "Sendarraíz", - 8935: "Refugio de los Rebanacabezas", - 8936: "Corona de Destrucción", - 8937: "Santuario de Dragones Obsidiana", - 8938: "Santuario de Dragones Rubí", - 8939: "Santuario de Dragones Esmeralda", - 8940: "Santuario de Dragones Azur", - 8942: "Zal'amak", - 8944: "Caverna de Llamacolmillos", - 8945: "Naz'agal", - 8946: "Enclave del Heraldo", - 8947: "Extensión Desgarrada", - 8948: "Ruinas Inhóspitas", - 8949: "Cicatriz del Impostor", - 8950: "Ruinas de Lordaeron", - 8951: "Guarida del Alto Inquisidor", - 8953: "Puertopuente", - 8954: "Puesto Proveescamas", - 8955: "Dev Area - H (copy)", - 8956: "Gol Thovas", - 8957: "Dev Area - I", - 8958: "Posada Colmillo Dorado", - 8959: "Cresta Restallante", - 8960: "Atul'Aman", - 8961: "Puerto Ruina Pétrea", - 8962: "Bancal de los Devotos", - 8963: "Meseta Peñarroja", - 8964: "Puerto Peñarroja", - 8965: "Xibala", - 8966: "Zalamar", - 8967: "Bancal de los Pesares", - 8968: "Humedales Primigenios", - 8969: "Guarida de Urok", - 8970: "Nazwatha", - 8971: "Reposo de Torga", - 8972: "Atul'Nazman", - 8973: "Corazón de la Oscuridad", - 8974: "Trono del Triunvirato", - 8975: "El Xenedar", - 8976: "Charca de Krag'wa", - 8977: "Fangal Olvidado", - 8978: "Tol Dagor", - 8979: "Prototipo Principal de Frentes de Guerra", - 8980: "Salones Infinitos", - 8994: "Huida de Argus", - 9004: "Caverna Profunda", - 9006: "Campo del Relicario", - 9007: "Campamento de la Liga de Expedicionarios", - 9008: "Ciénaga del Pavor", - 9009: "Sagrario de la Garra", - 9010: "Sombra de Azeroth", - 9011: "Hacienda de Norwington", - 9012: "Forja de los Eones", - 9021: "Fauces de N'Zoth", - 9022: "Embajada de Orgrimmar", - 9023: "[Test] Environment Art - Tina 01", - 9024: "Punto de Invasión", - 9025: "Punto de Invasión", - 9026: "Puerto del Tallador", - 9028: "Atal'Dazar", - 9029: "Isla Hozen", - 9032: "Albergue del Ocaso", - 9033: "Laderas Brumosas", - 9034: "Rancho de Albergue del Ocaso", - 9035: "Santuario de Shadra", - 9036: "Trono de Rezan", - 9037: "Cavernas de Consumo", - 9038: "Fuerte de Nath'raxas", - 9039: "Pantano de Sangre", - 9040: "Punto de Destino", - 9041: "Natha'vor", - 9042: "Valle Canto Tormenta", - 9043: "Camorra de Silithus", - 9044: "Corte del Vengador", - 9045: "Atal'Gral", - 9046: "Playa del Carroñero", - 9047: "Fosas de Alquitrán Shoaljai", - 9048: "Orilla de Krag'wa", - 9049: "Meseta Antul'Mita", - 9050: "Aguja de Nath'raxas", - 9051: "Las Muertes de Cromi", - 9052: "Bosque de Vallecánido", - 9059: "Grieta Bruma Otoñal", - 9078: "Fuerte Libre", - 9099: "[TEMP] CV JulienTest", - 9100: "Punto de invasión: Aurinor", - 9101: "Tropical Isle 8.0", - 9102: "Punto de invasión: Naigtal", - 9105: "Valle de la Flor Eterna", - 9106: "Punto de Invasión", - 9124: "Embarcadero Puntamuelle", - 9125: "Cala Aterrakraken", - 9126: "Punto de invasión: Cen'gar", - 9127: "Punto de invasión: Val", - 9128: "Punto de invasión: Sangua", - 9129: "Punto de Invasión", - 9130: "Punto del Verdugo", - 9131: "Bosque del Vigía", - 9132: "Terreno de Hawthorne", - 9133: "Zul'Ahjin", - 9134: "Gol Koval", - 9135: "Canal Ululante", - 9136: "Costa Hirviente", - 9137: "Desfiladero Oscuro", - 9138: "Guarida Velada", - 9139: "Asidero de la Luz", - 9140: "Desembarco de la Esperanza", - 9141: "Sima Devastada", - 9143: "Fosas de Castigo", - 9144: "Mirador del Corruptor", - 9145: "Brecha Ardiente", - 9146: "El Vindicaar", - 9147: "Antiguo Dique", - 9148: "Cuenca del Flechero", - 9149: "Alto del Asedio Vil", - 9150: "Círculo de Aspirantes", - 9151: "Núcleo de Estasis", - 9152: "Arsenal Lumbrevil", - 9153: "Terminal", - 9154: "Guarida de Ven'orn", - 9155: "Paso del Maligno", - 9156: "Cementerio de Carroñeros", - 9157: "Crisol de los Espíritus", - 9158: "Altar de Ritos Tenebrosos", - 9159: "Alto del Creador", - 9160: "Laboratorio Pútrido", - 9161: "Camino Profanado", - 9162: "Recoveco del Coleccionista", - 9163: "Julien Test Land", - 9164: "Fuerte Libre", - 9165: "Mina Rocavieja", - 9166: "Santuario de Xibala", - 9167: "Cementerio Lomatúmulo", - 9168: "Tierras Altas de Arathi", - 9169: "Santuario de Kel'vax", - 9170: "Cala Deforestada", - 9171: "Embajada de Ventormenta", - 9172: "Bancal de los Elegidos", - 9173: "El Zócalo", - 9174: "Yermos de Argus", - 9176: "Arboleda Inusitada", - 9177: "Guarida de Rocaescarcha", - 9178: "Tierra Inalcanzable", - 9179: "Frente Sethrak", - 9180: "Punto de invasión: Bonich", - 9181: "Arboleda de Naroua", - 9183: "RiverZone_Art", - 9186: "El Vacío Abisal", - 9187: "Núcleo Vil", - 9188: "Falla de Telogrus", - 9191: "Zeb'Saur", - 9192: "Alto del Pterror", - 9194: "Corte de los Espíritus", - 9195: "Explanada Sombría", - 9198: "Cuenca de Isolon", - 9201: "Guarida de Hir'eek", - 9202: "Barranco Marchito", - 9207: "Ciudad de Suramar", - 9222: "Valleotoño", - 9223: "Campamento del Mercader", - 9224: "Cavernas del Paso del Norte", - 9225: "Vega Árida", - 9226: "Avanzada Arenas Calcinadas", - 9227: "Campo de Carroña", - 9228: "Liza Arenas Calcinadas", - 9229: "Túmulo Funerario", - 9230: "Crumbling OverlookDELETE JDA", - 9231: "Risco Puaespina", - 9232: "Cueva Puaespina", - 9233: "Guarida de la Desesperación", - 9234: "Altar del Mimbre", - 9235: "Puerto de Guerra Rastari", - 9236: "Residencia Kennings", - 9263: "Cascadas Saltarroca", - 9264: "Cabaña Fangopez", - 9265: "Costa Chascapinzas", - 9266: "Meseta Verdeante", - 9268: "Alto Alpino", - 9269: "Camino Paso Alto", - 9270: "Entrañas de Torga", - 9271: "Senda del Cazador", - 9272: "Paso Fuenteclara", - 9273: "Cuenca Cerval", - 9274: "Cabaña Olvidada", - 9275: "Gol Var", - 9278: "Arena de Kul Tiras", - 9279: "Puntagarfio", - 9280: "Filo de la Discordia", - 9281: "Cubil de los Devoradores", - 9295: "Punto de invasión superior: Matrona Folnuna", - 9296: "Punto de invasión superior: Señor del foso Vilemus", - 9297: "Punto de invasión superior: Inquisidor Meto", - 9298: "Punto de invasión superior: Occularus", - 9299: "Punto de invasión superior: Sotanathor", - 9300: "Punto de invasión superior: Maestra Alluradel", - 9302: "Trono de la Ascensión", - 9303: "Guarida del Alquimista", - 9304: "Acantilados de la Tempestad", - 9305: "Punto del Náufrago", - 9306: "Elunaria", - 9307: "Acantilados Quebrados", - 9308: "Enclave del Observador", - 9309: "Minas Peñarroja", - 9310: "Silithus: La Herida", - 9311: "Campamento de Guerra Sethrak", - 9312: "Incursión en el Templo", - 9313: "Ciudad de Lunargenta", - 9314: "Campamento Base Thovas", - 9317: "Relato del Muerto", - 9318: "La Fuente del Sol", - 9319: "Campamento Última Brisa", - 9321: "Catacumbas de la Necrópolis", - 9322: "Reposo del Velaje", - 9323: "Alto Calaescama", - 9324: "Bajíos Perlatriste", - 9325: "Guarida de Xumanu", - 9327: "Tol Dagor", - 9328: "Cuenca Olvidada", - 9329: "Avanzada de los Buscadores", - 9330: "Gilded Docks of Zak'rajan DELETE JDA", - 9331: "Isla de Gilneas", - 9332: "Zak'rajan's Throne Room DELETE JDA", - 9333: "Santuario Elarian", - 9334: "Almacén de Maderas Algerson", - 9335: "Vaderia", - 9336: "Fauces de la Sierpe", - 9337: "Espesura del Viento del Oeste", - 9338: "Glaciar Menguante", - 9339: "Puesto de Arom", - 9345: "Ruta Dorada", - 9346: "Arenas Sagradas", - 9347: "Foso de Huesos", - 9348: "Alto del Esclavista", - 9352: "Nido de Rakjan", - 9353: "Pico de Rakjan", - 9354: "Asedio de Boralus", - 9355: "Pequeña Tortolla", - 9356: "Vía Vaporosa", - 9357: "Abrazo de Arathor", - 9358: "Arboleda Siempreverde", - 9359: "El Vindicaar", - 9360: "Termas Fuenteburbuja", - 9361: "Bancal de los Portavoces", - 9362: "Círculo del Botín", - 9363: "Horado Lomozarza", - 9364: "Senda del Conquistador", - 9365: "Risco Troncohelecho", - 9366: "Brennadam", - 9367: "Plaza de Brennadam", - 9368: "Ribera del Marinero", - 9369: "Fuerte Sabio", - 9370: "Fuerte Daelin", - 9371: "Aguja de Asalto Sethrak", - 9372: "Ensenada Risconiebla", - 9373: "Brecha Tajoarena", - 9374: "Tierras Anegadas", - 9375: "Fuentes Termales Caída Libre", - 9377: "Relato del Muerto", - 9378: "Puerta de Daelin", - 9379: "Alto del Vigía", - 9380: "Viejo Camino Drust", - 9381: "Estación Vientosur", - 9382: "Fosa de Sacrificios", - 9383: "BlizzCon 2017", - 9384: "Campamento Bellaco", - 9385: "Gol Osigr", - 9386: "Allied - Highmountain Tauren Unlock", - 9387: "Allied - Lightforged Draenei Unlock", - 9388: "Hero Stage LD Demo Area", - 9389: "Uldir", - 9391: "Catacumbas Putrefactas", - 9392: "Isla extraña de la BlizzCon", - 9393: "Fortaleza Petragrís", - 9394: "Aliadas - Orgrimmar", - 9395: "Aliadas - Ventormenta", - 9396: "Allied - Void Elf Unlock", - 9397: "Allied - Nightborne Unlock", - 9398: "Cámara de Martijoya", - 9400: "Dique Addington", - 9401: "Costa Enjoyada", - 9402: "Aldea Muelerroca", - 9403: "Delta Olarrasa", - 9404: "Atal'Dazar", - 9405: "Distrito de Empaquetado Puntamuelle", - 9406: "Paso de los Portavoces", - 9407: "Almacén de la Compañía Gobernalle", - 9408: "Profundidades Anegadas", - 9409: "Cloacas Encharcadas", - 9410: "Módulo Penitenciario: Scillus", - 9411: "Módulo Penitenciario: Karybis", - 9412: "Reducto del Sobrestante", - 9413: "Cuartel de los Oficiales", - 9414: "Arsenal de los Gobernalle", - 9415: "Falla de Telogrus", - 9416: "Médano del Halcón", - 9417: "Gruta del Errante", - 9418: "BoralusDev", - 9419: "Aldea de las Vides", - 9420: "Desfiladero de los Gorilas", - 9421: "Río Bocarrey", - 9422: "Hacienda Campodorado", - 9423: "Olas Ámbar", - 9424: "Mansión Crestavía", - 9425: "Sala del Maestro de Caza", - 9426: "Patio Frondoso", - 9427: "La despensa", - 9428: "Gran Recepción", - 9429: "Ático Olvidado", - 9430: "Sagrario de Nazwatha", - 9432: "Cima del Vigía", - 9433: "Acogida Petragrís", - 9434: "Barranco Fuegosangre", - 9435: "Costa de Sangre", - 9436: "Templo Hendido", - 9437: "Gruta de Gral", - 9439: "Tierras Altas de Arathi", - 9440: "Espesura Verdeante", - 9441: "Taller Marsalobre", - 9442: "Armadores Layson e Hijos", - 9443: "Isla de los Titanes", - 9444: "Hacienda de Burton", - 9455: "Nido Alto", - 9456: "Círculo de los Elementos", - 9457: "Cumbre Destral", - 9458: "Mina Mostacho Seco", - 9459: "Caída de Galen", - 9460: "Mina de Galson", - 9461: "Aserradero de las Tierra Altas", - 9462: "IsleCVTest", - 9463: "Lugar Sombrío", - 9464: "Cala Borrascosa", - 9465: "Costa Carmesí", - 9466: "Lodazal Hediondo", - 9467: "Islas Meseta Blanca", - 9468: "IsleCVTest2", - 9469: "Ruinas de Un'gol", - 9470: "Granja de Hidromiel Mildenhall", - 9471: "Llanuras de las Tierras Altas", - 9472: "Vega de Cristal", - 9473: "Punta de Corzocelada", - 9474: "Aldea del Viento del Sur", - 9475: "Avanzada Crepuscular", - 9476: "Colmen'Ashi", - 9477: "Colmen'Regal", - 9478: "Colmen'Zora", - 9479: "Drustvar", - 9481: "Prototipo Maldito", - 9482: "Costa Salobre", - 9483: "Isla Katalina", - 9485: "Paraje de las Maravillas", - 9486: "lost world [ph]", - 9487: "Paraje de las Maravillas", - 9488: "Isla Pantanosa", - 9489: "Cumbre Rocosa", - 9491: "Escupidera Salina", - 9492: "Senda del Valor", - 9493: "Cruce Redilnorte", - 9494: "Ar'gorok", - 9495: "Castillo de Stromgarde", - 9496: "Cayo Volcánico", - 9497: "Cuenca Arácnida", - 9499: "Finca Nueva", - 9500: "Mina de las Colinas Sombrías", - 9502: "Bastión Nocturno", - 9509: "Corlain", - 9510: "Arsenal de Corlain", - 9511: "Restos de Naufragio", - 9512: "Cascada", - 9513: "Lugar del Accidente", - 9514: "Mirador", - 9515: "Fosos de Alquitrán", - 9516: "Ruinas", - 9517: "Torre", - 9518: "El Salto", - 9519: "Pozas de las Mareas", - 9520: "Fogata", - 9521: "Cresta", - 9522: "Templo", - 9523: "Bosque Carmesí", - 9524: "Tul'vor", - 9525: "Altar de la Tormenta", - 9526: "Reposo de los Reyes", - 9527: "Templo de Sethraliss", - 9528: "Territorio inexplorado", - 9529: "Nave de la Alianza", - 9530: "Empresa Maderera de Scripps", - 9531: "Borde de las Profundidades", - 9532: "Muelle del Trillador", - 9533: "Derrumbamiento Sendarraíz", - 9535: "Francesco test - internal only", - 9536: "Mina Hondoinvierno", - 9537: "Cuatro Aguijones", - 9539: "Ruta Hundida", - 9540: "Aldea Flor de Nieve", - 9541: "Pandaren Farm Dev", - 9542: "Avanzada Colmiscama", - 9543: "Mugambala", - 9545: "Huerto de Hocking", - 9546: "Cresta del Piquero", - 9547: "Halconera de Corlain", - 9548: "Bosqueamparo", - 9552: "Entrenamiento de Misión", - 9553: "Ciudad de Ventormenta", - 9554: "Monasterio Canto Tormenta", - 9555: "Tierras Bajas Peñarroja", - 9556: "Refugio Tortuka", - 9557: "Campamento de Torka", - 9558: "Fosas de Alimentación", - 9559: "Templo de Kimbul", - 9560: "Corlain Superior", - 9561: "Mansión Crestavía", - 9562: "Almaespina", - 9563: "Guarida Furiarena", - 9564: "Virtud de la Doncella", - 9565: "Cráter de los Conquistadores", - 9566: "Reposo del Navegante", - 9567: "Caída de Bwoljin", - 9568: "Colinas Brumamorta", - 9569: "Santuario del Paso de los Caídos", - 9570: "Zuldazar", - 9571: "Dazar'alor", - 9572: "Resquicio Frondoso", - 9573: "Bancal de los Elegidos", - 9574: "Zanchul", - 9575: "Apiario de Rosaline", - 9576: "Cadena Aterradora", - 9577: "Tramo Aguapesar", - 9578: "Puerto Olaniebla", - 9579: "Cruce de la Marea", - 9580: "Campamento de Hondoinvierno", - 9581: "Ribera Inerte", - 9582: "Punto de Vinta", - 9583: "Granja de Deal", - 9584: "Pasaje Abandonado", - 9585: "Cripta Fétida", - 9586: "La Fuente de Sangre", - 9587: "Capilla de la Arboleda Blanca", - 9588: "Puntagarfio", - 9589: "Xal'vor", - 9591: "Arrecife Susurrante", - 9592: "Fuerte Victoria", - 9593: "Plaza de la Unidad", - 9594: "Melodía Abisal", - 9595: "Zanja Olarrasa", - 9597: "Guarida de Tongo", - 9598: "Gran Sello", - 9599: "Bendición de los Espíritus", - 9601: "Riachuelo Aguarrubor", - 9602: "Desembarco del Marinero", - 9603: "Isla Diente Marfil", - 9604: "Isla Colmillo", - 9606: "Cabaña de Eckhart", - 9607: "Llanos Salobres", - 9608: "Encrucijada de Arom", - 9609: "Campamento de Magni", - 9611: "Mercado Puntagarfio", - 9612: "Crackling RidgeDELETE JDA", - 9613: "Orilla Velada", - 9614: "Muelle Mercante", - 9615: "Arboleda Copacinérea", - 9616: "Jorundall", - 9617: "Calacualquiera", - 9618: "Rugido de la Tempestad", - 9619: "Reproche de Talanji", - 9620: "Sagrario del Hablaolas", - 9621: "Filo de la Tormenta", - 9622: "Tramo del Kraken", - 9623: "Cumbre Rompeolas", - 9624: "Cuenca Verdeante", - 9625: "Guarida de Thaelin", - 9626: "Laderas Rompeolas", - 9627: "Ágora Cruzaviento", - 9628: "Colinas Ventadas", - 9629: "Granja de Teller", - 9630: "Isla Dorada", - 9631: "Sala Puño de Roca", - 9632: "Barranco Pistahueso", - 9633: "Nido Rajacorteza", - 9635: "Bosque Saltarroca", - 9636: "Hogar Bosquenorte", - 9637: "Manantial Plácido", - 9638: "Chatarralandia", - 9639: "Círculo del Botín", - 9640: "Fuga del Contrabandista", - 9642: "Costa Parda", - 9643: "Cuenca Hondoinvierno", - 9644: "Avanzada Leñotalado", - 9645: "Deriva Sur", - 9646: "Dunas del Este", - 9647: "Nidal del Halcón", - 9648: "Aldea Brisamarina", - 9649: "Bajíos Relucientes", - 9651: "Model Test World", - 9652: "Altar de los Susurros Ahogados", - 9653: "Salas de Comunión", - 9654: "Playa Rojocieno", - 9655: "Avanzada Pernochispa", - 9656: "Cumbre de los Escarbadores", - 9657: "Puerto Ruina Pétrea", - 9658: "Vol'dun", - 9659: "Drustbar", - 9660: "El Tamborilero Beodo", - 9661: "Salón del Espíritu Perdido", - 9662: "Casa Termal", - 9663: "Campaña de Guerra: Alianza", - 9664: "Campaña de Guerra: Horda", - 9665: "Atal'Jani", - 9666: "Profundidades de Roca Negra", - 9667: "Cámara del Corazón", - 9668: "Altar de la Tormenta", - 9669: "Mares del Sur", - 9670: "Paso Borrascoso", - 9671: "Caverna Velada", - 9672: "Ciudad de Ventormenta", - 9673: "Montañas de Elwynn del Norte", - 9674: "Puerto de Ventormenta", - 9675: "Reposo del León", - 9677: "Torre del Velador", - 9678: "Cuesta Verdeante", - 9679: "Tierras Altas de Arathi", - 9680: "Ar'gorok", - 9681: "Mina Mostacho Seco", - 9682: "Círculo de los Elementos", - 9683: "Cruce Redilnorte", - 9684: "Nido Alto", - 9685: "Senda del Valor", - 9686: "Cumbre Destral", - 9687: "Finca Nueva", - 9688: "Aserradero de las Tierra Altas", - 9689: "Castillo de Stromgarde", - 9690: "Mina de Galson", - 9691: "Excavación Carroñera", - 9692: "Damarcus_World", - 9693: "Vista de los Buscadores", - 9694: "Puerto Saqueo", - 9695: "Viejo Camino Mercantil", - 9696: "Fuerte Guerracolmillo", - 9697: "Monte Talado", - 9698: "Mina Piedra de Sal", - 9699: "Punto Vigía del Mar", - 9700: "Glacial Veloaterido", - 9701: "Reposo de Vadekius", - 9702: "Círculo de Vínculo Interior", - 9703: "Allied - Dark Iron Dwarf Unlock", - 9704: "Allied - Mag'har Orc Unlock", - 9713: "Guarida Colmillo Gruñente", - 9714: "Expedición de los Buscadores", - 9715: "Campamento Náufrago", - 9716: "Guardia de la Avanzada", - 9717: "Bahía de los Reyes", - 9718: "Paso de las Tierras Altas", - 9719: "Lago Penacho", - 9720: "Círculo de Vínculo Interior", - 9721: "Zona Militarizada", - 9722: "Minas Potra", - 9723: "Cohetzan", - 9724: "Trayectoria: Bar, Asador y Campo de Minas", - 9725: "I+D", - 9726: "Villaescoria", - 9727: "La Carretera", - 9728: "Calle Reventón Blanco", - 9729: "Arenas de Platino", - 9730: "Laderas del Deshielo", - 9731: "Despensa del Cerdo", - 9734: "Tierras Altas de Arathi", - 9735: "Ar'gorok", - 9736: "Cumbre Destral", - 9737: "Finca Nueva", - 9738: "Nido Alto", - 9739: "Cruce Redilnorte", - 9740: "Mina Mostacho Seco", - 9741: "Círculo de los Elementos", - 9742: "Aserradero de las Tierra Altas", - 9743: "Castillo de Stromgarde", - 9744: "Senda del Valor", - 9745: "Mina de Galson", - 9746: "Círculo de Vínculo Interior", - 9747: "Refugio de la Zaga", - 9748: "Círculo de Vínculo Exterior", - 9749: "Granja de Dabyrie", - 9750: "Sentencia", - 9751: "Cañón Mostacho Seco", - 9752: "Granja Go'shek", - 9753: "Poblado Secacorteza", - 9754: "Sala Puño de Roca", - 9755: "Puente Thandol", - 9756: "Muralla de Thoradin", - 9757: "Puerta de Atrás", - 9758: "Muralla de Thoradin", - 9759: "Muralla de Thoradin", - 9760: "Cala de Faldir", - 9761: "El Arrecife Hundido", - 9762: "Círculo de Vínculo Este", - 9763: "La Hoja Oxidada", - 9764: "Thros, las Tierras Contagiadas", - 9765: "Academia Valiente", - 9766: "Jarra Dorada", - 9767: "Altar de la Tormenta", - 9768: "Nido de Buitres", - 9769: "Archivos del Monasterio", - 9770: "Cueva de Ai'twen", - 9771: "Cuartel de los Valiente", - 9772: "Dominio de Rastakhan", - 9773: "Guarida de Rezan", - 9774: "Guardia Occidente", - 9775: "Caverna Peñarroja", - 9776: "Puesto de Avanzada", - 9777: "Pozas del Colmillo", - 9778: "Isla Inexplorada", - 9779: "Puesto Vientoveloz", - 9780: "Canto de Sirena", - 9781: "Kunza-Kunza", - 9782: "Ruina Abandonada", - 9783: "Santuario de Huesos", - 9784: "Playa Remanso", - 9785: "Orilla del Carroñero", - 9786: "Gorgrond", - 9788: "Nido Rasgacielos", - 9789: "Campos de Presa", - 9793: "Cresta Caparazón", - 9794: "Peña del Pastor", - 9795: "Costa Manglar", - 9796: "Guarida del Lobo", - 9797: "Bonanza Pantoque", - 9798: "Colina Baliza", - 9799: "Montaña Rugiente", - 9800: "Mare Magnum", - 9801: "Posada Muelle Acogedor", - 9802: "Despacho del Capitán de Puerto", - 9803: "Islas Aullantes", - 9804: "Mirador de la Tormenta", - 9805: "Puente Takulu", - 9806: "Avanzada de Fincagrana", - 9807: "Altar de Putrefacción", - 9808: "Reinos del Este", - 9809: "Vol'dun", - 9810: "Risco Susurrante", - 9811: "Refugio Arenáceo", - 9812: "Llanuras de Tojek", - 9813: "Puesto Puñopiedra", - 9814: "Vestíbulo de los Infieles", - 9815: "Túneles del Eco", - 9816: "Garras de la Gran Madre", - 9817: "Gruta Galvanizada", - 9818: "Bazar Ojo Avizor", - 9819: "Atrio de Sethraliss", - 9820: "Túnel Derrumbado", - 9824: "Cala Pescalodo", - 9825: "Lecho Marea Lóbrega", - 9826: "Espada del Alba", - 9827: "Costa Deriva", - 9828: "Café del Bosque Trol", - 9829: "Caverna Cielo Caído", - 9830: "Rugido de la Tempestad", - 9831: "Hija de la Sirena", - 9832: "La Rosa Audaz", - 9833: "Cuenca Colmillofunesto", - 9836: "Pastos Crestacolina", - 9838: "Sala Puño de Roca", - 9935: "Bancal de Artesanos", - 9936: "Despertar del León", - 9937: "Mare Magnum", - 9938: "Quebrantarrocas", - 9939: "Ciudadela Extensión Infernal", - 9940: "Espina Quebrada", - 9941: "Cuenca de Azufre", - 9942: "Cresta de la Aflicción", - 9943: "Avanzada de las Bestias", - 9944: "Barbecho Floración Afilada", - 9945: "Fuerte Sol Eterno", - 9946: "Manantial de Azufre", - 9947: "Observatorio Colmipétreo", - 9948: "Arrecifes de Xibala", - 9949: "Claros de Tirisfal: El Añublo", - 9950: "Roca'gor", - 9951: "Mares del Sur", - 9952: "Colina Escamaspina", - 9955: "Cala Olvidada", - 9956: "Mirador Machacahierro", - 9957: "Llamada de Gral - Dependencias de la Tripulación", - 9958: "Llamada de Gral - Resto del Barco", - 9959: "Entrañas", - 9960: "8.0 Islands - Ship - Player Alliance - Kul'Tiras Medium 01 With Gangplank", - 9961: "Gruta Velada", - 9962: "Posada Colmiagua", - 9963: "Fuentes de Purificación", - 9964: "Santuario Mareagua", - 9965: "Guarida de Ulfar", - 9966: "Mirador Mugamba", - 9967: "Bosquepícea", - 9968: "Barrovuelco", - 9969: "Pico de la Viuda", - 9970: "Sala de la Fuerza", - 9971: "Cala Creciente", - 9972: "Pradera Berilo", - 9973: "Ríos Gemelos", - 9974: "Saliente Viñabruma", - 9975: "Túneles Lomozarza", - 9976: "Caverna Piedraviva", - 9977: "Grieta Tuercerraíz", - 9978: "Accidente de Malvatio", - 9979: "Distrito Mechamojada", - 9980: "Muelles Gobernalle", - 9981: "Costa Oscura", - 9982: "Mercado Vientos Alisios", - 9983: "Distrito Mechamojada", - 9984: "Almacén de la Compañía Gobernalle", - 9985: "Plaza de la Unidad", - 9986: "Brecha del Kraken", - 9988: "Hacienda Perdida", - 9992: "Mugambala", - 9993: "La Barra de Arena", - 9994: "Cámara de Rezan", - 9995: "Anclas Echadas", - 9996: "El Tanque Borracho", - 9997: "El Pergamino Perdido", - 9998: "El Barril Hundido", - 9999: "La Aguadilla", - 10000: "Bar Zambullida", - 10001: "Escondrijo de Heyman", - 10002: "Monte Cortis", - 10003: "Santuario de las Sombras", - 10004: "Precipicio del Olvido", - 10005: "Laboratorio de Raimond", - 10006: "Casa de Jol", - 10007: "La Cabeza Reducida", - 10008: "Posada Colmiagua", - 10009: "Posada Clamabestias", - 10010: "Puesto de Avanzada Hierro Negro", - 10011: "Puesto de Avanzada Nocheterna", - 10012: "Trono Hozen", - 10013: "Foso Aullante", - 10014: "Taberna de Torga", - 10015: "Tol Dagor", - 10016: "Residencia Davenport", - 10017: "Santuario de los Sabios", - 10018: "Contaduría Vientos Alisios", - 10020: "Santuario de Sangre", - 10021: "Ruinas de Xal'vor", - 10022: "Tierras de Fuego", - 10024: "Cima del Jabaespín", - 10025: "Descanso del Lord Almirante", - 10026: "Estrecho de Tiragarde", - 10027: "El Tonel Descarriado", - 10028: "Profundidades de Roca Negra", - 10029: "Estrado del Escarabajo", - 10030: "El Muro del Escarabajo", - 10031: "Sosiego del Valor", - 10033: "Senda de las Tormentas", - 10034: "Distrito de Upton", - 10035: "Guarida de Xak'lar", - 10036: "Operación: Mecandria", - 10037: "Distrito de Upton", - 10038: "Barrio del Ron", - 10039: "Corte de la Calavera", - 10040: "Desembarco de Sweete", - 10041: "La batalla por Stromgarde", - 10042: "Cabaña de Braxton", - 10043: "Tol Dagor", - 10046: "Mar Adusto", - 10047: "Asedio de Orgrimmar", - 10048: "Reposo del Vendaval", - 10052: "Nazjatar", - 10053: "Cuenca de Arathi", - 10057: "Crisol de Tormentas", - 10058: "Costa Oscura", - 10059: "Costa Oscura", - 10061: "[TEMP] Underwater Ideation", - 10062: "Test Dungeon - White Area", - 10063: "Test Dungeon - Gray Area", - 10064: "Arboleda Marchita", - 10065: "Ruinas de Auberdine", - 10066: "Río Fonroca", - 10067: "Lor'danel", - 10068: "Bashal'Aran", - 10069: "Ruinas de Lornesta", - 10070: "Cubil Lunadón", - 10071: "Cabo de la Niebla", - 10072: "Mar de la Bruma", - 10073: "Orilla Crepuscular", - 10074: "Vega Crepuscular", - 10075: "Campamento de Refugiados de Auberdine", - 10076: "Batalla de Dazar'alor", - 10077: "Nave de la Horda", - 10078: "Nave de la Horda", - 10079: "Nave de la Horda", - 10080: "Cantera Roca Acre", - 10081: "Claro Pavesa", - 10082: "Almacén Fresno", - 10084: "Costa Oscura", - 10085: "La batalla por Costa Oscura", - 10086: "Ruinas Petragrís", - 10087: "Cruce Desolado", - 10088: "Allied - Zandalari Unlock", - 10089: "Desembarco de Lor'danel", - 10090: "Dragado", - 10091: "Nave de la Alianza", - 10092: "Nave de la Alianza", - 10093: "Nave de la Alianza", - 10094: "Mina Lornesta", - 10095: "Ensenada Margrís", - 10154: "Cementerio de Dragones", - 10155: "Corona de Hielo", - 10156: "Tundra Boreal", - 10157: "Cuenca de Sholazar", - 10158: "La Muralla Boreal", - 10159: "Los Campos de Géiseres", - 10160: "Estación de Bombeo de Palanqueta", - 10161: "Llanuras Anegadas", - 10162: "Manantiales de Vapor", - 10163: "Talramas", - 10164: "Pozas Purulentas", - 10165: "Los Cubiles de los Moribundos", - 10166: "Ciudad Templo de En'kilah", - 10167: "Aguja de Sangre", - 10168: "Aguja de Putrefacción", - 10169: "Aguja de Dolor", - 10170: "Estación de la Punta Norte", - 10171: "Campo de Refugiados de Viento Oeste", - 10172: "Poblado Bruma de Hielo", - 10173: "El Martillo de Agmar", - 10174: "Santuario de Dragones Obsidiana", - 10175: "Angrathar la Puerta de Cólera", - 10176: "Conquista del Invierno", - 10177: "Las Tierras Perdidas", - 10178: "Corazón de río", - 10179: "Canope de Hablalluvia", - 10180: "Colina Corazón Frenético", - 10181: "Manglar Silvestre", - 10182: "El Fragmento Hendido", - 10183: "El Pilar Sangrevida", - 10184: "Poblado Caminamoho", - 10185: "El Pilar del Trecho Celestial", - 10186: "Claro Callonudillo", - 10187: "Enjambre Zafiro", - 10188: "La Línea de Añublo", - 10189: "Pista del Lago", - 10190: "Rápidos de Hablalluvia", - 10191: "Río Corazón Frenético", - 10192: "Río Conquista del Invierno", - 10193: "Pasaje Viento Helado", - 10194: "El Patio de los Huesos", - 10195: "Ciudadela de la Corona de Hielo", - 10196: "Puente Central", - 10197: "El Anillo Sumergido", - 10198: "El Templo Quebrado", - 10199: "Fortaleza de Conquista del Invierno", - 10200: "Puente Oriental", - 10201: "Puente Occidental", - 10202: "Torre de la Guardia en Llamas", - 10203: "Torre Filoinvierno", - 10204: "Torre de la Vista de las Sombras", - 10205: "El Caldero de Llamas", - 10206: "Cascadas Glaciales", - 10207: "El Bosque de las Sombras", - 10208: "El Cenagal Escalofrío", - 10209: "Las Estepas de la Vida", - 10210: "Taller Chispa Occidental", - 10211: "Taller Chispa Oriental", - 10212: "Reposo del Halcón", - 10213: "N'Zoth", - 10217: "La Badea de los Cielos", - 10218: "Garganta Grito de Guerra", - 10219: "Serrería Grito de Guerra", - 10220: "Bastión Ala de Plata", - 10221: "Espuma de la Luna", - 10222: "Catacresta", - 10223: "Estudio de Aegrim", - 10224: "Zul'Toa", - 10225: "Operación: Mecandria", - 10226: "Costa Oscura", - 10227: "Costa Oscura", - 10228: "Desembarco de Lor'danel", - 10229: "Arboleda Marchita", - 10230: "Lor'danel", - 10231: "Dragado", - 10232: "Ruinas de Auberdine", - 10233: "Almacén Fresno", - 10234: "Río Fonroca", - 10235: "Río Fonroca", - 10236: "Claro Pavesa", - 10237: "Lor'danel", - 10238: "Cruce Desolado", - 10239: "Bashal'Aran", - 10240: "Ensenada Margrís", - 10241: "Bashal'Aran", - 10242: "Ruinas de Lornesta", - 10243: "Mina Lornesta", - 10244: "Cubil Lunadón", - 10245: "Ruinas de Lornesta", - 10246: "Cabo de la Niebla", - 10247: "Cabo de la Niebla", - 10248: "Mar de la Bruma", - 10249: "Orilla Crepuscular", - 10250: "Vega Crepuscular", - 10251: "Campamento de Refugiados de Auberdine", - 10252: "Cantera Roca Acre", - 10253: "Claro Pavesa", - 10254: "Almacén Fresno", - 10255: "Cruce Desolado", - 10256: "Desembarco de Lor'danel", - 10257: "Dragado", - 10258: "Mina Lornesta", - 10259: "Ensenada Margrís", - 10260: "Cámara Tenebrosa", - 10261: "Territorio inexplorado", - 10262: "La Zancada", - 10263: "Gnomeregan", - 10265: "Shrine of the Storm (copy)", - 10266: "Crisol de Tormentas", - 10267: "Ocaso de Expedición", - 10268: "Ocaso de Delaryn", - 10270: "Cámara del Tesoro Zandalari", - 10271: "Dev Smoketest Area", - 10272: "Tol Dagor", - 10273: "Núcleo de las Sombras", - 10275: "La Corte del Sol", - 10277: "Arboleda Marchita", - 10278: "Ruinas de Auberdine", - 10279: "Orilla Crepuscular", - 10280: "Bashal'Aran", - 10281: "Cruce Desolado", - 10282: "Almacén Fresno", - 10283: "Cabo de la Niebla", - 10284: "Ocaso de Delaryn", - 10285: "Santuario de las Sombras", - 10286: "Zarcillo de Corrupción", - 10287: "Paso Crestagrana", - 10288: "Costa Oscura", - 10290: "Mecandria", - 10291: "Costa Sumergida", - 10292: "zzOLDBastion", - 10293: "Ruinas de Mathystra", - 10294: "Ruinas de Auberdine", - 10295: "Torre de Althalaxx", - 10296: "Salto de Fonroca", - 10297: "Bashal'Aran", - 10298: "Ameth'Aran", - 10299: "Páramo de los Ancianos", - 10300: "La Espada del Maestro", - 10301: "Excavación de Tripirrem", - 10302: "Cabo de la Niebla", - 10303: "Playa del Oleaje", - 10304: "Río Culebra", - 10305: "Kor'gar", - 10306: "Río Fonroca", - 10307: "Vega Crepuscular", - 10308: "Mar de la Bruma", - 10309: "Campamento de Refugiados de Auberdine", - 10310: "Lor'danel", - 10311: "Ruinas de Lornesta", - 10312: "Cuenca Fonroca", - 10313: "Campamento de Guerra Rompelanzas", - 10314: "Arboleda Marchita", - 10315: "El Ojo del Vórtice", - 10316: "Campamento del Bosque Negro", - 10317: "Fauces del Vacío", - 10318: "Valle Rompelanzas", - 10319: "La Playa Llameante", - 10320: "Cubil Lunadón", - 10321: "Paso Rompelanzas", - 10322: "Caverna Rompetierra", - 10323: "Restos del Amanecer de Plata", - 10324: "Ensenada Margrís", - 10325: "Orilla Crepuscular", - 10326: "Nazj'vel", - 10327: "Bajamar", - 10328: "Bajamar", - 10330: "Lanzas de Azshara", - 10331: "Cataratas Inferiores", - 10332: "El Arrecife Colgante", - 10333: "Nuevohogar", - 10334: "Mezzamere", - 10335: "Ensenada Cinérea", - 10337: "Aguja de Alathir", - 10338: "Recinto de los Esclavos Fondo Marino", - 10339: "Paso del Depredador", - 10340: "Bosque de Coral", - 10341: "Kal'methir", - 10342: "Ciudadela de Kal'methir", - 10343: "Sagrario de Iwen", - 10344: "Nar'anan", - 10345: "Mercado Sumergido", - 10346: "Zin-Azshari", - 10348: "Prototipo", - 10353: "Jardines Imperiales", - 10354: "Bancal Zanj'ir", - 10355: "Ciudadela de la Corona de Hielo", - 10356: "Bancal de azsh'ari", - 10357: "Frente de Vallefresno", - 10358: "Gnomeregan", - 10359: "El Trono Helado", - 10361: "Túneles Hondaspiral", - 10364: "Puerto de Boralus", - 10366: "Cámaras de Piedra", - 10367: "Núcleo de las Sombras", - 10368: "Mare Magnum", - 10369: "Granja Navalmar", - 10370: "El Zócalo", - 10371: "Dazar'alor", - 10372: "Cantera Roca Acre", - 10373: "Bancal de los Portavoces", - 10374: "Gran Bazar", - 10375: "Puerto de Zandalar", - 10376: "Bahía de los Reyes", - 10377: "Arboleda Maldita", - 10378: "Núcleo de las Sombras", - 10379: "Ciénaga Colmillo", - 10380: "Allied - Kul Tiran Unlock", - 10381: "Sala de los Rituales", - 10382: "Costa de Auberdine", - 10383: "Cámaras de Opulencia", - 10384: "Senda de los Ancestros", - 10385: "Sagrario de los Loa", - 10386: "Paseo de los Reyes", - 10387: "Corazón del Imperio", - 10388: "Paso Devastador", - 10389: "Aguja de Thoras'tal", - 10390: "Claro Pavesa", - 10391: "Almacén Fresno", - 10392: "Desembarco de Lor'danel", - 10393: "Cruce Desolado", - 10394: "Dragado", - 10396: "Aguja de Lemor'athra", - 10397: "Mulgore", - 10398: "Mesa de la Nube Roja", - 10399: "Campamento Narache", - 10400: "Feralas", - 10401: "Barranco Cortazarza", - 10402: "Las Llanuras Onduladas", - 10403: "Avanzada Gordunni", - 10404: "Los Baldíos del Sur", - 10405: "La Guarida de Mantospina", - 10406: "Colina de Vistalonga", - 10407: "El Gruñospina", - 10408: "El Cercado de Jabaguerreros", - 10409: "Lago Dumont", - 10410: "Dominios Altonato", - 10411: "Templo de Elun'alor", - 10412: "Cuenca Dientes de Dragón", - 10413: "Revendreth", - 10416: "Minas Abandonadas", - 10417: "Mulgore", - 10418: "Pernoóxido", - 10419: "Campamento de Expedición de Sobrechispa", - 10420: "Bahía de Prospección", - 10421: "Uldir", - 10422: "Refugio", - 10423: "Cámara Olvidada", - 10424: "Confín del Exilio", - 10425: "Palacio Eterno", - 10426: "Venganza Presta", - 10427: "Venganza Presta", - 10428: "El Vertido", - 10429: "Prisión de Tinta", - 10430: "Val'sharah", - 10431: "Arboleda de los Sueños", - 10432: "Bahía Esmeralda", - 10433: "Arboleda Corazón Oscuro", - 10435: "Chatarrerías", - 10436: "Guarida Oseomorralla", - 10437: "Vertedero de Bondo", - 10438: "Bosque Menguante", - 10439: "Depósito Trastovatio", - 10440: "Cuenca de Arathi", - 10441: "Bastión de Aterratrols", - 10442: "Guarida de los Rapiñadores", - 10443: "Granja", - 10444: "Herrería", - 10445: "Aserradero", - 10446: "Mina de oro", - 10447: "Establo", - 10448: "Profundidades Lúgubres", - 10450: "Laboratorio de Mardivas", - 10451: "Stratholme", - 10452: "Campamento Abandonado", - 10453: "Escondrijo Múrloc", - 10454: "Melodía de Tiffin", - 10455: "Melodía de Tiffin", - 10456: "El Sagrario Obsidiana", - 10458: "Valle de Alterac", - 10465: "Guarida de los Gruñones", - 10467: "Rocío del Oeste", - 10468: "Arrecifes Dentados", - 10469: "Alto del Bosque Menguante", - 10470: "Punta Rota", - 10472: "Torghast, Torre de los Condenados", - 10473: "Taberna de Hearthstone", - 10474: "Camino del Sueño Esmeralda", - 10475: "Sueño Esmeralda", - 10476: "Ventormenta", - 10477: "Espinazo del Dragón", - 10478: "Tierras Altas Crepusculares", - 10479: "Reducto Bermellón", - 10480: "Paso Faucedraco", - 10481: "Recodo de Vermis", - 10482: "Grim Batol", - 10483: "Territorio inexplorado", - 10488: "Visión de Ventormenta", - 10489: "Ciudad de Gilneas", - 10492: "Visión de Orgrimmar", - 10495: "Viejo Camino Mercantil", - 10496: "Caverna Desmoronada", - 10497: "Robotódromo", - 10500: "Cala Piedrasalmuera", - 10501: "Cámaras Escarlata", - 10502: "Campo de Entrenamiento", - 10503: "Brezal Jabaespín", - 10504: "Cámara del Corazón", - 10505: "Aldea Sangraleta", - 10506: "Gruta Cantaespina", - 10507: "Retirada de Miju", - 10508: "Rescate de Baine", - 10510: "Agujero de Moch'k", - 10511: "Cueva Tintada", - 10512: "Marisma Zanj'ir", - 10513: "Desafío de Maedin", - 10514: "Herrera", - 10515: "Herrera", - 10516: "Aserradero", - 10517: "Granja", - 10518: "Establos", - 10519: "Mina de oro", - 10520: "Bastión de Aterratrols", - 10521: "Guarida de los Rapiñadores", - 10522: "Ny'alotha, Ciudad del Despertar", - 10523: "Sagrario del Mago", - 10524: "La Vorágine", - 10525: "Repositorio Shirakess", - 10526: "El Ojo de la Eternidad", - 10527: "Túmulo de Hrun", - 10528: "Nido de Arpías", - 10529: "Ruinas Ogras", - 10530: "Ciudadela Magulladura Oscura", - 10531: "Escondrijo Lomorrugoso", - 10533: "Reposo Famélico", - 10534: "Bastión", - 10535: "Mirador Gorgoniano", - 10536: "Cueva Rocaherrumbre", - 10538: "Caverna Cristalizada", - 10539: "La Cueva Quemada", - 10543: "Descanso del Ingeniero", - 10544: "Refugio Fondo Marino", - 10545: "Cavernas Cantaespina", - 10546: "Puerta de la Reina", - 10547: "Confluencia de las Mareas", - 10548: "Guarida de la Ensenada", - 10549: "Reducto Quillaleta", - 10550: "Alto de Utama", - 10562: "Poza Abisal", - 10563: "Cámara de los Cosechadores", - 10564: "Anguila Grasienta", - 10565: "Oribos", - 10566: "Cuenca Oseomorralla", - 10567: "Circuito de Choquepiñón", - 10568: "Puente Magulladura Oscura", - 10574: "Punto de Tejechispas", - 10580: "Covacha del Eco", - 10581: "Ciudadela Magulladura Oscura", - 10582: "Cavernas del Tiempo", - 10584: "Cámaras de los Orígenes", - 10588: "Llanuras Magulladura Oscura", - 10589: "Rincón del Carroñero", - 10603: "Escondrijo de Murloco", - 10619: "Criadero Mielabdomen", - 10625: "Durotar", - 10626: "Río Furia del Sur", - 10627: "Río Furia del Sur", - 10628: "Tierras Crines de Acero", - 10629: "Cuenca Furia del Sur", - 10638: "Catacumbas de Karazhan", - 10639: "El Mar del Norte", - 10640: "Cenagal del Fin", - 10641: "Cámara de Vacío", - 10642: "Caverna Aguaoscura", - 10713: "Monte Alto", - 10714: "Tormenheim", - 10715: "Cimas Anegadas", - 10716: "Cámara de Aggramar", - 10717: "Mirador de Crowley", - 10718: "Campamento de Selección Abrasacielos", - 10719: "Cala de Nashal", - 10720: "Posición de los Renegados", - 10721: "Mirador Pico Negro", - 10722: "Hrydshal", - 10723: "Grajero Alatormenta", - 10724: "Sendero Rompeciclón", - 10725: "Cima de Thorim", - 10726: "Puertas del Valor", - 10727: "Suramar", - 10728: "Campo de los Reyes Caídos", - 10729: "Bosque de las Runas", - 10730: "Haustvald", - 10731: "Mare Magnum", - 10732: "Enclave Cuerno Férreo", - 10733: "Skold-Ashil", - 10734: "Desembarco Rastro del Pavor", - 10735: "Guardia Gris", - 10736: "Posición del Forestal", - 10737: "Puesto de Cullen", - 10738: "Avanzada de Lorna", - 10739: "Refugio de la Garra", - 10740: "Campamento de Guerra Vilskorn", - 10741: "Puesto de la Cima", - 10742: "Acantilados Boca Infernal", - 10743: "Cuerno del Cielo", - 10744: "Ofensiva de Cringrís", - 10745: "Bosque Hojabronce", - 10746: "Valdisdall", - 10747: "Mesa Velonieve", - 10748: "Valle de la Espada", - 10749: "Sepulcro del Cielo", - 10750: "Tel'anor", - 10751: "Arrecifes Boca Infernal", - 10752: "Puerto Mareskorn", - 10753: "Reposo del Escudo", - 10754: "Alto de los Vinculadores de Tinta", - 10755: "Morheim", - 10756: "Orilla Escama Estanca", - 10757: "Ruinas Sashj'tar", - 10758: "Bahía Vendaval Azur", - 10759: "Claro Solescarcha", - 10760: "Puesto Pino de Hielo", - 10761: "Colinas Aullantes", - 10762: "Jandvik", - 10763: "Estrecho de Hydrannon", - 10764: "Avanzada Pezuña Pétrea", - 10765: "Gran Paseo", - 10766: "Acantilados Viento Impetuoso", - 10767: "Lagar de Aegira", - 10768: "Cala Vendaval Azur", - 10769: "Cima Monte Alto", - 10770: "Cúspide de Cordana", - 10771: "Espesura Carmesí", - 10772: "Ciudad de Suramar", - 10773: "Viñedos Crepusculares", - 10774: "Acantilados Velonieve", - 10775: "Laderas Sañatormenta", - 10776: "Cima de la Tormenta", - 10777: "Paso del Rey Caído", - 10778: "Meseta de Ámbar", - 10779: "Bahía de Cuarzo", - 10780: "Nastrondir", - 10781: "Atalaya de Nashal", - 10782: "Pico Monte Alto", - 10783: "Altar Pluma Negra", - 10784: "Bosque de Jotun", - 10785: "Ciudadela Susurravientos", - 10786: "Roca del Velador", - 10787: "Garganta Marcapiedra", - 10788: "Dreyrgrot", - 10789: "Terrado de los Vigías", - 10790: "Akstad", - 10791: "Campos de Entrenamiento Hacharruna", - 10792: "Cantera Guardián de Piedra", - 10793: "Baluarte del Halcón Negro", - 10794: "Alto Miraespíritus", - 10795: "Villa de la Primera Arcanista", - 10796: "Animalario", - 10797: "Vrekt", - 10798: "Saliente Roca Lejana", - 10799: "Tierras Altas Rocosas", - 10800: "Bahía de Ashil", - 10801: "Parque Ocaso", - 10802: "Desembarco Luz de Luna", - 10803: "Mercado Engalanado", - 10804: "Corte de la Medianoche", - 10805: "Hacienda Enjoyada", - 10806: "Hafr Fjall", - 10807: "Calzada Rayo de Luna", - 10808: "Cala Nidocuervo", - 10809: "Costa de la Melancolía", - 10810: "Loch Varmad", - 10811: "Roca Gaviota", - 10812: "Avanzada Vilificada", - 10833: "Uldum", - 10835: "La Puerta de los Ciclos Eternos", - 10836: "Obelisco de las Estrellas", - 10837: "Tumba de Khartut", - 10838: "Orsis", - 10839: "Ramkahen", - 10840: "Nahom", - 10841: "Mar'at", - 10842: "Campos de Akhenet", - 10843: "Avanzada de la Legión Ramkahen", - 10844: "El Foso de Escamas", - 10845: "Bancal Piedrasol", - 10846: "Puerta de Hamatep", - 10847: "Río Vir'naal", - 10848: "Cuna de los Ancestros", - 10849: "Obelisco del Sol", - 10850: "Ciudad Perdida de los Tol'vir", - 10851: "Puerto de Ankhaten", - 10852: "Ruinas de Ammon", - 10853: "El Embarcadero Maldito", - 10854: "Los Pasos del Sino", - 10855: "La Senda de la Devastación", - 10856: "Tierras de Tahret", - 10857: "Sello del Rey del Sol", - 10858: "Ruinas de Sahket", - 10859: "Ruinas de Ahmtul", - 10860: "Ciudad de Neferset", - 10861: "Monte del Vigía del Sol", - 10862: "Presa Vir'naal", - 10863: "Obelisco de la Luna", - 10864: "Templo de Uldum", - 10865: "Oasis de Vir'sar", - 10866: "Ruinas de Khintaset", - 10867: "Puesto de Sullah", - 10868: "Ascenso del Hacedor", - 10869: "Afueras de la Ciudad de Neferset", - 10870: "Precipicio del Peregrino", - 10871: "Monte Akher", - 10872: "Tumbas de los Precursores", - 10873: "Puesto Comercial de Arsad", - 10874: "Paso de Keset", - 10876: "Avanzada de Vigilantes", - 10877: "Tumba del Rey del Sol", - 10878: "Oasis Vir'naal", - 10879: "Lago Vir'naal", - 10880: "Delta Vir'naal", - 10881: "Cima del Viento del Sur", - 10882: "Cámaras de los Orígenes", - 10883: "Cámara del Sol", - 10884: "Cámara de la Luna", - 10885: "Cámara de las Estrellas", - 10886: "Cañón del Céfiro", - 10887: "Solesa naksu [DNT]", - 10920: "La Escalera Velada", - 10957: "Bancal Gurthan", - 10958: "La Marcha Espectral", - 10959: "Puerta de la Primavera Eterna", - 10960: "El Nidal Secreto", - 10961: "Taberna en la Niebla", - 10962: "Camino de los Cien Pasos", - 10963: "La Senda Florida", - 10964: "Locura del Albañil", - 10968: "Pantano Olvidado", - 10969: "El Paso del Trueno", - 10978: "Bosque de la Desgracia", - 10979: "Refugioscuro", - 10980: "Castillo de Nathria", - 10981: "Catacumbas de Pesar", - 10982: "Aldea de la Caída", - 10983: "Colina Criptaolvidada", - 10984: "Corte de los Cosechadores", - 10985: "Distrito de las Ascuas", - 10986: "Salto del Pecado", - 10987: "Asilo Velado", - 10988: "Santuario de los Locos", - 10989: "Fortaleza de la Dominancia", - 10990: "Animalario del Maestro", - 10991: "Gruta Sangrienta", - 10992: "Goleta de Escorpijón", - 10993: "Refugio del Morador de las Arenas", - 10994: "Murallas Carbonizadas", - 10995: "Salas de la Expiación", - 10996: "Filo del Pecado", - 10997: "Archivam", - 10998: "Cripta de los Olvidados", - 10999: "Gran Empalizada", - 11000: "Distrito de Redelav", - 11001: "Distrito del Cáliz", - 11002: "Puerta Vieja", - 11003: "Torre Muroscuro", - 11004: "Puerta de la Dominancia", - 11005: "Guarida de Shassera", - 11006: "Cuenca Aterradora", - 11007: "Refugio del Acechador", - 11008: "Restos del Bestia Fluvial Hambrienta", - 11009: "Restos de la Vieja Blanchy", - 11011: "Guarida de Garrasesina", - 11012: "Fuerte Elisio", - 11013: "Vestíbulo de la Eternidad", - 11014: "Templo de la Pureza", - 11015: "Matorral de los Alimentadores", - 11016: "Cruce Diezmado", - 11017: "Ruinas Marchitas", - 11018: "Muro Silvano", - 11019: "Bosque Dragado", - 11020: "Mansión del Custodio", - 11021: "Mirador Piedravigilia", - 11023: "Ventorrillo de Mrgl", - 11024: "El Bosque de Jade", - 11025: "Valle de los Cuatro Vientos", - 11026: "Cima Kun-Lai", - 11027: "Estepas de Tong Long", - 11028: "Cataratas de la Serenidad", - 11029: "Camino de la Iluminación", - 11030: "Punta del Grito", - 11031: "Campamento Nooka Nooka", - 11032: "Aguja de Soledad", - 11033: "Lago Kittitata", - 11034: "Círculo de Escamas", - 11035: "Granjas Pie Atronador", - 11036: "Rancho Pie Atronador", - 11037: "Villoría Pie Atronador", - 11038: "Bosque del Tigre", - 11039: "Vega Escarpada", - 11040: "Granja de Shang", - 11041: "Cerro Makaku", - 11042: "Campo del Miko", - 11043: "Mirador Pintoresco", - 11044: "Ribera Turbia", - 11045: "Hogar de Caolín", - 11046: "Prado Aguasol", - 11047: "Arroyo Manduka", - 11048: "Espesura Paoquan", - 11049: "Marjal Exótico", - 11050: "Pozas de la Pureza", - 11051: "Nueva Cifera", - 11052: "El Abanico Dorado", - 11053: "Los Cultivos Florecientes", - 11054: "La Escalera Velada", - 11055: "Cataratas Huangtze", - 11056: "Aldea Bruma Otoñal", - 11057: "Ruinas de Guo-Lai", - 11058: "La Escalera Dorada", - 11059: "Lago Pétalo Níveo", - 11060: "Baluarte del Sol Poniente", - 11061: "La Pagoda Dorada", - 11062: "Cementerio Tu Shen", - 11063: "Alto de las Ruinas", - 11064: "Claro Brote Invernal", - 11065: "Las Cinco Hermanas", - 11079: "Santuario de las Siete Estrellas", - 11080: "Palacio Mogu'shan", - 11081: "Camino Jin Yang", - 11082: "Los Campos Estivales", - 11083: "Alto Ancestral", - 11099: "Puerta del Sol Poniente", - 11106: "La Vía del Emperador", - 11108: "Risco Sombra Otoñal", - 11109: "Colina Flor de Sol", - 11110: "Santuario de las Dos Lunas", - 11111: "Excavación de la Gran Flor", - 11356: "Continente", - 11375: "Palacio Eterno", - 11377: "Crisol del Aspirante", - 11378: "Archivos Eonios", - 11379: "Ciudadela de la Lealtad", - 11380: "Templo del Coraje", - 11381: "Reposo del Héroe", - 11382: "8.2.5 War Campaign", - 11383: "Vereda Real", - 11384: "Palacio Mogu'shan", - 11385: "Guarida de Gorm", - 11386: "Guarida del Abrecaminos", - 11387: "Vertedero de Bondo", - 11388: "Guarida Oseomorralla", - 11389: "El Vertido", - 11390: "Depósito Trastovatio", - 11391: "Pernoóxido", - 11392: "Circuito de Choquepiñón", - 11393: "Caverna Desmoronada", - 11394: "Escondrijo de Ekka", - 11395: "Cámaras Mogu'shan", - 11396: "Arboleda de Coral", - 11397: "Templo del Dragón de Jade", - 11398: "TravisTestTerrain", - 11399: "Desafío de Maedin", - 11400: "Las Fauces", - 11401: "El Bloqueo Dranosh'ar", - 11402: "Orgrimmar", - 11403: "Durotar", - 11404: "La Rosa Áurea", - 11405: "Salas de Guo-Lai", - 11406: "La Sala del Dragón", - 11407: "La Estancia de las Estatuas", - 11408: "La Estancia Enlosada", - 11409: "Cámara Ritual de Guo-Lai", - 11410: "Cámara de Guo-Lai", - 11411: "Templo de la Humildad", - 11412: "Forja Eterna", - 11413: "Trono de Himnos Eternos", - 11414: "Mirador del Exilio", - 11415: "Manantial de la Guardiana", - 11416: "Vórtice Enturbiado", - 11417: "MattTestTerrain", - 11418: "Cuenca Aleta Rota", - 11419: "Cinematic Test Map", - 11424: "Establos", - 11425: "Granja", - 11426: "Aserradero", - 11427: "Mina de oro", - 11431: "El Santuario Silente", - 11433: "La Desecación", - 11434: "Travesía Cenicienta", - 11435: "Muro Cinéreo", - 11436: "[Espacio de aventura 1]", - 11437: "[Espacio de aventura 2]", - 11438: "Cripta Agostada", - 11439: "Puente del Arrepentimiento", - 11440: "Fuerte Grommash", - 11441: "Fuerte Grommash", - 11442: "La Bodega", - 11443: "La Terraza Áurea", - 11444: "Reposo Estival", - 11445: "Camino de la Serenidad", - 11446: "Cámara del Reflejo", - 11447: "El Intercambio Imperial", - 11448: "El Bazar de las Estrellas", - 11449: "Galería Etérea", - 11450: "La Cámara Celestial", - 11451: "Cámara de la Iluminación", - 11452: "El Farol Áureo", - 11453: "El Paso del Emperador", - 11454: "El Bancal del Verano", - 11455: "Villaescoria", - 11456: "Punta del Terror", - 11457: "Puente de la Penitencia", - 11458: "Almacén Decrépito", - 11459: "MattTestTerrain2", - 11462: "Maldraxxus", - 11463: "MalDoodadtest", - 11464: "MalPestDoodadTest", - 11465: "Teatro del Dolor", - 11466: "Reducto Lúgubre", - 11467: "Punta de Lanza", - 11468: "Husos de Hueso", - 11469: "Sagrario del Incólume", - 11471: "Puerta Cinérea", - 11473: "Reposo del Aspirante", - 11474: "Cámara del Primer Reflejo", - 11475: "Descenso de Alanegra", - 11476: "EnvArtDevMaldraxxus- Pestilencia", - 11477: "EnvArtMaldraxxus- Campo de batalla", - 11478: "Relicario Primordial", - 11479: "Villaescoria", - 11486: "Enclave Nemónico", - 11487: "Jardín del Sosiego", - 11488: "Botín de la Primogénita", - 11489: "Reposo de Agthia", - 11490: "Forja del Céfiro", - 11491: "Estación de Transferencia Ta", - 11492: "Corte de la Euforia", - 11493: "Aria de Sophia", - 11494: "Obertura de Sophia", - 11495: "Ágora Albaeterna", - 11498: "Gracia de los Recordados", - 11499: "Cuenca de Ithys", - 11500: "Sonata de Sophia", - 11501: "Jardín de Edenia", - 11503: "Reposo de Calíope", - 11505: "Refugio Sabio", - 11507: "Reposo del Administrador", - 11508: "SinewTestArea", - 11509: "Reflejo de la Pureza", - 11510: "Ardenweald", - 11511: "Arboleda del Duramen", - 11512: "Lodazal Amortajado", - 11513: "Cañada de los Espíritus", - 11514: "Cresta Desmoronada", - 11515: "Tirna Vaal", - 11516: "Madrigueras Polvorientas", - 11517: "Estanques Apacibles", - 11518: "Charco Mugroso: Bosque de la Desgracia", - 11519: "Pantano Cantosueño", - 11520: "Los Tallos", - 11521: "Cumbre de la Pureza", - 11523: "Caverna Perlatriste", - 11524: "Llanos del Tormento", - 11525: "Madrigueras de las Bestias", - 11526: "Caldera de Zovaal", - 11527: "Desmotaeron", - 11528: "Gorgoa: Río de las Almas", - 11529: "Bastión de la Perdición", - 11530: "Crisol de los Malditos", - 11531: "Cuenca Invernal", - 11532: "Colmena Acechante", - 11533: "Tirna Noch", - 11534: "Claro Esmeralda", - 11535: "Senda del Renacer", - 11536: "Cueva Hacha Invernal", - 11537: "Heridas Olvidadas", - 11538: "Casa de los Rituales", - 11539: "Aliados - desbloqueo de mecagnomos", - 11540: "Aliadas - Desbloqueo de vulperas", - 12071: "Tirna Rowen", - 12073: "Dun Baldar", - 12212: "Azshara", - 12213: "Mina Ferrohondo", - 12214: "Roca de Durotan", - 12215: "Dun Baldar", - 12216: "Paso de Ala Gélida", - 12217: "Aldea Lobo Gélido", - 12218: "Torre de la Punta", - 12219: "Fuerte Hacha Invernal", - 12220: "Baluarte Sangrehielo", - 12221: "Bastión Lobo Gélido", - 12222: "Paso de la Daga Escarcha", - 12223: "Field of Strife", - 12224: "Cementerio Lobo Gélido", - 12225: "Cementerio Sangrehielo", - 12226: "Cementerio Avalancha", - 12227: "Cementerio Piedrahogar", - 12228: "Cementerio Pico Tormenta", - 12229: "Búnker Ala Gélida", - 12230: "Risco Zarpa Salvaje", - 12231: "Caverna Zarpa Salvaje", - 12232: "Mina Dentefrío", - 12233: "Paso de Dun Baldar", - 12234: "Búnker Piedrahogar", - 12235: "Avanzada Piedrahogar", - 12236: "Nordrassil", - 12239: "Cueva Ala Gélida", - 12240: "La Grieta Velada", - 12241: "Zanja de Hierro", - 12242: "Orillas de la Vida", - 12243: "El Crecimiento", - 12244: "Cumbres de Brillocascada", - 12245: "Cuenca Brillocascada", - 12246: "Arboleda de la Memoria", - 12247: "Acantilados Delicados", - 12248: "Descanso de Keres", - 12462: "Aldea Bruma Otoñal", - 12463: "Ruinas de Guo-Lai", - 12464: "La Escalera Dorada", - 12465: "Lago Pétalo Níveo", - 12466: "Colina Flor de Sol", - 12467: "Baluarte del Sol Poniente", - 12468: "Cementerio Tu Shen", - 12469: "Alto de las Ruinas", - 12470: "Claro Brote Invernal", - 12471: "Las Cinco Hermanas", - 12472: "Santuario de las Siete Estrellas", - 12473: "Palacio Mogu'shan", - 12474: "Camino Jin Yang", - 12475: "Los Campos Estivales", - 12476: "Alto Ancestral", - 12477: "Puerta del Sol Poniente", - 12478: "La Vía del Emperador", - 12479: "Risco Sombra Otoñal", - 12480: "La Pagoda Dorada", - 12481: "Santuario de las Dos Lunas", - 12622: "Valle del Crepúsculo Eterno", - 12623: "Valle del Crepúsculo Eterno", - 12624: "Gruta de Vaal", - 12765: "Visión de las Arenas Tortuosas", - 12766: "Visión de las Arenas Tortuosas", - 12767: "Obelisco de la Luna", - 12768: "Ascenso del Hacedor", - 12769: "Ruinas de Khintaset", - 12770: "Oasis Vir'naal", - 12771: "Ruinas de Sahket", - 12772: "Templo de Uldum", - 12773: "Ramkahen", - 12774: "Lago Vir'naal", - 12775: "Casa de las Plagas", - 12776: "Lágrimas de Akarek", - 12777: "Fundición de Colágeno", - 12778: "El Farol Áureo", - 12782: "Cripta de la Absolución", - 12783: "Casa de los Elegidos", - 12793: "Avanzada Forjafuego", - 12794: "Refugio de Nemea", - 12795: "Casa de los Ensamblajes", - 12796: "Patio de Remiendos", - 12797: "Villa Venaoscura", - 12798: "Hacienda Vinoamargo", - 12799: "Dominios Lodocaso", - 12800: "Residencia Prímula", - 12801: "Cámara de Redelav", - 12802: "Establos Aterrabrezo", - 12803: "Pastos Dorados", - 12804: "Hacienda Inquina", - 12805: "Descomposición de Glutharn", - 12806: "Salón Sangrepérfida", - 12807: "Mansión Colinaspino", - 12808: "Torre de Redelav", - 12809: "Bancal Eterno", - 12810: "Paseo del Contendiente", - 12811: "El Santuario Silente", - 12812: "Puesto Alto", - 12813: "Cámara de Separación", - 12814: "Arboleda de Endrinos", - 12815: "Primera Cámara de Calíope", - 12816: "Segunda Cámara de Calíope", - 12817: "Tercera Cámara de Calíope", - 12818: "Alto Otoñal", - 12819: "Zerekriss", - 12820: "Vortrexxis", - 12821: "Exoramas", - 12822: "Necropolis", - 12823: "Prisión Abandonada", - 12825: "Ciudad de Mecandria", - 12826: "Mercado Nocturno", - 12827: "Corte de los Cosechadores", - 12828: "Puerta de la Dominancia", - 12829: "Animalario del Maestro", - 12830: "Castillo de Nathria", - 12831: "Salas de la Expiación", - 12832: "Bosque de Hojacentella", - 12833: "Sala de las Bestias", - 12834: "Sala de la Bandera de Brisa de Plata", - 12835: "Sala de la Bandera Grito de Guerra", - 12836: "Nido Alamagna", - 12837: "Agujas de Ascensión", - 12838: "Cámara de los Pico Tormenta", - 12839: "Cámara de los Lobo Gélido", - 12840: "Anfiteatro del Lago Estrellado", - 12841: "Teatro del Dolor", - 12842: "Cavernas Sanguinas", - 12843: "Casa de los Ojos", - 12844: "Ramacentella", - 12845: "Biblioteca Prohibida", - 12846: "Puerta Espinosa", - 12847: "Bastión - Aventuras", - 12848: "Bastión - Tesoros", - 12849: "Catacumbas de Pesar", - 12851: "Costa de Jrokgar", - 12852: "Santuario", - 12853: "Mercado", - 12854: "Ruinas", - 12855: "Granja", - 12856: "Cantera", - 12857: "Profundidades de Roca Negra", - 12858: "Corazón del Bosque", - 12859: "Fin de la Muerte", - 12860: "Linde del Bosque", - 12861: "Sepulcro del Saber", - 12871: "Cámara de Digestión", - 12872: "Cámara de Digestión", - 12873: "Trono de Ramkahen", - 12874: "Retiro del Ancestro", - 12875: "Cuenca del Santuario Onírico", - 12876: "Trono del Prelado", - 12877: "Ny'alotha", - 12878: "Visión del Destino", - 12879: "Mirador Obsidiana", - 12880: "Anexo de la Profecía", - 12882: "Val'sharah", - 12883: "Templo de Elune", - 12884: "Jardín de la Noche", - 12885: "La Cámara Ritual", - 12886: "La Colmena", - 12888: "Fauces de Gor'ma", - 12889: "Foso Engendrador", - 12890: "Cámara de Renacimiento", - 12891: "Túneles de Descomposición", - 12892: "Desembarco Crepuscular", - 12893: "Bancal de Desolación", - 12894: "Cubil de Transfusión", - 12895: "N'Zoth", - 12896: "Enclave de Verdades Infinitas", - 12897: "Cubil del Devorador", - 12898: "Distrito del Carnicero", - 12899: "Cicatriz Contagiada", - 12900: "Colmena Gorm", - 12901: "Cámara Etérea", - 12902: "Arboleda del Despertar", - 12903: "Avanzada Sabueso Negro", - 12905: "Cámara del Corazón", - 12906: "Pozo del Retador", - 12909: "Acantilados del Sosiego", - 12910: "Arboleda del Manto Nocturno", - 12911: "Desierto de los Cuchillos", - 12916: "Estela Necrótica", - 12917: "Salto del Pecado", - 12918: "Tirna Glayn", - 12919: "Cuenca del Santuario Onírico", - 12920: "Claro Esmeralda", - 12921: "Senda del Renacer", - 12922: "Ramacentella", - 12923: "Corazón del Bosque", - 12924: "Ardenweald", - 12951: "El Trono Helado", - 12952: "Ciudadela de la Corona de Hielo", - 12953: "Villa del Lago", - 12954: "Arboleda del Terror", - 12955: "El Vestíbulo Dorado", - 12956: "La Sala de la Asamblea Carmesí", - 12957: "Pasadizo Olvidado", - 12958: "Cámara de los Reyes Inmemoriales", - 12959: "Trono de los Antiguos Conquistadores", - 12960: "Refugio del Señor de las Espadas", - 12961: "Santuario de Nala'shi", - 12975: "Paseo del Recuerdo", - 12989: "Descanso de Tirnenn", - 13160: "Muralla Defensiva de la Colmena Gorm", - 13161: "Filo de la Garra", - 13176: "La Cumbre del Vórtice", - 13177: "Trono de los Cuatro Vientos", - 13178: "Maldraxxus", - 13179: "Reducto Lúgubre", - 13180: "Punta de Lanza", - 13181: "Sagrario del Incólume", - 13182: "Zanja de Hierro", - 13183: "Desierto de los Cuchillos", - 13184: "Fundición de Colágeno", - 13185: "Casa de los Elegidos", - 13186: "Paseo del Contendiente", - 13187: "Trono del Prelado", - 13188: "Distrito del Carnicero", - 13189: "Casa de las Plagas", - 13190: "Teatro del Dolor", - 13191: "Cicatriz Contagiada", - 13192: "Túmulo Putrefacto", - 13193: "Extensión Ventada", - 13195: "Bosques Umbríos", - 13196: "Calcis", - 13197: "Mirador de Kaithe", - 13198: "Trono del Saber", - 13209: "Cuenca Abisal", - 13211: "Gruta de la Madre de la Colmena", - 13212: "Mina de la Gran Flor", - 13213: "Pasaje en Ruinas", - 13214: "Área de entrada de Las Fauces", - 13215: "Cocyrus", - 13216: "El Tremaculum", - 13217: "Cuna de la Ruina", - 13218: "Soto de la Médula", - 13219: "Prodigum", - 13220: "Penderghast", - 13221: "Lamento del Corvino", - 13222: "Faz del Olvido", - 13223: "Altar de Dominación", - 13224: "Castillo de Nathria", - 13226: "Refugio de la Acechamiedos", - 13227: "Espinazo del Dragón", - 13228: "Bajapeste", - 13229: "Bastión", - 13230: "Grieta Marcavil", - 13231: "Hogarraíz", - 13233: "Pozo de la Angustia", - 13235: "Cripta de la Hoja", - 13236: "Cresta de Scaplian", - 13237: "Mirador de la Luz Estelar", - 13238: "Factoría Primordial", - 13239: "Bancal Clamaíra", - 13240: "Ensamblaje Implacable", - 13305: "Avanzada de Peste", - 13306: "Matorral Ardiente", - 13307: "Cámara de la Calma Interior", - 13308: "Castillo de Nathria (gesta)", - 13309: "El Otro Lado", - 13310: "Corte de la Euforia", - 13311: "Ciudadela de la Lealtad", - 13312: "Gesta final de la curia de Maldraxxus", - 13313: "Maldraxxus", - 13314: "Husos de Hueso", - 13315: "Heridas Olvidadas", - 13316: "Casa de los Rituales", - 13317: "Descanso de Keres", - 13318: "Cresta de Scaplian", - 13319: "Descomposición de Glutharn", - 13320: "Paseo del Contendiente", - 13321: "Puesto Alto", - 13322: "Prisión Abandonada", - 13323: "Casa de los Ojos", - 13324: "Túmulo Putrefacto", - 13325: "Teatro del Dolor", - 13326: "Lágrimas de Akarek", - 13328: "Alcance Oscuro", - 13329: "La Corte de Ascuas", - 13330: "Cámara de las Cadenas", - 13331: "Estanques Fulgurantes", - 13332: "Las Fauces", - 13333: "Atalaya de la Araña", - 13334: "Nieblas de Tirna Scithe", - 13337: "Zolramus", - 13338: "Agujero en la Pared", - 13339: "Guarida de Renard", - 13340: "Nurakkir", - 13342: "Toxxulanar", - 13343: "No es Tierra de Doug", - 13344: "Descanso Desolado", - 13346: "Bancos de Cadáveres", - 13347: "Casa del Hilo", - 13348: "Amputorio", - 13350: "Crisol de los Malditos", - 13351: "Gorgoa: Río de las Almas", - 13352: "Calcis", - 13353: "Área de entrada de Las Fauces", - 13354: "Cocyrus", - 13355: "El Tremaculum", - 13356: "Cuna de la Ruina", - 13357: "Llanos del Tormento", - 13358: "Caldera de Zovaal", - 13363: "Ciénaga Virulenta", - 13364: "Claro Oculto", - 13365: "Cañada Surcada", - 13366: "Arboleda Destrozada", - 13367: "Invernadero de la Reina", - 13368: "Salón del Duque Loco", - 13369: "Fuerte Elisio", - 13370: "Alto de la Exaltación", - 13373: "Cuenca Desolada", - 13374: "Teatro del Dolor", - 13375: "Terminal Fúngica", - 13376: "Mirador Resplandeciente", - 13377: "El Mar del Norte", - 13385: "El Anillo", - 13387: "Ardenweald", - 13388: "Corazón del Bosque", - 13389: "Clarosereno", - 13390: "Cámara de la Memoria", - 13391: "Cámara de la Arconte", - 13392: "Tramo Destrozado", - 13393: "Estela Necrótica", - 13395: "Tirna Scithe", - 13396: "Espesura Velo de Niebla", - 13397: "Galería de Roble", - 13398: "Cámara Abandonada", - 13399: "Vigilia de Xandria", - 13400: "Sala de Skoldus", - 13402: "El Pincho", - 13403: "Cámaras de Fracturas", - 13404: "Forjas de Almas", - 13405: "Detrithos", - 13406: "Valle de las Mil Patas", - 13407: "Gruta Fusca", - 13408: "La Madriguera", - 13409: "Área de zona de los progenitores 1", - 13410: "Subforesta", - 13411: "Cruce Corazongélido", - 13412: "Mort'regar", - 13413: "La Parte Alta", - 13414: "Simposio del Pecado", - 13415: "Despertar del Aspirante", - 13416: "DEV Lowlands (DO NOT USE)", - 13417: "Bastión Invidente", - 13418: "Fortaleza del Maestro", - 13421: "Forja de la Pestilencia", - 13422: "Torre Quebrada", - 13423: "Desierto Burbujeante", - 13424: "Lodazal Virulento", - 13425: "Campos Rezumantes", - 13426: "Sagrario Purulento", - 13427: "Bosque Putrefacto", - 13428: "Dominio Empíreo", - 13429: "Galería de Roble", - 13430: "Espesura Velo de Niebla", - 13431: "Tirna Scithe", - 13432: "Descanso Desolado", - 13433: "La Zona Intermedia", - 13434: "El Observatorio", - 13435: "Sagrario Capanocturna", - 13437: "Refugio de Ve'nari", - 13438: "Cámaras de la Miseria", - 13439: "Salas Corazongélido", - 13440: "Pozo de la Angustia", - 13441: "Abismo Estigio", - 13442: "Aguja de Muro Cinéreo", - 13443: "Costurería de Carne", - 13455: "Pradera de Soryn", - 13456: "Consuelo de Haetio", - 13457: "Costa Dentada", - 13459: "Cuenca Aluvial", - 13460: "Forja Olvidada", - 13461: "Precipicio del Destino", - 13462: "Coliseo de Ascensión", - 13463: "Forja de Arrabio", - 13468: "Recodo Abandonado", - 13469: "Map 2437 [DEV AREA]", - 13470: "Terraza de la Gracia Interior", - 13471: "Cosecha Macabra", - 13472: "Último Tañido", - 13473: "Caída del Coraje", - 13474: "Zanja de los Exterminadores", - 13475: "Puente de la Contrición", - 13476: "Las Fauces", - 13477: "Altar Profanado", - 13478: "Rugido de la Muerte", - 13479: "Cuenca Desolada", - 13480: "Refugio Olvidado", - 13481: "Gruta Distante", - 13482: "Pozas Lejanas", - 13483: "Consuelo de Arkelios", - 13485: "Guarida de Kala", - 13486: "Depósito de Componentes", - 13487: "Grimorium", - 13488: "Sala de Escritos", - 13489: "Sala de Elixires", - 13490: "Sala de Brujería", - 13491: "Páramo Hambriento", - 13492: "Campos Manchados de Almas", - 13493: "Arcanum Primordial", - 13494: "Level Design [DEV MAP]", - 13495: "MattTestTerrain3", - 13496: "Tramo del Frío", - 13497: "Cámaras de Fracturas", - 13498: "Círculo de Transferencias", - 13499: "Círculo del Destino", - 13500: "Vigilia del Infinito", - 13501: "Santuario del Viento de Plata", - 13503: "Zolramus", - 13505: "Exoramas", - 13506: "awood_dev1", - 13507: "sbenyona_dev1", - 13508: "Capilla de Miri", - 13510: "Ascenso del Honor", - 13511: "Jardines del Reposo", - 13512: "Jardines Resplandecientes", - 13513: "Atrio de Piedad", - 13514: "Oratorio Solitario", - 13515: "Fuente de la Lealtad", - 13516: "Trono de la Arconte", - 13517: "Colina Criptaolvidada", - 13518: "Peldaños Hechiceros", - 13519: "Interactive World - Sparkles", - 13520: "Interactive World - Containers", - 13521: "Camino de la Sabiduría", - 13522: "Calabozo del Grabador de Runas", - 13523: "Salto del Pecado", - 13524: "Sala de las Espadas", - 13525: "Sala de Sortilegios", - 13526: "Sala de Icor", - 13527: "Cámara de Almas", - 13528: "Guarida de la Madre", - 13529: "Cavernas Resonantes", - 13530: "Aullido de la Muerte", - 13531: "Cobertizo del Torturador", - 13532: "Zone2lDoodadtest", - 13533: "Aguja de Guerra", - 13536: "Zereth Mortis", - 13537: "Torreón del Alba", - 13538: "Sanatorio del Extractor", - 13539: "Acherus: El Bastión de Ébano", - 13540: "Hoya de Stanikos", - 13541: "Enclave Velado", - 13542: "Caverna Afligida", - 13543: "Camino de las Tierras Altas", - 13545: "Exoramas", - 13546: "Torghast, Torre de los Condenados", - 13547: "Calabozo del Grabador de Runas", - 13548: "Puerto de Boralus", - 13549: "Tormenheim", - 13550: "Montañas Crestagrana", - 13552: "Sanatorio del Extractor", - 13553: "Salida de Las Fauces", - 13554: "10 Canyon", - 13555: "Reclusa de Seisvueltas", - 13556: "Revendreth", - 13560: "Detrithos", - 13561: "Sagrario de Dominación", - 13567: "Bancal de los Recolectores", - 13569: "Cólera del Carcelero", - 13570: "Korthia", - 13573: "Sepulcro de los Primeros", - 13574: "PTH Test Area", - 13575: "Cámaras de Adamantita", - 13576: "Zone2lDoodadtest2", - 13577: "Tazavesh, el Mercado Velado", - 13578: "Hacienda de Volo", - 13579: "Interactive World - Rain Test", - 13580: "Páramo Fúngico", - 13581: "Fronda Espesa", - 13582: "Filo de la Oscuridad", - 13583: "Cráter de Lug'thun", - 13585: "Interactive World - Night Test", - 13587: "Las Fauces", - 13588: "Llanos del Tormento", - 13589: "Madrigueras de las Bestias", - 13595: "Caldera de Zovaal", - 13596: "Desmotaeron", - 13597: "Gorgoa: Río de las Almas", - 13598: "Bastión de la Perdición", - 13599: "Crisol de los Malditos", - 13600: "Calcis", - 13601: "Cocyrus", - 13602: "Cuna de la Ruina", - 13603: "Soto de la Médula", - 13604: "Prodigum", - 13605: "Lamento del Corvino", - 13606: "Faz del Olvido", - 13607: "Altar de Dominación", - 13608: "Refugio de Ve'nari", - 13609: "Páramo Hambriento", - 13610: "Campos Manchados de Almas", - 13611: "Detrithos", - 13612: "Nido de Hilarruines", - 13613: "10.0 Hackathon: Living World", - 13619: "Ardenweald", - 13624: "Santuario de la Guía", - 13625: "Respiro del Guardián", - 13626: "Mirador del Magullador", - 13627: "Guarida del Erudito", - 13628: "Cuórum del Buscador", - 13629: "Nidal Borrascoso", - 13630: "Ascenso de la Esperanza", - 13631: "Cámara de los Secretos", - 13632: "Refugio", - 13633: "Campaña 9.1", - 13634: "El Tremaculum", - 13635: "Zone4TyrholdTest", - 13636: "10 Highlands", - 13637: "Bastión Celestial", - 13641: "Tormenheim", - 13642: "Islas Dragón", - 13643: "Mare Magnum", - 13644: "Orillas del Despertar", - 13645: "Llanuras de Ohn'ahra", - 13646: "Tierras Azures", - 13647: "Thaldraszus", - 13648: "Torturadores de Torghast", - 13650: "Andorhal", - 13652: "Relicario de Recuerdos", - 13653: "Pináculo de Dominación", - 13654: "Filo del Abismo", - 13655: "El Crisol", - 13656: "Cueva de los Secretos", - 13657: "Círculo de la Gloria", - 13658: "Gracia del Peregrino", - 13659: "Cúspides Resonantes", - 13660: "Caverna de la Contemplación", - 13664: "Calabozo del Grabador de Runas", - 13666: "Fortaleza de Helgarde", - 13667: "Cresta de la Claridad", - 13669: "Jardines Esmeralda", - 13671: "Cámara del Sigilo", - 13672: "Tazavesh, el Mercado Velado", - 13673: "Estuario del Despertar", - 13674: "Zerekriss", - 13677: "El Camino del Entendimiento", - 13680: "Repositorio Ta", - 13681: "Sanatorio del Extractor", - 13682: "Cuenca Desolada", - 13683: "Cuna del Nacimiento", - 13684: "Descanso Desolado", - 13685: "Caverna Afligida", - 13686: "Rugido de la Muerte", - 13687: "Aullido de la Muerte", - 13688: "Guarida de la Madre", - 13689: "Cobertizo del Torturador", - 13690: "Reposo de la Fe", - 13691: "Cámara del Conocimiento", - 13692: "Cámara de la Sabiduría", - 13694: "Cuenca del Exilio", - 13695: "La Falla", - 13697: "Precipicio Encadenado", - 13699: "Trono de la Arconte", - 13700: "Cueva de los Grómits", - 13701: "Fuente de la Lealtad", - 13702: "Verdor Indómito", - 13703: "Humedales Inquietos", - 13704: "Castillo Quiebrascama", - 13705: "Provis Esper", - 13706: "Provis Fauna", - 13707: "Sede Ka'a", - 13708: "Llanura de la Materialización", - 13709: "Invernadero de la Protectora", - 13710: "Remanso Uktulut", - 13711: "Observatorio Concordia", - 13713: "Senda Perdida", - 13714: "Campamento Cortaescama", - 13715: "Rápidos Desbordantes", - 13716: "Primavera Rebosante", - 13717: "Ciudadela Obsidiana", - 13718: "Roca de Hudson", - 13719: "Ascenso Ardiente", - 13720: "Fortaleza de Ruinadragón", - 13721: "Cumbres Quiebrascama", - 13722: "Costa Indómita", - 13723: "Avanzada Corazón de Dragón", - 13724: "Observatorio Cima Celestial", - 13725: "Ruinas de la Cámara de la Vida", - 13726: "Cuenca de los Cornisapos", - 13727: "Vitasagrario Rubí", - 13728: "Estanques de Vida Rubí", - 13729: "Arco de Vida Desmoronado", - 13730: "Enclave Raudoescarcha", - 13731: "Observatorio del Criadero", - 13732: "Campamento Base Dragontina", - 13733: "Reserva Agreste", - 13734: "Fronda de la Cúspide", - 13735: "Islote Lev'al", - 13736: "Observatorio de la Cúspide", - 13738: "Observatorio Obsidiana", - 13739: "Baluarte Obsidiana", - 13741: "Barranco Colmillorroca", - 13742: "Sepulcro de los Primeros", - 13743: "Avanzada Pasoleño", - 13744: "Residuo Coral", - 13745: "Fangal Leñanudo", - 13746: "Cuerno de Drusahl", - 13747: "Avanzada Aylaag", - 13748: "Bahía Silbajunco", - 13749: "Aldea Aletalodo", - 13750: "Trecho de Rusza'thar", - 13751: "Tierras Altas de Shikaar", - 13752: "Nidal de los Dracos de Tormenta", - 13753: "Criptas de lo Eterno", - 13754: "Vientos Segadores", - 13755: "Paso de Crucerrío", - 13756: "Portal del Terror", - 13757: "Arenas Infinitas", - 13758: "Índice del Orden", - 13759: "Mirador Abandonado", - 13761: "Alto Son del Viento", - 13762: "Claro Nemoroso", - 13763: "Cuenca Bosquetrébol", - 13764: "Maruukai", - 13765: "Nidal de Ohn'ahra", - 13766: "Ruinas de Nelthazan", - 13767: "Vigía de Ascuas", - 13768: "Cámara Durmiente", - 13769: "Confín Olvidado", - 13770: "Campo de Ferocidad", - 13771: "Daruukhan", - 13772: "Avanzada Anchapezuña", - 13774: "Campamento Kaighan", - 13775: "Manantiales Ohn'iri", - 13776: "Géiseres Ohn'iri", - 13777: "Retiro Crestagua", - 13778: "Espejo del Cielo", - 13779: "Szar Skeleth", - 13780: "Teerakai", - 13781: "Kurgans Eternos", - 13782: "Bastión Nokhudon", - 13787: "Agarre de Zovaal", - 13788: "Bancal de Formación", - 13790: "zsewell", - 13791: "Jardines Catalizadores", - 13793: "Caverna Geométrica", - 13794: "Osario Velado", - 13795: "Academia Algeth'ar", - 13796: "Corte de Algeth'era", - 13797: "Lago Eclipse", - 13798: "Gruta del Draco", - 13799: "Cavernas Chasqueadoras", - 13800: "Campamento Fétido", - 13801: "Tyrhold", - 13802: "Reserva de Tyrhold", - 13803: "Mirador Gelikyr", - 13804: "Zarzal Espinatrueno", - 13805: "Claro Léxico", - 13806: "Nido Bélico", - 13807: "Tierras Ancestrales Weyrn", - 13808: "Cráter Hiendetormenta", - 13809: "Caldera de los Ensalmadores", - 13810: "Cámara Petraescarcha", - 13811: "Sima Nidal", - 13812: "Jardines de la Unidad", - 13813: "Puerta del Fuerte Sur", - 13814: "Santuario del Jardín", - 13815: "Valle Calígine", - 13816: "Cima de Embozo de Tormenta", - 13817: "Mar de Dunas", - 13818: "Confluencia Temporal", - 13819: "Área en Desuso de Thaldraszus", - 13820: "Margen del Eón", - 13821: "Sagrario Perdido en las Arenas", - 13822: "Bosque Fluyente", - 13823: "Pasadizo del Tiempo", - 13825: "Archivo Azur", - 13826: "Destello Kauriq", - 13827: "Colina Apacible", - 13828: "Imbu", - 13829: "Vereda de Roemadera", - 13830: "Poblado Petrapellejo", - 13831: "Guarida Pinoherrumbre", - 13833: "Valle Red Trémula", - 13834: "Campamento de Antonidas", - 13835: "Galería Cobalto", - 13836: "Salto de los Astudracos", - 13837: "Iskaara", - 13838: "Orilla Górloc", - 13839: "Cámara de Formación", - 13840: "Repositorio de Protoformas", - 13841: "Camino del Origen", - 13842: "Vestíbulo de la Génesis", - 13843: "Colinas del Gran Árbol", - 13844: "Reposo del Traidor", - 13845: "Campamento del Risco Djaradin", - 13846: "Criadero de los Dracos", - 13847: "Hondonada Frondacuero", - 13848: "Poza Frondacuero", - 13849: "Fortaleza Kalthraz", - 13850: "Charca Dientenegro", - 13851: "Biblioteca Cobalto", - 13852: "Cubil Arroyodientes", - 13853: "Coto de Vakthros", - 13854: "Primalist POI", - 13855: "Primalist POI 2", - 13856: "Riscos de los Gigantes de Escarcha", - 13857: "Cuenca Pelaje Invernal", - 13858: "Cañón Brumoso", - 13859: "Campamento Desubicado", - 13860: "Tierras de Escarcha Superiores", - 13861: "Riscos Plumazul", - 13862: "Valdrakken", - 13863: "Oribos", - 13864: "Puesto Gelikyr", - 13865: "Santuario del Este", - 13866: "Paso de las Sombras", - 13867: "Acantilados Rebeldes", - 13868: "Sauna de Sueños Serenos", - 13869: "Gran Altiplano", - 13870: "mesten_dev1", - 13871: "Provis Strata", - 13872: "Mirador de los Señores de la Garfa", - 13873: "Pasaje de Riscoacero", - 13874: "Paso Escamado", - 13875: "Saliente de las Sombras", - 13876: "Cascadas", - 13877: "Muralla de Riscoacero", - 13878: "Vega del Acueducto", - 13879: "Explanada de los Ruegos", - 13880: "Instalación Titánica Sellada", - 13881: "Acantilados Salvajes", - 13882: "Provis Flora", - 13883: "Provis Terra", - 13884: "Mirador Temporal", - 13885: "Cueva de los Elementales de Hielo Colmillarr", - 13886: "Rincón del Ballenero", - 13887: "Torre de Iskhdormi", - 13888: "Atalaya de Tres Cascadas", - 13889: "Ruinas de Karnthar", - 13890: "Escondite Terrestre", - 13891: "Reposo Grávido", - 13892: "Escondrijo de Colmigrima", - 13893: "Estanque Ojo de Draco", - 13894: "Cavernas Centiescama", - 13895: "Atalaya de Theron", - 13896: "Isla de los Antecesores", - 13897: "Cataratas Dimensionales", - 13898: "Distritos Catalizadores", - 13918: "Valle Primordial", - 13921: "Rama Ancestral", - 13922: "Fuentes de Dragón Rugientes", - 13923: "Zarzas Frondacuero", - 13924: "Forja del Más Allá", - 13925: "Fragmentos Atemporales", - 13927: "Precipicio del Zángano", - 13928: "Fundición Floreciente", - 13931: "Avanzada Zelthrak", - 13932: "Campamento Nievepiel", - 13933: "Cubil de Kargozarpa", - 13934: "Escudo de Rhonin", - 13935: "Círculo del Pensamiento", - 13936: "Tierra de Marie Lazar", - 13938: "Falla Fisurada", - 13939: "Embajada Reposo Alado", - 13940: "Criadero Salvaje", - 13941: "Trono Obsidiana", - 13942: "Avanzada Escama Rubí", - 13943: "Acantilado Desmoronado", - 13944: "Asalto Raudoescarcha", - 13946: "Granja Pandaren de Marie Lazar", - 13948: "Campos de la Génesis", - 13949: "Nido de Garudeon", - 13950: "Claro Idílico", - 13952: "Locris Esper", - 13954: "Cámara Azur", - 13955: "LDNewHire", - 13956: "Custodia Eterna", - 13957: "Hogar Inmortal", - 13958: "Cuna de la Génesis", - 13959: "Fundición Infinita", - 13960: "Bastión del Guardián", - 13961: "Llanuras Efímeras", - 13962: "Pozo Devorador", - 13963: "Trapaza de los Especuladores", - 13964: "Acantilados Fulgurantes", - 13965: "Yugo de la Dominación", - 13966: "Repositorio de Génesis", - 13967: "Alcoba de Génesis", - 13968: "Uldaman", - 13969: "Plan Maestro", - 13970: "Arena Enigma", - 13971: "Coliseo de Maldraxxus", - 13972: "Coliseo de Maldraxxus", - 13975: "Claros de Tirisfal", - 13976: "Nido Humeante", - 13977: "Gruta Perdida", - 13978: "Guarida de los Guadáñidos", - 13979: "Ruinas de Lordaeron", - 13980: "Rémol", - 13981: "Lago Aguasclaras", - 13982: "Llanuras de Ohn'ahra", - 13983: "Claros de Tirisfal", - 13984: "Granja de Garren", - 13985: "Costa Norte", - 13986: "Mansión Friogar", - 13987: "Charca Aguaserena", - 13988: "Dominios de Calston", - 13989: "Apeadero del Ocelo de la Muerte", - 13990: "Posada de Rémol", - 13991: "Hondonada Frondacuero", - 13992: "El Futuro Primalista", - 13993: "Guerra Gnoll", - 13994: "Guerra del Mar de Dunas", - 13995: "Revolución Pandaren", - 13996: "Imperio Negro", - 13997: "Creektooth Den - Decayve", - 13998: "Afueras de Frondacuero", - 13999: "Nido de Asedio", - 14000: "Nido de Apoyo", - 14001: "Tumba de los Fieles", - 14002: "Mirador Hiendetormenta", - 14003: "Cuenca Arena Oscura", - 14004: "Tesoro de Faucentella", - 14006: "Burning Ascent [DNT]", - 14007: "Ciudadela Obsidiana", - 14009: "The Waking Shores [DNT]", - 14010: "Santuario Sombreado", - 14011: "Neltharus", - 14012: "Cienoescoria", - 14013: "Flujo de Ascuas", - 14014: "Terreno de Contenido", - 14017: "Cumbre de Vakthros", - 14018: "Reposo Frondoso", - 14019: "Vigilia de Merithra", - 14020: "Lunedania", - 14022: "Caverna Zaralek", - 14023: "JrzTest", - 14030: "Cámara de las Encarnaciones", - 14031: "Caverna Pizarrascua", - 14032: "Academia Algeth'ar", - 14033: "El Cuadrángulo", - 14034: "Bancal de los Sermones", - 14035: "Campo", - 14036: "Enclave de la Directora", - 14037: "Invernáculo", - 14038: "Env Art Land - Detail Doodad Test", - 14039: "Punto de Pesca de Khuri", - 14040: "Veta Fragmento Cenizo", - 14042: "Nido de Pinchocuernos", - 14045: "Claros de Tirisfal", - 14046: "Claros de Tirisfal", - 14047: "Cámaras Destrozadas", - 14050: "Cascadas Petrapiel", - 14054: "Vereda de Nokhud", - 14056: "Costa de la Tempestad", - 14063: "Estanques de Vida Rubí", - 14064: "Jardines Gelikyr", - 14065: "Loma Fundida", - 14071: "Ateneo Perdido", - 14075: "Colinas Saltarinas", - 14079: "Paseo Lila", - 14081: "Casa de las Plagas", - 14082: "Salas de Infusión", - 14083: "Crisol Enigma", - 14084: "Camino del Sueño Esmeralda", - 14085: "Cascadas Génesis", - 14086: "Salas Escamaceniza", - 14087: "Salas Escamaceniza", - 14088: "Campamento Gotarrisco", - 14089: "Vakthros", - 14091: "Caverna de Vakthros", - 14092: "Azmerloth", - 14093: "[PH] Epoch's Rest", - 14094: "Estepa del Errante", - 14095: "Cámara de las Encarnaciones", - 14096: "Guardería Escamitas", - 14097: "Soto Llamanzana", - 14098: "Azmerloth - Área intemporal", - 14099: "Caverna Sísmica", - 14100: "Cuenca Aguaclara", - 14101: "Pinar Sabiacresta", - 14103: "Ruinas Perdidas", - 14104: "Poblado Toghusuq", - 14105: "Cicatriz de la Tormenta", - 14106: "Cueva en Erupción", - 14107: "Avanzada del Invernadero", - 14142: "Fosas de Alimentación Salvajes", - 14143: "Excavación", - 14144: "Sala de los Guardianes", - 14145: "Excavación Abandonada", - 14146: "Cámara de Tyr", - 14147: "Puesto del Pinar", - 14148: "Fuera de los Límites Temporales", - 14149: "Revolución Pandaren - Tierra de Nadie", - 14150: "Isla Calavera de Dragón", - 14151: "Guerra Gnoll - Tierra de Nadie", - 14152: "Guerra del Mar de Dunas - Tierra de Nadie", - 14153: "Montaña Hiendetormenta", - 14154: "Costa Escama Cortante", - 14155: "Islote Morqut", - 14156: "Terrenos de los Cornisapos", - 14157: "Cámara de las Llamas", - 14158: "Gruta Siseante", - 14159: "Cubil Arroyodientes", - 14160: "Caverna Cortaescama", - 14343: "Sala de los Registros", - 14344: "Salón de Muestras", - 14345: "Salón de los Aspectos", - 14346: "Muelle de Uktulut", - 14347: "Puesto de Vistamontaña", - 14348: "Caverna del Aleteo", - 14349: "Cima Fuentes de Dragón", - 14350: "Observatorio de la Protectora", - 14355: "Abrevadero", - 14356: "Mallakh", - 14357: "Tierras Altas de Arathi", - 14429: "Cuenco Lleno", - 14430: "Reposo del Mago", - 14431: "Carpa de Mulsted", - 14432: "Posada La Bisagra Desaparecida", - 14433: "El Confín Olvidado", - 14434: "Posada La Galleta Conjurada", - 14436: "Terreno de pruebas Nokhudon", - 14437: "Campo de Entrenamiento", - 14438: "Cueva de Khadin", - 14440: "El Tesoro Oculto del Dragón", - 14441: "La Caldera Ardiente", - 14442: "Kanniak Perdido", - 14443: "Mirador del Rompetripas", - 14444: "Bosque Podrido", - 14445: "Cubil de la Descomposición", - 14447: "Cavernas Cepomuerte", - 14448: "Vitasagrario Rubí", - 14449: "Incursión Raudoescarcha", - 14450: "Cámaras de Infusión", - 14451: "Mirador Rubí", - 14452: "Posada de la Escama Verde", - 14454: "Confluencia Temporal", - 14455: "Forja de Arcanum", - 14456: "Isla de Kilpi", - 14457: "Sully Test Start", - 14458: "Isla Garralarga", - 14459: "Sully Test Plains", - 14460: "Sully Test Desert", - 14461: "Sully Test Forest", - 14462: "Cresta Espinatrueno", - 14463: "Avanzada Vigía del Águila", - 14464: "Cataratas Solitarias", - 14465: "Cueva de Hielo de los Astudracos", - 14466: "Rincón del Desterrado", - 14467: "Guarida del Corazón Pétreo", - 14468: "Apretón de Crines", - 14475: "Nidal de Rhuzast", - 14476: "Maruukai", - 14477: "Kurgans Eternos", - 14478: "Bastión Nokhudon", - 14479: "Vereda de Nokhud", - 14480: "Ofensiva Nokhud", - 14481: "Batalla de las Lanzas", - 14482: "Manantiales Ohn'iri", - 14484: "10.2 Devland", - 14485: "Llanuras de Ohn'ahra", - 14486: "Caverna Abandonada", - 14487: "Mirador Antiguo", - 14488: "Torre de Algeth'ar", - 14489: "Trono de los Aspectos", - 14490: "Rostro Victorioso", - 14491: "Laboratorio Nocturno", - 14492: "Mercado Negro", - 14493: "Anexo del Archivo Azur", - 14494: "Mirador de la Cascada", - 14495: "Nido Elevado", - 14497: "Filo de la Cascada", - 14498: "Festín Rubí", - 14499: "Refugio de Gladiador", - 14500: "Mirador del Cañón Brumoso", - 14501: "El Baño de Burbujas", - 14502: "La Copa de Despedida", - 14504: "Enclave Zafiro", - 14505: "Enclave Rubí", - 14506: "Enclave Obsidiana", - 14507: "Enclave Bronce", - 14508: "Enclave Esmeralda", - 14509: "Mercado de los Artesanos", - 14510: "Mare Magnum", - 14511: "Bahía Reposo Alado", - 14512: "Árbol Renacido", - 14513: "La Dragontina Robusta", - 14514: "Amanecer del Infinito", - 14515: "Charcos Ley", - 14516: "Canal Perdido", - 14517: "11Test1", - 14518: "Claros Orientales", - 14519: "10Zone6-Devland", - 14520: "Loamm", - 14523: "11 Zone3", - 14524: "Avanzada Uktulut", - 14525: "Valle de Alterac", - 14526: "Bastión Lobo Gélido", - 14527: "Aldea Lobo Gélido", - 14528: "Cámara de los Lobo Gélido", - 14529: "Sueño Esmeralda", - 14530: "Tesoro de Valdrakken", - 14532: "zzOLDThe Lost Atheneum", - 14533: "zzOLDThe Old Weyrn Grounds", - 14534: "Nido de Asedio", - 14535: "Nido de Apoyo", - 14537: "zzOLDHornswog Grounds", - 14538: "Guarida de los Guadáñidos", - 14541: "zzOLDScaleborn Pass", - 14544: "zzOLDStormsunder Mountain", - 14545: "Mirador Hiendetormenta", - 14546: "Mirador de los Señores de la Garfa", - 14582: "Cuenca Arena Oscura", - 14583: "Costa de la Tempestad", - 14584: "Tierras Ancestrales Weyrn", - 14585: "Cráter Hiendetormenta", - 14586: "Caldera de los Ensalmadores", - 14587: "Cámara Petraescarcha", - 14588: "Sima Nidal", - 14589: "Mirador de los Señores de la Garfa", - 14590: "Paso Escamado", - 14591: "Ateneo Perdido", - 14592: "Montaña Hiendetormenta", - 14593: "Costa Escama Cortante", - 14594: "Islote Morqut", - 14595: "Terrenos de los Cornisapos", - 14596: "Isla Calavera de Dragón", - 14597: "Nido Bélico", - 14598: "zzOLDDarksand Hollow", - 14599: "zzOLDTempest Coast", - 14603: "zzOLDFroststone Vault", - 14605: "Mirador de los Señores de la Garfa", - 14611: "zzOLDHornswog Grounds", - 14614: "Nido Elevado", - 14615: "11Test2", - 14616: "Cuencas Brilloscuro", - 14617: "Trono de los Aspectos", - 14618: "Azure Vault (delete me)", - 14619: "Cámaras de Zskera", - 14620: "Elección de Dragón Azul", - 14643: "Amirdrassil, la Esperanza del Sueño", - 14644: "Lago Abisal", - 14645: "Nido de los Despellejadores", - 14646: "Nal ks'kol", - 14647: "Campamento de la Llama Hendida", - 14648: "Acceso a Aberrus", - 14649: "Yermos de Azufre", - 14650: "Ruinas de Drekazera", - 14651: "Barranco Muerdeácido", - 14652: "Ruinas del Campo de Batalla", - 14653: "Caldera Zaqali", - 14654: "Nidal del Reptadraco", - 14655: "Reposo Obsidiana", - 14656: "Morqut", - 14657: "Nuevas contrataciones WB", - 14658: "11Test3", - 14659: "Campamento Dragontina", - 14660: "Trono Veridiano", - 14661: "11Test4", - 14662: "11Test5", - 14663: "Aberrus, el Crisol Ensombrecido", - 14664: "Cámara Enterrada", - 14665: "Khaz Algar", - 14667: "El Pasaje", - 14668: "ZZ_AlexandraDD", - 14669: "Campos de Cristal", - 14670: "Tumba de los Fieles", - 14671: "Tesoro de Faucentella", - 14672: "Gruta Perdida", - 14682: "Guarnición Azufre", - 14683: "Torres Fulgurantes", - 14684: "Choza del Ermitaño", - 14685: "Pastos Dentados", - 14686: "Tierra de las Profesiones", - 14687: "Subzona del Lago", - 14688: "Subzona del Lago de Lava", - 14689: "Rincón de Tyler", - 14690: "Cañón de las Cascadas", - 14691: "Atalaya de la Garfa", - 14692: "Cámaras de Zskera", - 14693: "Avanzada de la Aguja Obsidiana", - 14694: "Gambito de Iridikron", - 14695: "Gambito de Iridikron", - 14696: "Destellorogg", - 14697: "Espinazo Escarchado", - 14698: "Cala de la Tempestad", - 14699: "Mirador del Señor Alado", - 14700: "Nidal del Astudraco", - 14701: "Costa Velada", - 14702: "Mirador del Cielo", - 14703: "Pico Petraescarcha", - 14704: "Desbordamiento Fundido", - 14705: "Aberrus, el Crisol Ensombrecido", - 14706: "La Vorágine", - 14707: "Amy Test Forest", - 14708: "Amy Test Town", - 14709: "Shane Test Area", - 14710: "Mirada de Neltharion", - 14711: "Pasaje", - 14712: "Atalaya de Igira", - 14713: "Lago Brilloespora", - 14714: "Columna de las Cascadas", - 14715: "Don de los Ancianos", - 14716: "Oasis Ardiente", - 14717: "Isla de Dorn", - 14718: "Consuelo del Guardián", - 14719: "Aterrizaje de Ébano", - 14720: "Tabla rota", - 14721: "Cueva Misteriosa A", - 14723: "11Test6", - 14725: "Galería de los Huesos", - 14727: "Cámara Azur", - 14730: "Nido Abandonado", - 14731: "Cueva Misteriosa B", - 14732: "Cueva Misteriosa C", - 14733: "Cueva Misteriosa D", - 14734: "Cueva Misteriosa E", - 14735: "Cueva Misteriosa F", - 14736: "Cueva Misteriosa G", - 14737: "Cueva Misteriosa H", - 14738: "Cueva Misteriosa I", - 14739: "Cueva Misteriosa J", - 14740: "Colmillos del Guardián", - 14742: "Posada de Todos los Momentos", - 14743: "Camino Viejo a Loamm", - 14744: "Camino Viejo a Loamm", - 14745: "Brecha del Oteador", - 14746: "Brecha del Oteador", - 14747: "Zone 4 Dev Land", - 14748: "Khaz Algar", - 14750: "Islas Dragón", - 14751: "Orillas del Despertar", - 14752: "Azj-Kahet", - 14753: "Ciudad Tejida", - 14754: "Gusanales", - 14755: "Guarida de la Tejedora", - 14756: "Fuerte Asedio", - 14757: "Rak-Ahat", - 14758: "Confín Arathi", - 14759: "Abismo Escurridizo", - 14760: "Rak-Zakaz", - 14761: "Resguardo Sedoso", - 14762: "Lago Quebrado", - 14763: "Respiradero", - 14764: "Extensión del Este", - 14765: "Garganta Espasmódica", - 14766: "Campamento Salvaje Ul'ar", - 14767: "Mmarl", - 14768: "Rak-Ush", - 14769: "Hondura Delirante", - 14770: "Azmerloth", - 14771: "Dornogal", - 14772: "Aldea Foldefrey", - 14773: "Minarroca", - 14774: "Orilla Tranquila", - 14775: "Guardia de la Tormenta", - 14776: "Proscenio", - 14777: "Cuenca de Boscorraíz", - 14778: "Costa Destellante", - 14779: "Tramo de Golgrin", - 14780: "Dhar Bandar", - 14781: "Lagar de Tragoceniza", - 14782: "Foldeferro", - 14783: "Dhar Kazhad", - 14784: "Villatrueno", - 14785: "Molino Skolzgal", - 14786: "Foldeceniza", - 14787: "Alto del Luto", - 14788: "Minaópalo", - 14789: "Dhar Oztan", - 14790: "Transformatorio", - 14791: "Bazar Umbrío", - 14792: "Palacio Nerub'ar", - 14793: "Madejas", - 14794: "Madrigueras", - 14795: "Cavernas Resonantes", - 14796: "Gundargaz", - 14797: "Estación Tirahierro", - 14798: "Estación de Bombeo", - 14799: "Puerta de Santificación", - 14800: "Taller Terráneo", - 14801: "Mina de Nibelgaz", - 14802: "Minas Perdidas", - 14803: "Túneles", - 14804: "Guarida Chasqueadora", - 14805: "Bancal de Brunwin", - 14807: "Plataforma Relumbrante", - 14808: "Yermos Ruinosos", - 14809: "Cuencas Altas", - 14810: "Río de las Cataratas", - 14811: "Gruta Viviente", - 14812: "Punto Oportunidad", - 14813: "Sima Lóbrega", - 14814: "Campamento Murroch", - 14815: "Mina de Taelloch", - 14816: "Cuenca Obsidiana", - 14817: "Exterior de la Petrocámara", - 14818: "Exterior de la Grieta de Flama Oscura", - 14819: "Excavación Abisal", - 14820: "Punto de Vetasombría", - 14821: "Gruta Fétida", - 14822: "Aguja Piedrapilar", - 14823: "", - 14824: "", - 14825: "", - 14826: "", - 14827: "", - 14828: "", - 14829: "", - 14830: "", - 14831: "", - 14832: "", - 14833: "", - 14834: "", - 14835: "", - 14836: "", - 14837: "Cabaña de Durgaz", - 14838: "Santificación", - 14839: "Gruta del Rencor", - 14840: "A.Z.E.R.O.T.H.", - 14841: "Azq'roth", - 14842: "Ulderoth", - 14843: "Azoteroth", - 14844: "Azegonía", - 14845: "Tierras del Conflicto", - 14846: "Ulderoth", - 14848: "Muro de la Égida", - 14849: "Cámaras del Valor", - 14850: "Campo Vibratorio", - 14851: "Campos Fúngicos", - 14852: "Charca Piedraserena", - 14853: "Mazmorra de prueba", - 14854: "Finca Yelmocolina", - 14855: "11Test7", - 14856: "Rama Ancestral", - 14857: "Cruce Superficial", - 14858: "Desembarco Verdeante", - 14859: "Campos del Ensueño", - 14860: "Amirdrassil", - 14861: "Brecha Ígnea", - 14862: "Túmulos del Ensueño", - 14863: "Bastión Primalista", - 14864: "Sima Agostadora", - 14865: "Bobina del horno", - 14872: "Monasterio Escarlata", - 14873: "Jardines de la Capilla", - 14874: "Claustro Abandonado", - 14875: "Manantiales del Peñascal", - 14876: "Ciénaga Cantarina", - 14877: "Raíces Costeras", - 14880: "Ramas Benditas", - 14882: "Grieta de Flama Oscura", - 14883: "La Petrocámara", - 14885: "Vestíbulo Temporal", - 14886: "Sagrario de la Cronología", - 14887: "Umbral de los Milenios", - 14888: "Las líneas temporales", - 14889: "Enclave de la Eternidad", - 14890: "Radio del Invierno Interminable", - 14891: "Caída de Galakrond", - 14892: "Encrucijada del Destino", - 14893: "Cavernas Gélidas", - 14894: "Precipicio de Iridikron", - 14895: "Confluencia Infinita", - 14896: "Vereda Sinuosa", - 14897: "Campo de Batalla Inmemorial", - 14898: "Ascenso de Murozond", - 14899: "Bosque Fluyente", - 14900: "Arenas cambiantes", - 14901: "Fosos de Sangre", - 14902: "Sima Susurrante", - 14903: "Punto Golpehondo", - 14904: "Ojo de Ysera", - 14905: "Bastión de Ruinaescoria", - 14908: "11.0 - Underground Test", - 14910: "Refugio de las Cascadas", - 14911: "Cataratas de Piedra celestial", - 14912: "Mirador del Manantial", - 14913: "Manantial de la Vida", - 14914: "Cañón Bravofuego", - 14915: "Campo de Llamas", - 14916: "Amabilidad de Dunelle", - 14917: "Mereldar", - 14918: "Priorato de la Llama Sagrada", - 14919: "Cenagal Estancado", - 14920: "La Cuenca", - 14921: "Abundancia de Beledar", - 14922: "Campos de Primaverdía", - 14923: "Los Colmillos", - 14924: "Ronstropicio", - 14925: "Tierras de Veneración", - 14926: "Reclamación de Velhan", - 14927: "Desembarco de Lothar", - 14928: "Baluarte de la Luz", - 14929: "Paciencia de Livia", - 14930: "Ascenso de Tenir", - 14934: "Forja del Cronoherrero", - 14935: "Cámara de Ferrodormu", - 14936: "Bastión Nocturno", - 14937: "Santuario de la Raíz", - 14938: "El Grajero", - 14939: "Cima Cenicienta", - 14940: "Raíces Serpenteantes", - 14942: "Bosque Retorcido", - 14943: "Zarzal Veridiscente", - 14944: "Inframar", - 14945: "Avance de Faerin", - 14946: "Pilar-Nido Vosh", - 14947: "Pilar-Nido Noth", - 14948: "Pilar-Nido Xesh", - 14949: "Canales Lóbregos", - 14950: "Ojo de Ansurek", - 14951: "Senda de la Seda", - 14952: "Sima Reptante", - 14953: "Vía Hilandera", - 14954: "Priorato de la Llama Sagrada", - 14955: "Calcina", - 14956: "Riscos Sueñofrondoso", - 14957: "Capricho Fúngico", - 14958: "Campamento Salvaje Or'lay", - 14959: "Arrecife Lúcido", - 14960: "Campamento Central", - 14961: "Arboleda del Escalofrío", - 14962: "El Otro Lado", - 14963: "Mapa de desarrollo - Entrenamiento de Misión", - 14964: "Orillas Somnolientas", - 14965: "Vista del Despertar", - 14966: "Mapa de desarrollo - Confín del Exilio", - 14967: "Mapa de desarrollo - Zereth Mortis", - 14968: "Mapa de desarrollo - Mecandria", - 14969: "Amirdrassil", - 14970: "El Imperio Negro", - 14971: "El Rompealbas", - 14972: "Costa de Peridoto", - 14973: "Río de Lluvia Primaveral", - 14974: "Arboleda Humeante", - 14975: "Baluarte Humeante", - 14976: "Marisma Dichastival", - 14977: "Arrecife Escarpado", - 14978: "Arrecife Escarpado", - 14979: "Ciudad Tejida", - 14980: "Palacio Nerub'ar", - 14981: "Rama Ancestral", - 14982: "Jardines Esmeralda", - 14983: "Lunedania", - 14984: "Cicatriz de la Tormenta", - 14985: "Tierras Azures", - 14986: "Llanuras de Ohn'ahra", - 14987: "James's Dynamic Mount Testground", - 14988: "Niebla Espiral", - 14990: "Barranco de Cargahonda", - 14991: "Jardín Salvaje", - 14992: "Paso Verdeante", - 14993: "Puerto Quilla", - 14994: "Cuenca del Pico Onírico", - 14995: "Reposo de Aderic", - 14996: "Los Cabos", - 14997: "Mansión de Cringrís", - 14998: "Caverna del Micomante", - 14999: "Minas Reptaterra", - 15000: "Reposo de Kriegval", - 15001: "Guarida de Zekvir", - 15002: "Estación de Bombeo", - 15003: "Foso del Pavor", - 15004: "Brecha Deslizante", - 15005: "Sagrario del Ocaso", - 15006: "Sumidero", - 15007: "Espiral Serpenteante", - 15008: "Abismo de Tak-Rethan", - 15009: "Infrastillo", - 15010: "Líneas temporales", - 15011: "FX - Test Dungeon", - 15013: "Guarida del Túmulo de Sor'theril", - 15014: "Ocaso Floreciente", - 15015: "Cuenca Espirala", - 15017: "Ribera Tejerraíz", - 15019: "Pasaje Tallado", - 15020: "Gran Cementerio de Raíles", - 15021: "El Ceratorio", - 15022: "Salón del Rey Vela", - 15023: "Profundidades Lóbregas", - 15024: "Mina Antorcha", - 15025: "Orilla Desvaída", - 15026: "Corazón de Amirdrassil", - 15027: "Vía del Núcleo 2510", - 15028: "Aberrus, el Crisol Ensombrecido", - 15029: "Pico Tronatesta", - 15030: "Taller de Gólems Forjahonda", - 15031: "Cantera de Vetasombría", - 15032: "Ciénaga de las Sombras", - 15033: "Frente Quebrado", - 15034: "Chispaluz", - 15035: "Playa del Sol Ausente", - 15036: "Charco Hambriento", - 15037: "Floración de la Luz", - 15038: "Baliza del Acantilado", - 15039: "Dhar Durgaz", - 15040: "Los Tres Escudos", - 15041: "Frondavil", - 15042: "Bancal de los Guardianes", - 15043: "Tierras de la Forja", - 15044: "Casa de la Fundación", - 15045: "Thul Medran", - 15046: "Vía del Núcleo", - 15047: "Avanzada del Descenso", - 15048: "Sala del Despertar", - 15049: "Foso de Volcoross", - 15050: "Claustro del Manantial", - 15051: "Cámara del Corazón", - 15052: "Grieta de Flama Oscura", - 15053: "Nido de Aviana", - 15055: "zzoldPriory of the Sacred Flame", - 15056: "Reposo de Tyr", - 15057: "Castillo de Stromgarde", - 15058: "Tierras Altas de Arathi", - 15059: "Finca Nueva", - 15060: "Aserradero de las Tierra Altas", - 15061: "Senda del Valor", - 15062: "Mina de Galson", - 15063: "Círculo de Vínculo Interior", - 15064: "Nido Alto", - 15065: "Cruce Redilnorte", - 15066: "Muralla de Thoradin", - 15067: "Cumbre Destral", - 15068: "Ar'gorok", - 15069: "Mina Mostacho Seco", - 15070: "Círculo de los Elementos", - 15071: "Cala de Faldir", - 15072: "Puente Thandol", - 15073: "Refugio de la Zaga", - 15074: "Círculo de Vínculo Exterior", - 15075: "Granja de Dabyrie", - 15076: "Granja Go'shek", - 15077: "Sala Puño de Roca", - 15078: "Poblado Secacorteza", - 15079: "Círculo de Vínculo Este", - 15080: "Sentencia", - 15081: "Cañón Mostacho Seco", - 15082: "Mar Adusto", - 15083: "Bahía Brebaje", - 15084: "Arrecife Hundido", - 15085: "Mare Magnum", - 15086: "Trecho de Caidaclara", - 15087: "Reposo del Trabajo", - 15088: "Paso en Disputa", - 15089: "Granja de Tuétano", - 15090: "Carro de los Renegados", - 15091: "Cordón Destrozado", - 15092: "Asiento Desatendido", - 15093: "Ara-Kara, Ciudad de los Ecos", - 15094: "Paraje Alzaocaso", - 15096: "Desembarco del Invierno", - 15097: "Anhelo de Sina", - 15098: "Puerta Insigne", - 15099: "Estación de Bombeo", - 15100: "Taller Terráneo", - 15101: "Cruce", - 15103: "Lagar de Tragoceniza", - 15104: "Taelloch", - 15105: "Amirdrassil", - 15106: "Arena Lúgubre", - 15107: "Cruce de Lorel", - 15108: "Mapa de desarrollo - Confín del Exilio - Torre", - 15109: "Sala de Sonido - Prueba de humo de audio", - 15110: "Puerta del Contendiente", - 15111: "Bazar Umbrío", - 15112: "Cuencas Altas", - 15113: "Transformatorio", - 15114: "Viñedos Sombrarboleda", - 15115: "Bel'ameth", - 15116: "Ciénaga Cantarina", - 15117: "Raíces Costeras", - 15118: "Ocaso Floreciente", - 15119: "Raíces Serpenteantes", - 15120: "Refugio de las Cascadas", - 15121: "Belanaar", - 15122: "Zarzal Veridiscente", - 15123: "Cuenca Espirala", - 15124: "Río de Lluvia Primaveral", - 15125: "Bosque Retorcido", - 15126: "Campos del Ensueño", - 15127: "Marisma Dichastival", - 15128: "Refugio de Arlithrien", - 15129: "Atalaya del Crepúsculo", - 15130: "Atalaya del Alba", - 15131: "Atalaya del Atardecer", - 15132: "Bancal de la Luna", - 15133: "Despertar la máquina", - 15134: "Madrigueras", - 15135: "Pluma Plateada", - 15136: "Cámara Afilada", - 15137: "Galería del Artesano", - 15138: "Mareas Cambiantes", - 15139: "Santuario de la Raíz", - 15140: "Alcoba de Riscopenumbra", - 15141: "Máquina del despertar", - 15142: "Cortafuegos de la Negación", - 15143: "Unidad de Procesamiento", - 15144: "Interfaz de Aniquilación", - 15145: "Santificación", - 15146: "Inframar", - 15147: "Tierras de Veneración", - 15148: "Reclamación de Velhan", - 15149: "Mereldar", - 15150: "Priorato de la Llama Sagrada", - 15151: "Archivo de Azeroth", - 15152: "Profundidades Angustiosas", - 15153: "Cloacas de Salien", - 15154: "Hebras Desgarradas", - 15155: "Mirador del Eco", - 15156: "Subdepósito", - 15157: "Patio del Hidromiel", - 15158: "Cueva Sombrafría", - 15166: "Locura de Isaec", - 15168: "Terraza del Botánico", - 15169: "Puesto de Energía de Vetasombría", - 15170: "Mina de Vetasombría", - 15171: "Muelle de Fabricación Forjahonda", - 15172: "Ascenso del Custodio del Consejo", - 15174: "Estuario Inquieto", - 15175: "Sumidero", - 15176: "Agarres Sanguinos", - 15177: "Dalaran", - 15178: "Plaza del Cruce", - 15179: "Caída de la Fortuna", - 15180: "11.0 warband camp", - 15181: "La Fisura", - 15182: "Loma Guardapétrea", - 15183: "Puerta del Juramento", - 15184: "Claustro de los Labrapiedras", - 15185: "Descanso de Thraegar", - 15326: "Hondonada de los Bramidos", - 15327: "Abismo de Tak-Rethan", - 15328: "Desembarco del Grajero", - 15329: "Cuartel de los Exploradores de las Profundidades", - 15330: "Fosa de Sacrificios Antigua", - 15335: "Lágrimas de Morgaen", - 15337: "Priorato de la Llama Sagrada", - 15338: "Los Grifos Llameantes", - 15339: "Los Destiladores", - 15340: "La Zumbería", - 15341: "Suite de la MIEL.E.O.", - 15342: "Sala del Despertar", - 15343: "KVigue Test", - 15345: "Las Cataratas Nieblabrillante", - 15346: "Portal Colgante", - 15353: "Túmulos del Ensueño", - 15354: "Puerta de Santificación", - 15356: "Cataratas Ancestrales", - 15357: "Cantera Velaescoria", - 15358: "Tamiz Discordante", - 15359: "Crisol del Hendido", - 15360: "Desembarco del Errante", - 15361: "Túneles Vetagema", - 15362: "Torre de Entrada de Nerub-ar", - 15363: "Gran Muralla", - 15364: "Foso Latente", - 15365: "Charco Coagulante", - 15366: "Tramo Ascendente", - 15367: "Bancal de Majestuosidad", - 15368: "Arena de Kili-zar", - 15369: "Extensión Oscilante", - 15370: "Nártex", - 15371: "Caverna Serpenteante", - 15372: "Redil de Incubación", - 15373: "Galería de las Telarañas", - 15374: "Sala de los Susurros", - 15375: "Corte de la Seda", - 15376: "Corona de las Sombras", - 15377: "Interfaz de Análisis", - 15378: "Baluarte de Autorización", - 15379: "Foro Procedimental", - 15380: "Máquina del Despertar", - 15381: "Archivo", - 15382: "Horizonte Cósmico", - 15383: "Tramo del Equinoccio", - 15384: "Poza Susurrante", - 15385: "Sombra del Amanecer", - 15386: "Escudo Sumergido", - 15387: "Campamento de la Banda Guerrera", - 15431: "Silithus", - 15432: "Estrado del Escarabajo", - 15433: "Muro del Escarabajo", - 15434: "Avanzada Crepúsculo", - 15435: "Colmen'Zora", - 15436: "Campamento Barbabronce", - 15437: "Colmen'Regal", - 15438: "Aldea del Viento del Sur", - 15439: "Huesos de Grakkarond", - 15440: "Pilar de la Ascensión", - 15441: "Campamento Crepuscular", - 15442: "Puesto Crepuscular", - 15443: "Fuerte Cenarion", - 15444: "Sosiego del Valor", - 15445: "Punta de Corzocelada", - 15446: "Campamento Crepuscular Devastado", - 15447: "Vega de Cristal", - 15448: "Colmen'Ashi", - 15450: "Aliento del Vigilante", - 15451: "Valle Indómito", - 15453: "Taberna Tocando Fondo", - 15460: "Terreno Santificado", - 15470: "Laboratorio de la Gran Ensambladora", - 15474: "Paso Andahondo", - 15509: "Confín Olvidado", - 15510: "Nido Bélico", - 15513: "Dimensión de Bolsillo", - 15514: "Gruta del Sedal", - 15517: "Boticario Real", - 15525: "Mare Magnum", - 15544: "Ascenso de Tenir", - 15545: "Mereldar", - 15546: "Tierras de Veneración", - 15547: "Abundancia de Beledar", - 15548: "Charco Hambriento", - 15549: "Campos de Primaverdía", - 15550: "Cenagal Estancado", - 15551: "Los Colmillos", - 15552: "Inframar", - 15667: "Profundidades", - 15780: "Forbidden Reach - (11.0.5 Version) - Dracthyr for All", - 15786: "Santificación", - 15913: "Profundidades de Roca Negra", -} - -def get_zone_name(zone_id): - """Devuelve el nombre de la zona basado en su ID.""" - return ZONE_NAMES.get(zone_id, "Zona Desconocida") diff --git a/manage.py b/manage.py deleted file mode 100644 index 3d96dc5..0000000 --- a/manage.py +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env python -"""Django's command-line utility for administrative tasks.""" -import os -import sys - - -def main(): - """Run administrative tasks.""" - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'novawow.settings') - try: - from django.core.management import execute_from_command_line - except ImportError as exc: - raise ImportError( - "Couldn't import Django. Are you sure it's installed and " - "available on your PYTHONPATH environment variable? Did you " - "forget to activate a virtual environment?" - ) from exc - execute_from_command_line(sys.argv) - - -if __name__ == '__main__': - main() diff --git a/novawow/__init__.py b/novawow/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/novawow/asgi.py b/novawow/asgi.py deleted file mode 100644 index 89e7d60..0000000 --- a/novawow/asgi.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -ASGI config for novawow project. - -It exposes the ASGI callable as a module-level variable named ``application``. - -For more information on this file, see -https://docs.djangoproject.com/en/5.1/howto/deployment/asgi/ -""" - -import os - -from django.core.asgi import get_asgi_application - -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'novawow.settings') - -application = get_asgi_application() diff --git a/novawow/settings.py b/novawow/settings.py deleted file mode 100644 index d6c48df..0000000 --- a/novawow/settings.py +++ /dev/null @@ -1,313 +0,0 @@ -""" -Django settings for novawow project. - -Generated by 'django-admin startproject' using Django 5.1.2. - -For more information on this file, see -https://docs.djangoproject.com/en/5.1/topics/settings/ - -For the full list of settings and their values, see -https://docs.djangoproject.com/en/5.1/ref/settings/ -""" - -from pathlib import Path -import os - -from dotenv import load_dotenv - -# Build paths inside the project like this: BASE_DIR / 'subdir'. -BASE_DIR = Path(__file__).resolve().parent.parent - -# Carga las variables de entorno desde un archivo .env (no versionado) -load_dotenv(BASE_DIR / '.env') - - -def _env_bool(name, default=False): - """Lee una variable de entorno booleana ('1', 'true', 'yes', 'on').""" - return os.getenv(name, str(default)).lower() in ('1', 'true', 'yes', 'on') - - -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/ - -# SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = os.getenv('DJANGO_SECRET_KEY', 'django-insecure-dev-key-change-me') - -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = _env_bool('DJANGO_DEBUG', False) - -if not DEBUG: - ALLOWED_HOSTS = ['nightspire.gg', 'www.nightspire.gg', '217.160.229.24', 'localhost', '127.0.0.1'] - SECURE_SSL_REDIRECT = False - SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') - SECURE_HSTS_SECONDS = 31536000 # Habilita HSTS por 1 año - SECURE_HSTS_INCLUDE_SUBDOMAINS = True - SECURE_HSTS_PRELOAD = True - SESSION_COOKIE_SECURE = True - CSRF_COOKIE_SECURE = True -else: - # Modo desarrollo, no se aplican configuraciones de seguridad - ALLOWED_HOSTS = ['localhost', '127.0.0.1', 'nightspire.gg', 'www.nightspire.gg'] - SECURE_SSL_REDIRECT = False - SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') - SECURE_HSTS_SECONDS = 0 - SECURE_HSTS_INCLUDE_SUBDOMAINS = False - SECURE_HSTS_PRELOAD = False - SESSION_COOKIE_SECURE = False - CSRF_COOKIE_SECURE = False - - -CSRF_TRUSTED_ORIGINS = [ - "https://www.nightspire.gg", - "https://nightspire.gg" -] - -# Application definition - -INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'home', - 'django_ckeditor_5', - 'django_extensions', - 'wotlk_db', - 'forum', - 'django_vite', -] - -# Cloudflare Turnstile (captcha). La clave de sitio es pública; la secreta va en .env. -TURNSTILE_SITE_KEY = os.getenv('TURNSTILE_SITE_KEY', '') -TURNSTILE_SECRET_KEY = os.getenv('TURNSTILE_SECRET_KEY', '') - -# Configuración para AC SOAP -AC_SOAP_URL = os.getenv('AC_SOAP_URL', 'http://127.0.0.1:2079') -AC_SOAP_USER = os.getenv('AC_SOAP_USER', 'AC_SOAP') -AC_SOAP_PASSWORD = os.getenv('AC_SOAP_PASSWORD', '') -AC_SOAP_URN = os.getenv('AC_SOAP_URN', 'urn:AC') - - -# Configuración de SumUp -SUMUP_CLIENT_ID = os.getenv('SUMUP_CLIENT_ID', '') -SUMUP_CLIENT_SECRET = os.getenv('SUMUP_CLIENT_SECRET', '') -SUMUP_MERCHANT_EMAIL = os.getenv('SUMUP_MERCHANT_EMAIL', '') -SUMUP_CURRENCY = os.getenv('SUMUP_CURRENCY', 'EUR') # Puedes cambiarlo si necesitas otra moneda - - -# Configuración del correo electrónico para SMTP de Gmail -EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' -EMAIL_HOST = os.getenv('EMAIL_HOST', 'smtp.gmail.com') -EMAIL_PORT = int(os.getenv('EMAIL_PORT', '587')) -EMAIL_USE_TLS = _env_bool('EMAIL_USE_TLS', True) -EMAIL_USE_SSL = _env_bool('EMAIL_USE_SSL', False) -EMAIL_HOST_USER = os.getenv('EMAIL_HOST_USER', '') -EMAIL_HOST_PASSWORD = os.getenv('EMAIL_HOST_PASSWORD', '') # App Password de Gmail -DEFAULT_FROM_EMAIL = EMAIL_HOST_USER - - -CKEDITOR_5_CONFIGS = { - 'default': { - 'toolbar': [ - 'heading', '|', 'bold', 'italic', 'underline', 'link', 'bulletedList', 'numberedList', - '|', 'alignment', 'outdent', 'indent', '|', 'blockQuote', 'codeBlock', - 'insertTable', 'mediaEmbed', '|', 'undo', 'redo' - ], - 'height': '400px', - 'width': '100%', - 'language': 'es', - 'table': { - 'contentToolbar': [ - 'tableColumn', 'tableRow', 'mergeTableCells' - ] - }, - 'mediaEmbed': { - 'previewsInData': True - }, - } -} - - -CSP_FRAME_SRC = [ - "https://www.youtube.com", - "https://www.youtube.com/embed/", -] -CSP_DEFAULT_SRC = ["'self'", "https://www.youtube.com"] - -X_FRAME_OPTIONS = 'SAMEORIGIN' -SECURE_CONTENT_TYPE_NOSNIFF = False - - - -MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'whitenoise.middleware.WhiteNoiseMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.locale.LocaleMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'home.middleware.MaintenanceMiddleware', -] - -ROOT_URLCONF = 'novawow.urls' - -URL_PRINCIPAL = 'http://127.0.0.1:8000' - -# Nombre del servidor -NOMBRE_SERVIDOR = "Nova WoW" - -SESSION_ENGINE = 'django.contrib.sessions.backends.db' -SESSION_COOKIE_NAME = 'novawow_session' -SESSION_COOKIE_HTTPONLY = True -SESSION_EXPIRE_AT_BROWSER_CLOSE = False -SESSION_COOKIE_AGE = 3600 # 1 hora - -# Año actual -from datetime import datetime -ANIO_ACTUAL = datetime.now().year - -TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [BASE_DIR / 'templates'], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - 'home.context_processors.url_principal', - 'home.context_processors.configuracion_global', - 'home.context_processors.get_server_info', - ], - }, - }, -] - -STRIPE_PUBLIC_KEY = os.getenv('STRIPE_PUBLIC_KEY', '') -STRIPE_SECRET_KEY = os.getenv('STRIPE_SECRET_KEY', '') -STRIPE_WEBHOOK_SECRET = os.getenv('STRIPE_WEBHOOK_SECRET', '') - - -WSGI_APPLICATION = 'novawow.wsgi.application' - - -# Database -# https://docs.djangoproject.com/en/5.1/ref/settings/#databases - -# Credenciales MySQL compartidas por todas las bases de datos -DB_USER = os.getenv('DB_USER', '') -DB_PASSWORD = os.getenv('DB_PASSWORD', '') -DB_HOST = os.getenv('DB_HOST', '127.0.0.1') -DB_PORT = os.getenv('DB_PORT', '3306') - -_ACORE_OPTIONS = {'init_command': "SET sql_mode='STRICT_TRANS_TABLES'"} - - -def _mysql_db(name, options=None): - conf = { - 'ENGINE': 'django.db.backends.mysql', - 'NAME': name, - 'USER': DB_USER, - 'PASSWORD': DB_PASSWORD, - 'HOST': DB_HOST, - 'PORT': DB_PORT, - } - if options: - conf['OPTIONS'] = options - return conf - - -DATABASES = { - 'default': _mysql_db(os.getenv('DB_NAME_DEFAULT', 'django_wow')), - 'acore_auth': _mysql_db(os.getenv('DB_NAME_AUTH', 'acore_auth'), _ACORE_OPTIONS), - 'acore_characters': _mysql_db(os.getenv('DB_NAME_CHARACTERS', 'acore_characters'), _ACORE_OPTIONS), - 'acore_world': _mysql_db(os.getenv('DB_NAME_WORLD', 'acore_world'), _ACORE_OPTIONS), - # Base de datos del portal web (foro, etc.) - 'acore_web': _mysql_db(os.getenv('DB_NAME_WEB', 'acore_web'), _ACORE_OPTIONS), -} - -# Nivel GM mínimo (acore_auth.account_access.gmlevel) para moderar el foro -FORUM_MOD_GMLEVEL = int(os.getenv('FORUM_MOD_GMLEVEL', '2')) -# Tamaños de página del foro -FORUM_TOPICS_PER_PAGE = int(os.getenv('FORUM_TOPICS_PER_PAGE', '20')) -FORUM_POSTS_PER_PAGE = int(os.getenv('FORUM_POSTS_PER_PAGE', '15')) - - -AC_LOGON = None - -# Password validation -# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators - -AUTH_PASSWORD_VALIDATORS = [ - { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', - }, -] - - -# Internationalization -# https://docs.djangoproject.com/en/5.1/topics/i18n/ - -LANGUAGES = [ - ('es', 'Español'), - # Agrega otros idiomas aquí si los necesitas - # ('en', 'English'), -] - -LANGUAGE_CODE = 'es' - -TIME_ZONE = 'UTC' - -USE_I18N = True - -USE_TZ = True - - -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/5.1/howto/static-files/ - -STATIC_URL = '/static/' -STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') - -# --- Integración con Vite (islas React/TSX) --- -# En producción (DEBUG=False) se sirve el bundle construido vía manifest; -# en desarrollo (DEBUG=True) se usa el dev server de Vite con HMR. -DJANGO_VITE = { - "default": { - "dev_mode": DEBUG, - "static_url_prefix": "dist", - "manifest_path": os.path.join(BASE_DIR, "static", "dist", ".vite", "manifest.json"), - } -} - -# Directorios adicionales donde buscar archivos estáticos -STATICFILES_DIRS = [ - os.path.join(BASE_DIR, 'static'), -] - -# WhiteNoise sirve los estáticos desde el propio proceso gunicorn (comprimidos), -# sin depender de que el reverse proxy tenga acceso de lectura a /root. -STORAGES = { - "default": {"BACKEND": "django.core.files.storage.FileSystemStorage"}, - "staticfiles": {"BACKEND": "whitenoise.storage.CompressedStaticFilesStorage"}, -} - -# Default primary key field type -# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field - -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' diff --git a/novawow/urls.py b/novawow/urls.py deleted file mode 100644 index 51742e5..0000000 --- a/novawow/urls.py +++ /dev/null @@ -1,24 +0,0 @@ -from django.conf import settings -from django.conf.urls.i18n import i18n_patterns -from django.urls import path, include -from django.contrib import admin -from .views import redirect_to_spanish # Importamos la vista de redirección -from django.conf.urls.static import static - -urlpatterns = [ - path('', redirect_to_spanish), # Redirige la raíz a `/es/` - path('i18n/', include('django.conf.urls.i18n')), # URL para cambiar el idioma - path('api/', include('home.api_urls')), # API JSON de las islas React (sin prefijo i18n) -] - -urlpatterns += i18n_patterns( - path('admin/', admin.site.urls), - path('', include('home.urls')), # Incluye las URLs de la app `home` - path('', include('forum.urls')), # Foro (rutas /forum/...) - path('wotlk/', include('wotlk_db.urls')), - path('ckeditor5/', include('django_ckeditor_5.urls')), -) - -# Esto debe estar al final para que Django sirva archivos estáticos durante el desarrollo -if settings.DEBUG: - urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) diff --git a/novawow/views.py b/novawow/views.py deleted file mode 100644 index 17dabdd..0000000 --- a/novawow/views.py +++ /dev/null @@ -1,4 +0,0 @@ -from django.shortcuts import redirect - -def redirect_to_spanish(request): - return redirect('/es/') diff --git a/novawow/wsgi.py b/novawow/wsgi.py deleted file mode 100644 index f6c89ca..0000000 --- a/novawow/wsgi.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -WSGI config for novawow project. - -It exposes the WSGI callable as a module-level variable named ``application``. - -For more information on this file, see -https://docs.djangoproject.com/en/5.1/howto/deployment/wsgi/ -""" - -import os - -from django.core.wsgi import get_wsgi_application - -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'novawow.settings') - -application = get_wsgi_application() diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 2b8537a..0000000 --- a/requirements.txt +++ /dev/null @@ -1,20 +0,0 @@ -asgiref==3.11.1 -certifi==2026.6.17 -charset-normalizer==3.4.9 -Django==6.0.7 -django-ckeditor-5==0.2.20 -django-extensions==4.1 -gmpy2==2.3.1 -idna==3.18 -mysqlclient==2.2.8 -pillow==12.3.0 -requests==2.34.2 -sqlparse==0.5.5 -stripe==15.3.0 -typing_extensions==4.16.0 -urllib3==2.7.0 -python-dotenv==1.2.2 -gunicorn==26.0.0 -nh3==0.3.6 -whitenoise -django-vite diff --git a/static/admin/css/autocomplete.css b/static/admin/css/autocomplete.css deleted file mode 100644 index 7478c2c..0000000 --- a/static/admin/css/autocomplete.css +++ /dev/null @@ -1,279 +0,0 @@ -select.admin-autocomplete { - width: 20em; -} - -.select2-container--admin-autocomplete.select2-container { - min-height: 30px; -} - -.select2-container--admin-autocomplete .select2-selection--single, -.select2-container--admin-autocomplete .select2-selection--multiple { - min-height: 30px; - padding: 0; -} - -.select2-container--admin-autocomplete.select2-container--focus .select2-selection, -.select2-container--admin-autocomplete.select2-container--open .select2-selection { - border-color: var(--body-quiet-color); - min-height: 30px; -} - -.select2-container--admin-autocomplete.select2-container--focus .select2-selection.select2-selection--single, -.select2-container--admin-autocomplete.select2-container--open .select2-selection.select2-selection--single { - padding: 0; -} - -.select2-container--admin-autocomplete.select2-container--focus .select2-selection.select2-selection--multiple, -.select2-container--admin-autocomplete.select2-container--open .select2-selection.select2-selection--multiple { - padding: 0; -} - -.select2-container--admin-autocomplete .select2-selection--single { - background-color: var(--body-bg); - border: 1px solid var(--border-color); - border-radius: 4px; -} - -.select2-container--admin-autocomplete .select2-selection--single .select2-selection__rendered { - color: var(--body-fg); - line-height: 30px; -} - -.select2-container--admin-autocomplete .select2-selection--single .select2-selection__clear { - cursor: pointer; - float: right; - font-weight: bold; -} - -.select2-container--admin-autocomplete .select2-selection--single .select2-selection__placeholder { - color: var(--body-quiet-color); -} - -.select2-container--admin-autocomplete .select2-selection--single .select2-selection__arrow { - height: 26px; - position: absolute; - top: 1px; - right: 1px; - width: 20px; -} - -.select2-container--admin-autocomplete .select2-selection--single .select2-selection__arrow b { - border-color: #888 transparent transparent transparent; - border-style: solid; - border-width: 5px 4px 0 4px; - height: 0; - left: 50%; - margin-left: -4px; - margin-top: -2px; - position: absolute; - top: 50%; - width: 0; -} - -.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--single .select2-selection__clear { - float: left; -} - -.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--single .select2-selection__arrow { - left: 1px; - right: auto; -} - -.select2-container--admin-autocomplete.select2-container--disabled .select2-selection--single { - background-color: var(--darkened-bg); - cursor: default; -} - -.select2-container--admin-autocomplete.select2-container--disabled .select2-selection--single .select2-selection__clear { - display: none; -} - -.select2-container--admin-autocomplete.select2-container--open .select2-selection--single .select2-selection__arrow b { - border-color: transparent transparent #888 transparent; - border-width: 0 4px 5px 4px; -} - -.select2-container--admin-autocomplete .select2-selection--multiple { - background-color: var(--body-bg); - border: 1px solid var(--border-color); - border-radius: 4px; - cursor: text; -} - -.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__rendered { - box-sizing: border-box; - list-style: none; - margin: 0; - padding: 0 10px 5px 5px; - width: 100%; - display: flex; - flex-wrap: wrap; -} - -.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__rendered li { - list-style: none; -} - -.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__placeholder { - color: var(--body-quiet-color); - margin-top: 5px; - float: left; -} - -.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__clear { - cursor: pointer; - float: right; - font-weight: bold; - margin: 5px; - position: absolute; - right: 0; -} - -.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__choice { - background-color: var(--darkened-bg); - border: 1px solid var(--border-color); - border-radius: 4px; - cursor: default; - float: left; - margin-right: 5px; - margin-top: 5px; - padding: 0 5px; -} - -.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__choice__remove { - color: var(--body-quiet-color); - cursor: pointer; - display: inline-block; - font-weight: bold; - margin-right: 2px; -} - -.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__choice__remove:hover { - color: var(--body-fg); -} - -.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__choice, .select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__placeholder, .select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-search--inline { - float: right; -} - -.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__choice { - margin-left: 5px; - margin-right: auto; -} - -.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove { - margin-left: 2px; - margin-right: auto; -} - -.select2-container--admin-autocomplete.select2-container--focus .select2-selection--multiple { - border: solid var(--body-quiet-color) 1px; - outline: 0; -} - -.select2-container--admin-autocomplete.select2-container--disabled .select2-selection--multiple { - background-color: var(--darkened-bg); - cursor: default; -} - -.select2-container--admin-autocomplete.select2-container--disabled .select2-selection__choice__remove { - display: none; -} - -.select2-container--admin-autocomplete.select2-container--open.select2-container--above .select2-selection--single, .select2-container--admin-autocomplete.select2-container--open.select2-container--above .select2-selection--multiple { - border-top-left-radius: 0; - border-top-right-radius: 0; -} - -.select2-container--admin-autocomplete.select2-container--open.select2-container--below .select2-selection--single, .select2-container--admin-autocomplete.select2-container--open.select2-container--below .select2-selection--multiple { - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; -} - -.select2-container--admin-autocomplete .select2-search--dropdown { - background: var(--darkened-bg); -} - -.select2-container--admin-autocomplete .select2-search--dropdown .select2-search__field { - background: var(--body-bg); - color: var(--body-fg); - border: 1px solid var(--border-color); - border-radius: 4px; -} - -.select2-container--admin-autocomplete .select2-search--inline .select2-search__field { - background: transparent; - color: var(--body-fg); - border: none; - outline: 0; - box-shadow: none; - -webkit-appearance: textfield; -} - -.select2-container--admin-autocomplete .select2-results > .select2-results__options { - max-height: 200px; - overflow-y: auto; - color: var(--body-fg); - background: var(--body-bg); -} - -.select2-container--admin-autocomplete .select2-results__option[role=group] { - padding: 0; -} - -.select2-container--admin-autocomplete .select2-results__option[aria-disabled=true] { - color: var(--body-quiet-color); -} - -.select2-container--admin-autocomplete .select2-results__option[aria-selected=true] { - background-color: var(--selected-bg); - color: var(--body-fg); -} - -.select2-container--admin-autocomplete .select2-results__option .select2-results__option { - padding-left: 1em; -} - -.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__group { - padding-left: 0; -} - -.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option { - margin-left: -1em; - padding-left: 2em; -} - -.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option { - margin-left: -2em; - padding-left: 3em; -} - -.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option { - margin-left: -3em; - padding-left: 4em; -} - -.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option { - margin-left: -4em; - padding-left: 5em; -} - -.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option { - margin-left: -5em; - padding-left: 6em; -} - -.select2-container--admin-autocomplete .select2-results__option--highlighted[aria-selected] { - background-color: var(--primary); - color: var(--primary-fg); -} - -.select2-container--admin-autocomplete .select2-results__group { - cursor: default; - display: block; - padding: 6px; -} - -.errors .select2-selection { - border: 1px solid var(--error-fg); -} diff --git a/static/admin/css/base.css b/static/admin/css/base.css deleted file mode 100644 index ac28326..0000000 --- a/static/admin/css/base.css +++ /dev/null @@ -1,1179 +0,0 @@ -/* - DJANGO Admin styles -*/ - -/* VARIABLE DEFINITIONS */ -html[data-theme="light"], -:root { - --primary: #79aec8; - --secondary: #417690; - --accent: #f5dd5d; - --primary-fg: #fff; - - --body-fg: #333; - --body-bg: #fff; - --body-quiet-color: #666; - --body-medium-color: #444; - --body-loud-color: #000; - - --header-color: #ffc; - --header-branding-color: var(--accent); - --header-bg: var(--secondary); - --header-link-color: var(--primary-fg); - - --breadcrumbs-fg: #c4dce8; - --breadcrumbs-link-fg: var(--body-bg); - --breadcrumbs-bg: #264b5d; - - --link-fg: #417893; - --link-hover-color: #036; - --link-selected-fg: var(--secondary); - - --hairline-color: #e8e8e8; - --border-color: #ccc; - - --error-fg: #ba2121; - - --message-success-bg: #dfd; - --message-warning-bg: #ffc; - --message-error-bg: #ffefef; - - --darkened-bg: #f8f8f8; /* A bit darker than --body-bg */ - --selected-bg: #e4e4e4; /* E.g. selected table cells */ - --selected-row: #ffc; - - --button-fg: #fff; - --button-bg: var(--secondary); - --button-hover-bg: #205067; - --default-button-bg: #205067; - --default-button-hover-bg: var(--secondary); - --close-button-bg: #747474; - --close-button-hover-bg: #333; - --delete-button-bg: #ba2121; - --delete-button-hover-bg: #a41515; - - --object-tools-fg: var(--button-fg); - --object-tools-bg: var(--close-button-bg); - --object-tools-hover-bg: var(--close-button-hover-bg); - - --font-family-primary: - "Segoe UI", - system-ui, - Roboto, - "Helvetica Neue", - Arial, - sans-serif, - "Apple Color Emoji", - "Segoe UI Emoji", - "Segoe UI Symbol", - "Noto Color Emoji"; - --font-family-monospace: - ui-monospace, - Menlo, - Monaco, - "Cascadia Mono", - "Segoe UI Mono", - "Roboto Mono", - "Oxygen Mono", - "Ubuntu Monospace", - "Source Code Pro", - "Fira Mono", - "Droid Sans Mono", - "Courier New", - monospace, - "Apple Color Emoji", - "Segoe UI Emoji", - "Segoe UI Symbol", - "Noto Color Emoji"; - - color-scheme: light; -} - -html, body { - height: 100%; -} - -body { - margin: 0; - padding: 0; - font-size: 0.875rem; - font-family: var(--font-family-primary); - color: var(--body-fg); - background: var(--body-bg); -} - -/* LINKS */ - -a:link, a:visited { - color: var(--link-fg); - text-decoration: none; - transition: color 0.15s, background 0.15s; -} - -a:focus, a:hover { - color: var(--link-hover-color); -} - -a:focus { - text-decoration: underline; -} - -a img { - border: none; -} - -a.section:link, a.section:visited { - color: var(--header-link-color); - text-decoration: none; -} - -a.section:focus, a.section:hover { - text-decoration: underline; -} - -/* GLOBAL DEFAULTS */ - -p, ol, ul, dl { - margin: .2em 0 .8em 0; -} - -p { - padding: 0; - line-height: 140%; -} - -h1,h2,h3,h4,h5 { - font-weight: bold; -} - -h1 { - margin: 0 0 20px; - font-weight: 300; - font-size: 1.25rem; -} - -h2 { - font-size: 1rem; - margin: 1em 0 .5em 0; -} - -h2.subhead { - font-weight: normal; - margin-top: 0; -} - -h3 { - font-size: 0.875rem; - margin: .8em 0 .3em 0; - color: var(--body-medium-color); - font-weight: bold; -} - -h4 { - font-size: 0.75rem; - margin: 1em 0 .8em 0; - padding-bottom: 3px; - color: var(--body-medium-color); -} - -h5 { - font-size: 0.625rem; - margin: 1.5em 0 .5em 0; - color: var(--body-quiet-color); - text-transform: uppercase; - letter-spacing: 1px; -} - -ul > li { - list-style-type: square; - padding: 1px 0; -} - -li ul { - margin-bottom: 0; -} - -li, dt, dd { - font-size: 0.8125rem; - line-height: 1.25rem; -} - -dt { - font-weight: bold; - margin-top: 4px; -} - -dd { - margin-left: 0; -} - -form { - margin: 0; - padding: 0; -} - -fieldset { - margin: 0; - min-width: 0; - padding: 0; - border: none; - border-top: 1px solid var(--hairline-color); -} - -details summary { - cursor: pointer; -} - -blockquote { - font-size: 0.6875rem; - color: #777; - margin-left: 2px; - padding-left: 10px; - border-left: 5px solid #ddd; -} - -code, pre { - font-family: var(--font-family-monospace); - color: var(--body-quiet-color); - font-size: 0.75rem; - overflow-x: auto; -} - -pre.literal-block { - margin: 10px; - background: var(--darkened-bg); - padding: 6px 8px; -} - -code strong { - color: #930; -} - -hr { - clear: both; - color: var(--hairline-color); - background-color: var(--hairline-color); - height: 1px; - border: none; - margin: 0; - padding: 0; - line-height: 1px; -} - -/* TEXT STYLES & MODIFIERS */ - -.small { - font-size: 0.6875rem; -} - -.mini { - font-size: 0.625rem; -} - -.help, p.help, form p.help, div.help, form div.help, div.help li { - font-size: 0.6875rem; - color: var(--body-quiet-color); -} - -div.help ul { - margin-bottom: 0; -} - -.help-tooltip { - cursor: help; -} - -p img, h1 img, h2 img, h3 img, h4 img, td img { - vertical-align: middle; -} - -.quiet, a.quiet:link, a.quiet:visited { - color: var(--body-quiet-color); - font-weight: normal; -} - -.clear { - clear: both; -} - -.nowrap { - white-space: nowrap; -} - -.hidden { - display: none !important; -} - -/* TABLES */ - -table { - border-collapse: collapse; - border-color: var(--border-color); -} - -td, th { - font-size: 0.8125rem; - line-height: 1rem; - border-bottom: 1px solid var(--hairline-color); - vertical-align: top; - padding: 8px; -} - -th { - font-weight: 500; - text-align: left; -} - -thead th, -tfoot td { - color: var(--body-quiet-color); - padding: 5px 10px; - font-size: 0.6875rem; - background: var(--body-bg); - border: none; - border-top: 1px solid var(--hairline-color); - border-bottom: 1px solid var(--hairline-color); -} - -tfoot td { - border-bottom: none; - border-top: 1px solid var(--hairline-color); -} - -thead th.required { - font-weight: bold; -} - -tr.alt { - background: var(--darkened-bg); -} - -tr:nth-child(odd), .row-form-errors { - background: var(--body-bg); -} - -tr:nth-child(even), -tr:nth-child(even) .errorlist, -tr:nth-child(odd) + .row-form-errors, -tr:nth-child(odd) + .row-form-errors .errorlist { - background: var(--darkened-bg); -} - -/* SORTABLE TABLES */ - -thead th { - padding: 5px 10px; - line-height: normal; - text-transform: uppercase; - background: var(--darkened-bg); -} - -thead th a:link, thead th a:visited { - color: var(--body-quiet-color); -} - -thead th.sorted { - background: var(--selected-bg); -} - -thead th.sorted .text { - padding-right: 42px; -} - -table thead th .text span { - padding: 8px 10px; - display: block; -} - -table thead th .text a { - display: block; - cursor: pointer; - padding: 8px 10px; -} - -table thead th .text a:focus, table thead th .text a:hover { - background: var(--selected-bg); -} - -thead th.sorted a.sortremove { - visibility: hidden; -} - -table thead th.sorted:hover a.sortremove { - visibility: visible; -} - -table thead th.sorted .sortoptions { - display: block; - padding: 9px 5px 0 5px; - float: right; - text-align: right; -} - -table thead th.sorted .sortpriority { - font-size: .8em; - min-width: 12px; - text-align: center; - vertical-align: 3px; - margin-left: 2px; - margin-right: 2px; -} - -table thead th.sorted .sortoptions a { - position: relative; - width: 14px; - height: 14px; - display: inline-block; - background: url(../img/sorting-icons.svg) 0 0 no-repeat; - background-size: 14px auto; -} - -table thead th.sorted .sortoptions a.sortremove { - background-position: 0 0; -} - -table thead th.sorted .sortoptions a.sortremove:after { - content: '\\'; - position: absolute; - top: -6px; - left: 3px; - font-weight: 200; - font-size: 1.125rem; - color: var(--body-quiet-color); -} - -table thead th.sorted .sortoptions a.sortremove:focus:after, -table thead th.sorted .sortoptions a.sortremove:hover:after { - color: var(--link-fg); -} - -table thead th.sorted .sortoptions a.sortremove:focus, -table thead th.sorted .sortoptions a.sortremove:hover { - background-position: 0 -14px; -} - -table thead th.sorted .sortoptions a.ascending { - background-position: 0 -28px; -} - -table thead th.sorted .sortoptions a.ascending:focus, -table thead th.sorted .sortoptions a.ascending:hover { - background-position: 0 -42px; -} - -table thead th.sorted .sortoptions a.descending { - top: 1px; - background-position: 0 -56px; -} - -table thead th.sorted .sortoptions a.descending:focus, -table thead th.sorted .sortoptions a.descending:hover { - background-position: 0 -70px; -} - -/* FORM DEFAULTS */ - -input, textarea, select, .form-row p, form .button { - margin: 2px 0; - padding: 2px 3px; - vertical-align: middle; - font-family: var(--font-family-primary); - font-weight: normal; - font-size: 0.8125rem; -} -.form-row div.help { - padding: 2px 3px; -} - -textarea { - vertical-align: top; -} - -/* -Minifiers remove the default (text) "type" attribute from "input" HTML tags. -Add input:not([type]) to make the CSS stylesheet work the same. -*/ -input:not([type]), input[type=text], input[type=password], input[type=email], -input[type=url], input[type=number], input[type=tel], textarea, select, -.vTextField { - border: 1px solid var(--border-color); - border-radius: 4px; - padding: 5px 6px; - margin-top: 0; - color: var(--body-fg); - background-color: var(--body-bg); -} - -/* -Minifiers remove the default (text) "type" attribute from "input" HTML tags. -Add input:not([type]) to make the CSS stylesheet work the same. -*/ -input:not([type]):focus, input[type=text]:focus, input[type=password]:focus, -input[type=email]:focus, input[type=url]:focus, input[type=number]:focus, -input[type=tel]:focus, textarea:focus, select:focus, .vTextField:focus { - border-color: var(--body-quiet-color); -} - -select { - height: 1.875rem; -} - -select[multiple] { - /* Allow HTML size attribute to override the height in the rule above. */ - height: auto; - min-height: 150px; -} - -/* FORM BUTTONS */ - -.button, input[type=submit], input[type=button], .submit-row input, a.button { - background: var(--button-bg); - padding: 10px 15px; - border: none; - border-radius: 4px; - color: var(--button-fg); - cursor: pointer; - transition: background 0.15s; -} - -a.button { - padding: 4px 5px; -} - -.button:active, input[type=submit]:active, input[type=button]:active, -.button:focus, input[type=submit]:focus, input[type=button]:focus, -.button:hover, input[type=submit]:hover, input[type=button]:hover { - background: var(--button-hover-bg); -} - -.button[disabled], input[type=submit][disabled], input[type=button][disabled] { - opacity: 0.4; -} - -.button.default, input[type=submit].default, .submit-row input.default { - border: none; - font-weight: 400; - background: var(--default-button-bg); -} - -.button.default:active, input[type=submit].default:active, -.button.default:focus, input[type=submit].default:focus, -.button.default:hover, input[type=submit].default:hover { - background: var(--default-button-hover-bg); -} - -.button[disabled].default, -input[type=submit][disabled].default, -input[type=button][disabled].default { - opacity: 0.4; -} - - -/* MODULES */ - -.module { - border: none; - margin-bottom: 30px; - background: var(--body-bg); -} - -.module p, .module ul, .module h3, .module h4, .module dl, .module pre { - padding-left: 10px; - padding-right: 10px; -} - -.module blockquote { - margin-left: 12px; -} - -.module ul, .module ol { - margin-left: 1.5em; -} - -.module h3 { - margin-top: .6em; -} - -.module h2, .module caption, .inline-group h2 { - margin: 0; - padding: 8px; - font-weight: 400; - font-size: 0.8125rem; - text-align: left; - background: var(--header-bg); - color: var(--header-link-color); -} - -.module caption, -.inline-group h2 { - font-size: 0.75rem; - letter-spacing: 0.5px; - text-transform: uppercase; -} - -.module table { - border-collapse: collapse; -} - -/* MESSAGES & ERRORS */ - -ul.messagelist { - padding: 0; - margin: 0; -} - -ul.messagelist li { - display: block; - font-weight: 400; - font-size: 0.8125rem; - padding: 10px 10px 10px 65px; - margin: 0 0 10px 0; - background: var(--message-success-bg) url(../img/icon-yes.svg) 40px 12px no-repeat; - background-size: 16px auto; - color: var(--body-fg); - word-break: break-word; -} - -ul.messagelist li.warning { - background: var(--message-warning-bg) url(../img/icon-alert.svg) 40px 14px no-repeat; - background-size: 14px auto; -} - -ul.messagelist li.error { - background: var(--message-error-bg) url(../img/icon-no.svg) 40px 12px no-repeat; - background-size: 16px auto; -} - -.errornote { - font-size: 0.875rem; - font-weight: 700; - display: block; - padding: 10px 12px; - margin: 0 0 10px 0; - color: var(--error-fg); - border: 1px solid var(--error-fg); - border-radius: 4px; - background-color: var(--body-bg); - background-position: 5px 12px; - overflow-wrap: break-word; -} - -ul.errorlist { - margin: 0 0 4px; - padding: 0; - color: var(--error-fg); - background: var(--body-bg); -} - -ul.errorlist li { - font-size: 0.8125rem; - display: block; - margin-bottom: 4px; - overflow-wrap: break-word; -} - -ul.errorlist li:first-child { - margin-top: 0; -} - -ul.errorlist li a { - color: inherit; - text-decoration: underline; -} - -td ul.errorlist { - margin: 0; - padding: 0; -} - -td ul.errorlist li { - margin: 0; -} - -.form-row.errors { - margin: 0; - border: none; - border-bottom: 1px solid var(--hairline-color); - background: none; -} - -.form-row.errors ul.errorlist li { - padding-left: 0; -} - -.errors input, .errors select, .errors textarea, -td ul.errorlist + input, td ul.errorlist + select, td ul.errorlist + textarea { - border: 1px solid var(--error-fg); -} - -.description { - font-size: 0.75rem; - padding: 5px 0 0 12px; -} - -/* BREADCRUMBS */ - -div.breadcrumbs { - background: var(--breadcrumbs-bg); - padding: 10px 40px; - border: none; - color: var(--breadcrumbs-fg); - text-align: left; -} - -div.breadcrumbs a { - color: var(--breadcrumbs-link-fg); -} - -div.breadcrumbs a:focus, div.breadcrumbs a:hover { - color: var(--breadcrumbs-fg); -} - -/* ACTION ICONS */ - -.viewlink, .inlineviewlink { - padding-left: 16px; - background: url(../img/icon-viewlink.svg) 0 1px no-repeat; -} - -.hidelink { - padding-left: 16px; - background: url(../img/icon-hidelink.svg) 0 1px no-repeat; -} - -.addlink { - padding-left: 16px; - background: url(../img/icon-addlink.svg) 0 1px no-repeat; -} - -.changelink, .inlinechangelink { - padding-left: 16px; - background: url(../img/icon-changelink.svg) 0 1px no-repeat; -} - -.deletelink { - padding-left: 16px; - background: url(../img/icon-deletelink.svg) 0 1px no-repeat; -} - -a.deletelink:link, a.deletelink:visited { - color: #CC3434; /* XXX Probably unused? */ -} - -a.deletelink:focus, a.deletelink:hover { - color: #993333; /* XXX Probably unused? */ - text-decoration: none; -} - -/* OBJECT TOOLS */ - -.object-tools { - font-size: 0.625rem; - font-weight: bold; - padding-left: 0; - float: right; - position: relative; - margin-top: -48px; -} - -.object-tools li { - display: block; - float: left; - margin-left: 5px; - height: 1rem; -} - -.object-tools a { - border-radius: 15px; -} - -.object-tools a:link, .object-tools a:visited { - display: block; - float: left; - padding: 3px 12px; - background: var(--object-tools-bg); - color: var(--object-tools-fg); - font-weight: 400; - font-size: 0.6875rem; - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.object-tools a:focus, .object-tools a:hover { - background-color: var(--object-tools-hover-bg); -} - -.object-tools a:focus{ - text-decoration: none; -} - -.object-tools a.viewsitelink, .object-tools a.addlink { - background-repeat: no-repeat; - background-position: right 7px center; - padding-right: 26px; -} - -.object-tools a.viewsitelink { - background-image: url(../img/tooltag-arrowright.svg); -} - -.object-tools a.addlink { - background-image: url(../img/tooltag-add.svg); -} - -/* OBJECT HISTORY */ - -#change-history table { - width: 100%; -} - -#change-history table tbody th { - width: 16em; -} - -#change-history .paginator { - color: var(--body-quiet-color); - border-bottom: 1px solid var(--hairline-color); - background: var(--body-bg); - overflow: hidden; -} - -/* PAGE STRUCTURE */ - -#container { - position: relative; - width: 100%; - min-width: 980px; - padding: 0; - display: flex; - flex-direction: column; - height: 100%; -} - -#container > .main { - display: flex; - flex: 1 0 auto; -} - -.main > .content { - flex: 1 0; - max-width: 100%; -} - -.skip-to-content-link { - position: absolute; - top: -999px; - margin: 5px; - padding: 5px; - background: var(--body-bg); - z-index: 1; -} - -.skip-to-content-link:focus { - left: 0px; - top: 0px; -} - -#content { - padding: 20px 40px; -} - -.dashboard #content { - width: 600px; -} - -#content-main { - float: left; - width: 100%; -} - -#content-related { - float: right; - width: 260px; - position: relative; - margin-right: -300px; -} - -@media (forced-colors: active) { - #content-related { - border: 1px solid; - } -} - -/* COLUMN TYPES */ - -.colMS { - margin-right: 300px; -} - -.colSM { - margin-left: 300px; -} - -.colSM #content-related { - float: left; - margin-right: 0; - margin-left: -300px; -} - -.colSM #content-main { - float: right; -} - -.popup .colM { - width: auto; -} - -/* HEADER */ - -#header { - width: auto; - height: auto; - display: flex; - justify-content: space-between; - align-items: center; - padding: 10px 40px; - background: var(--header-bg); - color: var(--header-color); -} - -#header a:link, #header a:visited, #logout-form button { - color: var(--header-link-color); -} - -#header a:focus , #header a:hover { - text-decoration: underline; -} - -@media (forced-colors: active) { - #header { - border-bottom: 1px solid; - } -} - -#branding { - display: flex; -} - -#site-name { - padding: 0; - margin: 0; - margin-inline-end: 20px; - font-weight: 300; - font-size: 1.5rem; - color: var(--header-branding-color); -} - -#site-name a:link, #site-name a:visited { - color: var(--accent); -} - -#branding h2 { - padding: 0 10px; - font-size: 0.875rem; - margin: -8px 0 8px 0; - font-weight: normal; - color: var(--header-color); -} - -#branding a:hover { - text-decoration: none; -} - -#logout-form { - display: inline; -} - -#logout-form button { - background: none; - border: 0; - cursor: pointer; - font-family: var(--font-family-primary); -} - -#user-tools { - float: right; - margin: 0 0 0 20px; - text-align: right; -} - -#user-tools, #logout-form button{ - padding: 0; - font-weight: 300; - font-size: 0.6875rem; - letter-spacing: 0.5px; - text-transform: uppercase; -} - -#user-tools a, #logout-form button { - border-bottom: 1px solid rgba(255, 255, 255, 0.25); -} - -#user-tools a:focus, #user-tools a:hover, -#logout-form button:active, #logout-form button:hover { - text-decoration: none; - border-bottom: 0; -} - -#logout-form button:active, #logout-form button:hover { - margin-bottom: 1px; -} - -/* SIDEBAR */ - -#content-related { - background: var(--darkened-bg); -} - -#content-related .module { - background: none; -} - -#content-related h3 { - color: var(--body-quiet-color); - padding: 0 16px; - margin: 0 0 16px; -} - -#content-related h4 { - font-size: 0.8125rem; -} - -#content-related p { - padding-left: 16px; - padding-right: 16px; -} - -#content-related .actionlist { - padding: 0; - margin: 16px; -} - -#content-related .actionlist li { - line-height: 1.2; - margin-bottom: 10px; - padding-left: 18px; -} - -#content-related .module h2 { - background: none; - padding: 16px; - margin-bottom: 16px; - border-bottom: 1px solid var(--hairline-color); - font-size: 1.125rem; - color: var(--body-fg); -} - -.delete-confirmation form input[type="submit"] { - background: var(--delete-button-bg); - border-radius: 4px; - padding: 10px 15px; - color: var(--button-fg); -} - -.delete-confirmation form input[type="submit"]:active, -.delete-confirmation form input[type="submit"]:focus, -.delete-confirmation form input[type="submit"]:hover { - background: var(--delete-button-hover-bg); -} - -.delete-confirmation form .cancel-link { - display: inline-block; - vertical-align: middle; - height: 0.9375rem; - line-height: 0.9375rem; - border-radius: 4px; - padding: 10px 15px; - color: var(--button-fg); - background: var(--close-button-bg); - margin: 0 0 0 10px; -} - -.delete-confirmation form .cancel-link:active, -.delete-confirmation form .cancel-link:focus, -.delete-confirmation form .cancel-link:hover { - background: var(--close-button-hover-bg); -} - -/* POPUP */ -.popup #content { - padding: 20px; -} - -.popup #container { - min-width: 0; -} - -.popup #header { - padding: 10px 20px; -} - -/* PAGINATOR */ - -.paginator { - display: flex; - align-items: center; - gap: 4px; - font-size: 0.8125rem; - padding-top: 10px; - padding-bottom: 10px; - line-height: 22px; - margin: 0; - border-top: 1px solid var(--hairline-color); - width: 100%; -} - -.paginator a:link, .paginator a:visited { - padding: 2px 6px; - background: var(--button-bg); - text-decoration: none; - color: var(--button-fg); -} - -.paginator a.showall { - border: none; - background: none; - color: var(--link-fg); -} - -.paginator a.showall:focus, .paginator a.showall:hover { - background: none; - color: var(--link-hover-color); -} - -.paginator .end { - margin-right: 6px; -} - -.paginator .this-page { - padding: 2px 6px; - font-weight: bold; - font-size: 0.8125rem; - vertical-align: top; -} - -.paginator a:focus, .paginator a:hover { - color: white; - background: var(--link-hover-color); -} - -.paginator input { - margin-left: auto; -} - -.base-svgs { - display: none; -} - -.visually-hidden { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - overflow: hidden; - clip: rect(0,0,0,0); - white-space: nowrap; - border: 0; - color: var(--body-fg); - background-color: var(--body-bg); -} diff --git a/static/admin/css/changelists.css b/static/admin/css/changelists.css deleted file mode 100644 index 005b776..0000000 --- a/static/admin/css/changelists.css +++ /dev/null @@ -1,343 +0,0 @@ -/* CHANGELISTS */ - -#changelist { - display: flex; - align-items: flex-start; - justify-content: space-between; -} - -#changelist .changelist-form-container { - flex: 1 1 auto; - min-width: 0; -} - -#changelist table { - width: 100%; -} - -.change-list .hiddenfields { display:none; } - -.change-list .filtered table { - border-right: none; -} - -.change-list .filtered { - min-height: 400px; -} - -.change-list .filtered .results, .change-list .filtered .paginator, -.filtered #toolbar, .filtered div.xfull { - width: auto; -} - -.change-list .filtered table tbody th { - padding-right: 1em; -} - -#changelist-form .results { - overflow-x: auto; - width: 100%; -} - -#changelist .toplinks { - border-bottom: 1px solid var(--hairline-color); -} - -#changelist .paginator { - color: var(--body-quiet-color); - border-bottom: 1px solid var(--hairline-color); - background: var(--body-bg); - overflow: hidden; -} - -/* CHANGELIST TABLES */ - -#changelist table thead th { - padding: 0; - white-space: nowrap; - vertical-align: middle; -} - -#changelist table thead th.action-checkbox-column { - width: 1.5em; - text-align: center; -} - -#changelist table tbody td.action-checkbox { - text-align: center; -} - -#changelist table tfoot { - color: var(--body-quiet-color); -} - -/* TOOLBAR */ - -#toolbar { - padding: 8px 10px; - margin-bottom: 15px; - border-top: 1px solid var(--hairline-color); - border-bottom: 1px solid var(--hairline-color); - background: var(--darkened-bg); - color: var(--body-quiet-color); -} - -#toolbar form input { - border-radius: 4px; - font-size: 0.875rem; - padding: 5px; - color: var(--body-fg); -} - -#toolbar #searchbar { - height: 1.1875rem; - border: 1px solid var(--border-color); - padding: 2px 5px; - margin: 0; - vertical-align: top; - font-size: 0.8125rem; - max-width: 100%; -} - -#toolbar #searchbar:focus { - border-color: var(--body-quiet-color); -} - -#toolbar form input[type="submit"] { - border: 1px solid var(--border-color); - font-size: 0.8125rem; - padding: 4px 8px; - margin: 0; - vertical-align: middle; - background: var(--body-bg); - box-shadow: 0 -15px 20px -10px rgba(0, 0, 0, 0.15) inset; - cursor: pointer; - color: var(--body-fg); -} - -#toolbar form input[type="submit"]:focus, -#toolbar form input[type="submit"]:hover { - border-color: var(--body-quiet-color); -} - -#changelist-search img { - vertical-align: middle; - margin-right: 4px; -} - -#changelist-search .help { - word-break: break-word; -} - -/* FILTER COLUMN */ - -#changelist-filter { - flex: 0 0 240px; - order: 1; - background: var(--darkened-bg); - border-left: none; - margin: 0 0 0 30px; -} - -@media (forced-colors: active) { - #changelist-filter { - border: 1px solid; - } -} - -#changelist-filter h2 { - font-size: 0.875rem; - text-transform: uppercase; - letter-spacing: 0.5px; - padding: 5px 15px; - margin-bottom: 12px; - border-bottom: none; -} - -#changelist-filter h3, -#changelist-filter details summary { - font-weight: 400; - padding: 0 15px; - margin-bottom: 10px; -} - -#changelist-filter details summary > * { - display: inline; -} - -#changelist-filter details > summary { - list-style-type: none; -} - -#changelist-filter details > summary::-webkit-details-marker { - display: none; -} - -#changelist-filter details > summary::before { - content: '→'; - font-weight: bold; - color: var(--link-hover-color); -} - -#changelist-filter details[open] > summary::before { - content: '↓'; -} - -#changelist-filter ul { - margin: 5px 0; - padding: 0 15px 15px; - border-bottom: 1px solid var(--hairline-color); -} - -#changelist-filter ul:last-child { - border-bottom: none; -} - -#changelist-filter li { - list-style-type: none; - margin-left: 0; - padding-left: 0; -} - -#changelist-filter a { - display: block; - color: var(--body-quiet-color); - word-break: break-word; -} - -#changelist-filter li.selected { - border-left: 5px solid var(--hairline-color); - padding-left: 10px; - margin-left: -15px; -} - -#changelist-filter li.selected a { - color: var(--link-selected-fg); -} - -#changelist-filter a:focus, #changelist-filter a:hover, -#changelist-filter li.selected a:focus, -#changelist-filter li.selected a:hover { - color: var(--link-hover-color); -} - -#changelist-filter #changelist-filter-extra-actions { - font-size: 0.8125rem; - margin-bottom: 10px; - border-bottom: 1px solid var(--hairline-color); -} - -/* DATE DRILLDOWN */ - -.change-list .toplinks { - display: flex; - padding-bottom: 5px; - flex-wrap: wrap; - gap: 3px 17px; - font-weight: bold; -} - -.change-list .toplinks a { - font-size: 0.8125rem; -} - -.change-list .toplinks .date-back { - color: var(--body-quiet-color); -} - -.change-list .toplinks .date-back:focus, -.change-list .toplinks .date-back:hover { - color: var(--link-hover-color); -} - -/* ACTIONS */ - -.filtered .actions { - border-right: none; -} - -#changelist table input { - margin: 0; - vertical-align: baseline; -} - -/* Once the :has() pseudo-class is supported by all browsers, the tr.selected - selector and the JS adding the class can be removed. */ -#changelist tbody tr.selected { - background-color: var(--selected-row); -} - -#changelist tbody tr:has(.action-select:checked) { - background-color: var(--selected-row); -} - -@media (forced-colors: active) { - #changelist tbody tr.selected { - background-color: SelectedItem; - } - #changelist tbody tr:has(.action-select:checked) { - background-color: SelectedItem; - } -} - -#changelist .actions { - padding: 10px; - background: var(--body-bg); - border-top: none; - border-bottom: none; - line-height: 1.5rem; - color: var(--body-quiet-color); - width: 100%; -} - -#changelist .actions span.all, -#changelist .actions span.action-counter, -#changelist .actions span.clear, -#changelist .actions span.question { - font-size: 0.8125rem; - margin: 0 0.5em; -} - -#changelist .actions:last-child { - border-bottom: none; -} - -#changelist .actions select { - vertical-align: top; - height: 1.5rem; - color: var(--body-fg); - border: 1px solid var(--border-color); - border-radius: 4px; - font-size: 0.875rem; - padding: 0 0 0 4px; - margin: 0; - margin-left: 10px; -} - -#changelist .actions select:focus { - border-color: var(--body-quiet-color); -} - -#changelist .actions label { - display: inline-block; - vertical-align: middle; - font-size: 0.8125rem; -} - -#changelist .actions .button { - font-size: 0.8125rem; - border: 1px solid var(--border-color); - border-radius: 4px; - background: var(--body-bg); - box-shadow: 0 -15px 20px -10px rgba(0, 0, 0, 0.15) inset; - cursor: pointer; - height: 1.5rem; - line-height: 1; - padding: 4px 8px; - margin: 0; - color: var(--body-fg); -} - -#changelist .actions .button:focus, #changelist .actions .button:hover { - border-color: var(--body-quiet-color); -} diff --git a/static/admin/css/dark_mode.css b/static/admin/css/dark_mode.css deleted file mode 100644 index 7e12a81..0000000 --- a/static/admin/css/dark_mode.css +++ /dev/null @@ -1,130 +0,0 @@ -@media (prefers-color-scheme: dark) { - :root { - --primary: #264b5d; - --primary-fg: #f7f7f7; - - --body-fg: #eeeeee; - --body-bg: #121212; - --body-quiet-color: #d0d0d0; - --body-medium-color: #e0e0e0; - --body-loud-color: #ffffff; - - --breadcrumbs-link-fg: #e0e0e0; - --breadcrumbs-bg: var(--primary); - - --link-fg: #81d4fa; - --link-hover-color: #4ac1f7; - --link-selected-fg: #6f94c6; - - --hairline-color: #272727; - --border-color: #353535; - - --error-fg: #e35f5f; - --message-success-bg: #006b1b; - --message-warning-bg: #583305; - --message-error-bg: #570808; - - --darkened-bg: #212121; - --selected-bg: #1b1b1b; - --selected-row: #00363a; - - --close-button-bg: #333333; - --close-button-hover-bg: #666666; - - color-scheme: dark; - } - } - - -html[data-theme="dark"] { - --primary: #264b5d; - --primary-fg: #f7f7f7; - - --body-fg: #eeeeee; - --body-bg: #121212; - --body-quiet-color: #d0d0d0; - --body-medium-color: #e0e0e0; - --body-loud-color: #ffffff; - - --breadcrumbs-link-fg: #e0e0e0; - --breadcrumbs-bg: var(--primary); - - --link-fg: #81d4fa; - --link-hover-color: #4ac1f7; - --link-selected-fg: #6f94c6; - - --hairline-color: #272727; - --border-color: #353535; - - --error-fg: #e35f5f; - --message-success-bg: #006b1b; - --message-warning-bg: #583305; - --message-error-bg: #570808; - - --darkened-bg: #212121; - --selected-bg: #1b1b1b; - --selected-row: #00363a; - - --close-button-bg: #333333; - --close-button-hover-bg: #666666; - - color-scheme: dark; -} - -/* THEME SWITCH */ -.theme-toggle { - cursor: pointer; - border: none; - padding: 0; - background: transparent; - vertical-align: middle; - margin-inline-start: 5px; - margin-top: -1px; -} - -.theme-toggle svg { - vertical-align: middle; - height: 1rem; - width: 1rem; - display: none; -} - -/* -Fully hide screen reader text so we only show the one matching the current -theme. -*/ -.theme-toggle .visually-hidden { - display: none; -} - -html[data-theme="auto"] .theme-toggle .theme-label-when-auto { - display: block; -} - -html[data-theme="dark"] .theme-toggle .theme-label-when-dark { - display: block; -} - -html[data-theme="light"] .theme-toggle .theme-label-when-light { - display: block; -} - -/* ICONS */ -.theme-toggle svg.theme-icon-when-auto, -.theme-toggle svg.theme-icon-when-dark, -.theme-toggle svg.theme-icon-when-light { - fill: var(--header-link-color); - color: var(--header-bg); -} - -html[data-theme="auto"] .theme-toggle svg.theme-icon-when-auto { - display: block; -} - -html[data-theme="dark"] .theme-toggle svg.theme-icon-when-dark { - display: block; -} - -html[data-theme="light"] .theme-toggle svg.theme-icon-when-light { - display: block; -} diff --git a/static/admin/css/dashboard.css b/static/admin/css/dashboard.css deleted file mode 100644 index 242b81a..0000000 --- a/static/admin/css/dashboard.css +++ /dev/null @@ -1,29 +0,0 @@ -/* DASHBOARD */ -.dashboard td, .dashboard th { - word-break: break-word; -} - -.dashboard .module table th { - width: 100%; -} - -.dashboard .module table td { - white-space: nowrap; -} - -.dashboard .module table td a { - display: block; - padding-right: .6em; -} - -/* RECENT ACTIONS MODULE */ - -.module ul.actionlist { - margin-left: 0; -} - -ul.actionlist li { - list-style-type: none; - overflow: hidden; - text-overflow: ellipsis; -} diff --git a/static/admin/css/forms.css b/static/admin/css/forms.css deleted file mode 100644 index 4f49b61..0000000 --- a/static/admin/css/forms.css +++ /dev/null @@ -1,512 +0,0 @@ -@import url('widgets.css'); - -/* FORM ROWS */ - -.form-row { - overflow: hidden; - padding: 10px; - font-size: 0.8125rem; - border-bottom: 1px solid var(--hairline-color); -} - -.form-row img, .form-row input { - vertical-align: middle; -} - -.form-row label input[type="checkbox"] { - margin-top: 0; - vertical-align: 0; -} - -form .form-row p { - padding-left: 0; -} - -.flex-container { - display: flex; -} - -.form-multiline { - flex-wrap: wrap; -} - -.form-multiline > div { - padding-bottom: 10px; -} - -/* FORM LABELS */ - -label { - font-weight: normal; - color: var(--body-quiet-color); - font-size: 0.8125rem; -} - -.required label, label.required { - font-weight: bold; -} - -/* RADIO BUTTONS */ - -form div.radiolist div { - padding-right: 7px; -} - -form div.radiolist.inline div { - display: inline-block; -} - -form div.radiolist label { - width: auto; -} - -form div.radiolist input[type="radio"] { - margin: -2px 4px 0 0; - padding: 0; -} - -form ul.inline { - margin-left: 0; - padding: 0; -} - -form ul.inline li { - float: left; - padding-right: 7px; -} - -/* FIELDSETS */ - -fieldset .fieldset-heading, -fieldset .inline-heading, -:not(.inline-related) .collapse summary { - border: 1px solid var(--header-bg); - margin: 0; - padding: 8px; - font-weight: 400; - font-size: 0.8125rem; - background: var(--header-bg); - color: var(--header-link-color); -} - -/* ALIGNED FIELDSETS */ - -.aligned label { - display: block; - padding: 4px 10px 0 0; - min-width: 160px; - width: 160px; - word-wrap: break-word; -} - -.aligned label:not(.vCheckboxLabel):after { - content: ''; - display: inline-block; - vertical-align: middle; -} - -.aligned label + p, .aligned .checkbox-row + div.help, .aligned label + div.readonly { - padding: 6px 0; - margin-top: 0; - margin-bottom: 0; - margin-left: 0; - overflow-wrap: break-word; -} - -.aligned ul label { - display: inline; - float: none; - width: auto; -} - -.aligned .form-row input { - margin-bottom: 0; -} - -.colMS .aligned .vLargeTextField, .colMS .aligned .vXMLLargeTextField { - width: 350px; -} - -form .aligned ul { - margin-left: 160px; - padding-left: 10px; -} - -form .aligned div.radiolist { - display: inline-block; - margin: 0; - padding: 0; -} - -form .aligned p.help, -form .aligned div.help { - margin-top: 0; - margin-left: 160px; - padding-left: 10px; -} - -form .aligned p.date div.help.timezonewarning, -form .aligned p.datetime div.help.timezonewarning, -form .aligned p.time div.help.timezonewarning { - margin-left: 0; - padding-left: 0; - font-weight: normal; -} - -form .aligned p.help:last-child, -form .aligned div.help:last-child { - margin-bottom: 0; - padding-bottom: 0; -} - -form .aligned input + p.help, -form .aligned textarea + p.help, -form .aligned select + p.help, -form .aligned input + div.help, -form .aligned textarea + div.help, -form .aligned select + div.help { - margin-left: 160px; - padding-left: 10px; -} - -form .aligned select option:checked { - background-color: var(--selected-row); -} - -form .aligned ul li { - list-style: none; -} - -form .aligned table p { - margin-left: 0; - padding-left: 0; -} - -.aligned .vCheckboxLabel { - padding: 1px 0 0 5px; -} - -.aligned .vCheckboxLabel + p.help, -.aligned .vCheckboxLabel + div.help { - margin-top: -4px; -} - -.colM .aligned .vLargeTextField, .colM .aligned .vXMLLargeTextField { - width: 610px; -} - -fieldset .fieldBox { - margin-right: 20px; -} - -/* WIDE FIELDSETS */ - -.wide label { - width: 200px; -} - -form .wide p.help, -form .wide ul.errorlist, -form .wide div.help { - padding-left: 50px; -} - -form div.help ul { - padding-left: 0; - margin-left: 0; -} - -.colM fieldset.wide .vLargeTextField, .colM fieldset.wide .vXMLLargeTextField { - width: 450px; -} - -/* COLLAPSIBLE FIELDSETS */ - -.collapse summary .fieldset-heading, -.collapse summary .inline-heading { - background: transparent; - border: none; - color: currentColor; - display: inline; - margin: 0; - padding: 0; -} - -/* MONOSPACE TEXTAREAS */ - -fieldset.monospace textarea { - font-family: var(--font-family-monospace); -} - -/* SUBMIT ROW */ - -.submit-row { - padding: 12px 14px 12px; - margin: 0 0 20px; - background: var(--darkened-bg); - border: 1px solid var(--hairline-color); - border-radius: 4px; - overflow: hidden; - display: flex; - gap: 10px; - flex-wrap: wrap; -} - -body.popup .submit-row { - overflow: auto; -} - -.submit-row input { - height: 2.1875rem; - line-height: 0.9375rem; -} - -.submit-row input, .submit-row a { - margin: 0; -} - -.submit-row input.default { - text-transform: uppercase; -} - -.submit-row a.deletelink { - margin-left: auto; -} - -.submit-row a.deletelink { - display: block; - background: var(--delete-button-bg); - border-radius: 4px; - padding: 0.625rem 0.9375rem; - height: 0.9375rem; - line-height: 0.9375rem; - color: var(--button-fg); -} - -.submit-row a.closelink { - display: inline-block; - background: var(--close-button-bg); - border-radius: 4px; - padding: 10px 15px; - height: 0.9375rem; - line-height: 0.9375rem; - color: var(--button-fg); -} - -.submit-row a.deletelink:focus, -.submit-row a.deletelink:hover, -.submit-row a.deletelink:active { - background: var(--delete-button-hover-bg); - text-decoration: none; -} - -.submit-row a.closelink:focus, -.submit-row a.closelink:hover, -.submit-row a.closelink:active { - background: var(--close-button-hover-bg); - text-decoration: none; -} - -/* CUSTOM FORM FIELDS */ - -.vSelectMultipleField { - vertical-align: top; -} - -.vCheckboxField { - border: none; -} - -.vDateField, .vTimeField { - margin-right: 2px; - margin-bottom: 4px; -} - -.vDateField { - min-width: 6.85em; -} - -.vTimeField { - min-width: 4.7em; -} - -.vURLField { - width: 30em; -} - -.vLargeTextField, .vXMLLargeTextField { - width: 48em; -} - -.flatpages-flatpage #id_content { - height: 40.2em; -} - -.module table .vPositiveSmallIntegerField { - width: 2.2em; -} - -.vIntegerField { - width: 5em; -} - -.vBigIntegerField { - width: 10em; -} - -.vForeignKeyRawIdAdminField { - width: 5em; -} - -.vTextField, .vUUIDField { - width: 20em; -} - -/* INLINES */ - -.inline-group { - padding: 0; - margin: 0 0 30px; -} - -.inline-group thead th { - padding: 8px 10px; -} - -.inline-group .aligned label { - width: 160px; -} - -.inline-related { - position: relative; -} - -.inline-related h4, -.inline-related:not(.tabular) .collapse summary { - margin: 0; - color: var(--body-medium-color); - padding: 5px; - font-size: 0.8125rem; - background: var(--darkened-bg); - border: 1px solid var(--hairline-color); - border-left-color: var(--darkened-bg); - border-right-color: var(--darkened-bg); -} - -.inline-related h3 span.delete { - float: right; -} - -.inline-related h3 span.delete label { - margin-left: 2px; - font-size: 0.6875rem; -} - -.inline-related fieldset { - margin: 0; - background: var(--body-bg); - border: none; - width: 100%; -} - -.inline-group .tabular fieldset.module { - border: none; -} - -.inline-related.tabular fieldset.module table { - width: 100%; - overflow-x: scroll; -} - -.last-related fieldset { - border: none; -} - -.inline-group .tabular tr.has_original td { - padding-top: 2em; -} - -.inline-group .tabular tr td.original { - padding: 2px 0 0 0; - width: 0; - _position: relative; -} - -.inline-group .tabular th.original { - width: 0px; - padding: 0; -} - -.inline-group .tabular td.original p { - position: absolute; - left: 0; - height: 1.1em; - padding: 2px 9px; - overflow: hidden; - font-size: 0.5625rem; - font-weight: bold; - color: var(--body-quiet-color); - _width: 700px; -} - -.inline-group ul.tools { - padding: 0; - margin: 0; - list-style: none; -} - -.inline-group ul.tools li { - display: inline; - padding: 0 5px; -} - -.inline-group div.add-row, -.inline-group .tabular tr.add-row td { - color: var(--body-quiet-color); - background: var(--darkened-bg); - padding: 8px 10px; - border-bottom: 1px solid var(--hairline-color); -} - -.inline-group .tabular tr.add-row td { - padding: 8px 10px; - border-bottom: 1px solid var(--hairline-color); -} - -.inline-group ul.tools a.add, -.inline-group div.add-row a, -.inline-group .tabular tr.add-row td a { - background: url(../img/icon-addlink.svg) 0 1px no-repeat; - padding-left: 16px; - font-size: 0.75rem; -} - -.empty-form { - display: none; -} - -/* RELATED FIELD ADD ONE / LOOKUP */ - -.related-lookup { - margin-left: 5px; - display: inline-block; - vertical-align: middle; - background-repeat: no-repeat; - background-size: 14px; -} - -.related-lookup { - width: 1rem; - height: 1rem; - background-image: url(../img/search.svg); -} - -form .related-widget-wrapper ul { - display: inline-block; - margin-left: 0; - padding-left: 0; -} - -.clearable-file-input input { - margin-top: 0; -} diff --git a/static/admin/css/login.css b/static/admin/css/login.css deleted file mode 100644 index 805a34b..0000000 --- a/static/admin/css/login.css +++ /dev/null @@ -1,61 +0,0 @@ -/* LOGIN FORM */ - -.login { - background: var(--darkened-bg); - height: auto; -} - -.login #header { - height: auto; - padding: 15px 16px; - justify-content: center; -} - -.login #header h1 { - font-size: 1.125rem; - margin: 0; -} - -.login #header h1 a { - color: var(--header-link-color); -} - -.login #content { - padding: 20px; -} - -.login #container { - background: var(--body-bg); - border: 1px solid var(--hairline-color); - border-radius: 4px; - overflow: hidden; - width: 28em; - min-width: 300px; - margin: 100px auto; - height: auto; -} - -.login .form-row { - padding: 4px 0; -} - -.login .form-row label { - display: block; - line-height: 2em; -} - -.login .form-row #id_username, .login .form-row #id_password { - padding: 8px; - width: 100%; - box-sizing: border-box; -} - -.login .submit-row { - padding: 1em 0 0 0; - margin: 0; - text-align: center; -} - -.login .password-reset-link { - text-align: center; -} diff --git a/static/admin/css/nav_sidebar.css b/static/admin/css/nav_sidebar.css deleted file mode 100644 index 7eb0de9..0000000 --- a/static/admin/css/nav_sidebar.css +++ /dev/null @@ -1,150 +0,0 @@ -.sticky { - position: sticky; - top: 0; - max-height: 100vh; -} - -.toggle-nav-sidebar { - z-index: 20; - left: 0; - display: flex; - align-items: center; - justify-content: center; - flex: 0 0 23px; - width: 23px; - border: 0; - border-right: 1px solid var(--hairline-color); - background-color: var(--body-bg); - cursor: pointer; - font-size: 1.25rem; - color: var(--link-fg); - padding: 0; -} - -[dir="rtl"] .toggle-nav-sidebar { - border-left: 1px solid var(--hairline-color); - border-right: 0; -} - -.toggle-nav-sidebar:hover, -.toggle-nav-sidebar:focus { - background-color: var(--darkened-bg); -} - -#nav-sidebar { - z-index: 15; - flex: 0 0 275px; - left: -276px; - margin-left: -276px; - border-top: 1px solid transparent; - border-right: 1px solid var(--hairline-color); - background-color: var(--body-bg); - overflow: auto; -} - -[dir="rtl"] #nav-sidebar { - border-left: 1px solid var(--hairline-color); - border-right: 0; - left: 0; - margin-left: 0; - right: -276px; - margin-right: -276px; -} - -.toggle-nav-sidebar::before { - content: '\00BB'; -} - -.main.shifted .toggle-nav-sidebar::before { - content: '\00AB'; -} - -.main > #nav-sidebar { - visibility: hidden; -} - -.main.shifted > #nav-sidebar { - margin-left: 0; - visibility: visible; -} - -[dir="rtl"] .main.shifted > #nav-sidebar { - margin-right: 0; -} - -#nav-sidebar .module th { - width: 100%; - overflow-wrap: anywhere; -} - -#nav-sidebar .module th, -#nav-sidebar .module caption { - padding-left: 16px; -} - -#nav-sidebar .module td { - white-space: nowrap; -} - -[dir="rtl"] #nav-sidebar .module th, -[dir="rtl"] #nav-sidebar .module caption { - padding-left: 8px; - padding-right: 16px; -} - -#nav-sidebar .current-app .section:link, -#nav-sidebar .current-app .section:visited { - color: var(--header-color); - font-weight: bold; -} - -#nav-sidebar .current-model { - background: var(--selected-row); -} - -@media (forced-colors: active) { - #nav-sidebar .current-model { - background-color: SelectedItem; - } -} - -.main > #nav-sidebar + .content { - max-width: calc(100% - 23px); -} - -.main.shifted > #nav-sidebar + .content { - max-width: calc(100% - 299px); -} - -@media (max-width: 767px) { - #nav-sidebar, #toggle-nav-sidebar { - display: none; - } - - .main > #nav-sidebar + .content, - .main.shifted > #nav-sidebar + .content { - max-width: 100%; - } -} - -#nav-filter { - width: 100%; - box-sizing: border-box; - padding: 2px 5px; - margin: 5px 0; - border: 1px solid var(--border-color); - background-color: var(--darkened-bg); - color: var(--body-fg); -} - -#nav-filter:focus { - border-color: var(--body-quiet-color); -} - -#nav-filter.no-results { - background: var(--message-error-bg); -} - -#nav-sidebar table { - width: 100%; -} diff --git a/static/admin/css/responsive.css b/static/admin/css/responsive.css deleted file mode 100644 index 932e824..0000000 --- a/static/admin/css/responsive.css +++ /dev/null @@ -1,967 +0,0 @@ -/* Tablets */ - -input[type="submit"], button { - -webkit-appearance: none; - appearance: none; -} - -@media (max-width: 1024px) { - /* Basic */ - - html { - -webkit-text-size-adjust: 100%; - } - - td, th { - padding: 10px; - font-size: 0.875rem; - } - - .small { - font-size: 0.75rem; - } - - /* Layout */ - - #container { - min-width: 0; - } - - #content { - padding: 15px 20px 20px; - } - - div.breadcrumbs { - padding: 10px 30px; - } - - /* Header */ - - #header { - flex-direction: column; - padding: 15px 30px; - justify-content: flex-start; - } - - #site-name { - margin: 0 0 8px; - line-height: 1.2; - } - - #user-tools { - margin: 0; - font-weight: 400; - line-height: 1.85; - text-align: left; - } - - #user-tools a { - display: inline-block; - line-height: 1.4; - } - - /* Dashboard */ - - .dashboard #content { - width: auto; - } - - #content-related { - margin-right: -290px; - } - - .colSM #content-related { - margin-left: -290px; - } - - .colMS { - margin-right: 290px; - } - - .colSM { - margin-left: 290px; - } - - .dashboard .module table td a { - padding-right: 0; - } - - td .changelink, td .addlink { - font-size: 0.8125rem; - } - - /* Changelist */ - - #toolbar { - border: none; - padding: 15px; - } - - #changelist-search > div { - display: flex; - flex-wrap: nowrap; - max-width: 480px; - } - - #changelist-search label { - line-height: 1.375rem; - } - - #toolbar form #searchbar { - flex: 1 0 auto; - width: 0; - height: 1.375rem; - margin: 0 10px 0 6px; - } - - #toolbar form input[type=submit] { - flex: 0 1 auto; - } - - #changelist-search .quiet { - width: 0; - flex: 1 0 auto; - margin: 5px 0 0 25px; - } - - #changelist .actions { - display: flex; - flex-wrap: wrap; - padding: 15px 0; - } - - #changelist .actions label { - display: flex; - } - - #changelist .actions select { - background: var(--body-bg); - } - - #changelist .actions .button { - min-width: 48px; - margin: 0 10px; - } - - #changelist .actions span.all, - #changelist .actions span.clear, - #changelist .actions span.question, - #changelist .actions span.action-counter { - font-size: 0.6875rem; - margin: 0 10px 0 0; - } - - #changelist-filter { - flex-basis: 200px; - } - - .change-list .filtered .results, - .change-list .filtered .paginator, - .filtered #toolbar, - .filtered .actions, - - #changelist .paginator { - border-top-color: var(--hairline-color); /* XXX Is this used at all? */ - } - - #changelist .results + .paginator { - border-top: none; - } - - /* Forms */ - - label { - font-size: 1rem; - } - - /* - Minifiers remove the default (text) "type" attribute from "input" HTML - tags. Add input:not([type]) to make the CSS stylesheet work the same. - */ - .form-row input:not([type]), - .form-row input[type=text], - .form-row input[type=password], - .form-row input[type=email], - .form-row input[type=url], - .form-row input[type=tel], - .form-row input[type=number], - .form-row textarea, - .form-row select, - .form-row .vTextField { - box-sizing: border-box; - margin: 0; - padding: 6px 8px; - min-height: 2.25rem; - font-size: 1rem; - } - - .form-row select { - height: 2.25rem; - } - - .form-row select[multiple] { - height: auto; - min-height: 0; - } - - fieldset .fieldBox + .fieldBox { - margin-top: 10px; - padding-top: 10px; - border-top: 1px solid var(--hairline-color); - } - - textarea { - max-width: 100%; - max-height: 120px; - } - - .aligned label { - padding-top: 6px; - } - - .aligned .related-lookup, - .aligned .datetimeshortcuts, - .aligned .related-lookup + strong { - align-self: center; - margin-left: 15px; - } - - form .aligned div.radiolist { - margin-left: 2px; - } - - .submit-row { - padding: 8px; - } - - .submit-row a.deletelink { - padding: 10px 7px; - } - - .button, input[type=submit], input[type=button], .submit-row input, a.button { - padding: 7px; - } - - /* Selector */ - - .selector { - display: flex; - width: 100%; - } - - .selector .selector-filter { - display: flex; - align-items: center; - } - - .selector .selector-filter label { - margin: 0 8px 0 0; - } - - .selector .selector-filter input { - width: 100%; - min-height: 0; - flex: 1 1; - } - - .selector-available, .selector-chosen { - width: auto; - flex: 1 1; - display: flex; - flex-direction: column; - } - - .selector select { - width: 100%; - flex: 1 0 auto; - margin-bottom: 5px; - } - - .selector ul.selector-chooser { - width: 26px; - height: 52px; - padding: 2px 0; - border-radius: 20px; - transform: translateY(-10px); - } - - .selector-add, .selector-remove { - width: 20px; - height: 20px; - background-size: 20px auto; - } - - .selector-add { - background-position: 0 -120px; - } - - .selector-remove { - background-position: 0 -80px; - } - - a.selector-chooseall, a.selector-clearall { - align-self: center; - } - - .stacked { - flex-direction: column; - max-width: 480px; - } - - .stacked > * { - flex: 0 1 auto; - } - - .stacked select { - margin-bottom: 0; - } - - .stacked .selector-available, .stacked .selector-chosen { - width: auto; - } - - .stacked ul.selector-chooser { - width: 52px; - height: 26px; - padding: 0 2px; - transform: none; - } - - .stacked .selector-chooser li { - padding: 3px; - } - - .stacked .selector-add, .stacked .selector-remove { - background-size: 20px auto; - } - - .stacked .selector-add { - background-position: 0 -40px; - } - - .stacked .active.selector-add { - background-position: 0 -40px; - } - - .active.selector-add:focus, .active.selector-add:hover { - background-position: 0 -140px; - } - - .stacked .active.selector-add:focus, .stacked .active.selector-add:hover { - background-position: 0 -60px; - } - - .stacked .selector-remove { - background-position: 0 0; - } - - .stacked .active.selector-remove { - background-position: 0 0; - } - - .active.selector-remove:focus, .active.selector-remove:hover { - background-position: 0 -100px; - } - - .stacked .active.selector-remove:focus, .stacked .active.selector-remove:hover { - background-position: 0 -20px; - } - - .help-tooltip, .selector .help-icon { - display: none; - } - - .datetime input { - width: 50%; - max-width: 120px; - } - - .datetime span { - font-size: 0.8125rem; - } - - .datetime .timezonewarning { - display: block; - font-size: 0.6875rem; - color: var(--body-quiet-color); - } - - .datetimeshortcuts { - color: var(--border-color); /* XXX Redundant, .datetime span also sets #ccc */ - } - - .form-row .datetime input.vDateField, .form-row .datetime input.vTimeField { - width: 75%; - } - - .inline-group { - overflow: auto; - } - - /* Messages */ - - ul.messagelist li { - padding-left: 55px; - background-position: 30px 12px; - } - - ul.messagelist li.error { - background-position: 30px 12px; - } - - ul.messagelist li.warning { - background-position: 30px 14px; - } - - /* Login */ - - .login #header { - padding: 15px 20px; - } - - .login #site-name { - margin: 0; - } - - /* GIS */ - - div.olMap { - max-width: calc(100vw - 30px); - max-height: 300px; - } - - .olMap + .clear_features { - display: block; - margin-top: 10px; - } - - /* Docs */ - - .module table.xfull { - width: 100%; - } - - pre.literal-block { - overflow: auto; - } -} - -/* Mobile */ - -@media (max-width: 767px) { - /* Layout */ - - #header, #content { - padding: 15px; - } - - div.breadcrumbs { - padding: 10px 15px; - } - - /* Dashboard */ - - .colMS, .colSM { - margin: 0; - } - - #content-related, .colSM #content-related { - width: 100%; - margin: 0; - } - - #content-related .module { - margin-bottom: 0; - } - - #content-related .module h2 { - padding: 10px 15px; - font-size: 1rem; - } - - /* Changelist */ - - #changelist { - align-items: stretch; - flex-direction: column; - } - - #toolbar { - padding: 10px; - } - - #changelist-filter { - margin-left: 0; - } - - #changelist .actions label { - flex: 1 1; - } - - #changelist .actions select { - flex: 1 0; - width: 100%; - } - - #changelist .actions span { - flex: 1 0 100%; - } - - #changelist-filter { - position: static; - width: auto; - margin-top: 30px; - } - - .object-tools { - float: none; - margin: 0 0 15px; - padding: 0; - overflow: hidden; - } - - .object-tools li { - height: auto; - margin-left: 0; - } - - .object-tools li + li { - margin-left: 15px; - } - - /* Forms */ - - .form-row { - padding: 15px 0; - } - - .aligned .form-row, - .aligned .form-row > div { - max-width: 100vw; - } - - .aligned .form-row > div { - width: calc(100vw - 30px); - } - - .flex-container { - flex-flow: column; - } - - .flex-container.checkbox-row { - flex-flow: row; - } - - textarea { - max-width: none; - } - - .vURLField { - width: auto; - } - - fieldset .fieldBox + .fieldBox { - margin-top: 15px; - padding-top: 15px; - } - - .aligned label { - width: 100%; - min-width: auto; - padding: 0 0 10px; - } - - .aligned label:after { - max-height: 0; - } - - .aligned .form-row input, - .aligned .form-row select, - .aligned .form-row textarea { - flex: 1 1 auto; - max-width: 100%; - } - - .aligned .checkbox-row input { - flex: 0 1 auto; - margin: 0; - } - - .aligned .vCheckboxLabel { - flex: 1 0; - padding: 1px 0 0 5px; - } - - .aligned label + p, - .aligned label + div.help, - .aligned label + div.readonly { - padding: 0; - margin-left: 0; - } - - .aligned p.file-upload { - font-size: 0.8125rem; - } - - span.clearable-file-input { - margin-left: 15px; - } - - span.clearable-file-input label { - font-size: 0.8125rem; - padding-bottom: 0; - } - - .aligned .timezonewarning { - flex: 1 0 100%; - margin-top: 5px; - } - - form .aligned .form-row div.help { - width: 100%; - margin: 5px 0 0; - padding: 0; - } - - form .aligned ul, - form .aligned ul.errorlist { - margin-left: 0; - padding-left: 0; - } - - form .aligned div.radiolist { - margin-top: 5px; - margin-right: 15px; - margin-bottom: -3px; - } - - form .aligned div.radiolist:not(.inline) div + div { - margin-top: 5px; - } - - /* Related widget */ - - .related-widget-wrapper { - width: 100%; - display: flex; - align-items: flex-start; - } - - .related-widget-wrapper .selector { - order: 1; - } - - .related-widget-wrapper > a { - order: 2; - } - - .related-widget-wrapper .radiolist ~ a { - align-self: flex-end; - } - - .related-widget-wrapper > select ~ a { - align-self: center; - } - - /* Selector */ - - .selector { - flex-direction: column; - gap: 10px 0; - } - - .selector-available, .selector-chosen { - flex: 1 1 auto; - } - - .selector select { - max-height: 96px; - } - - .selector ul.selector-chooser { - display: block; - width: 52px; - height: 26px; - padding: 0 2px; - transform: none; - } - - .selector ul.selector-chooser li { - float: left; - } - - .selector-remove { - background-position: 0 0; - } - - .active.selector-remove:focus, .active.selector-remove:hover { - background-position: 0 -20px; - } - - .selector-add { - background-position: 0 -40px; - } - - .active.selector-add:focus, .active.selector-add:hover { - background-position: 0 -60px; - } - - /* Inlines */ - - .inline-group[data-inline-type="stacked"] .inline-related { - border: 1px solid var(--hairline-color); - border-radius: 4px; - margin-top: 15px; - overflow: auto; - } - - .inline-group[data-inline-type="stacked"] .inline-related > * { - box-sizing: border-box; - } - - .inline-group[data-inline-type="stacked"] .inline-related .module { - padding: 0 10px; - } - - .inline-group[data-inline-type="stacked"] .inline-related .module .form-row { - border-top: 1px solid var(--hairline-color); - border-bottom: none; - } - - .inline-group[data-inline-type="stacked"] .inline-related .module .form-row:first-child { - border-top: none; - } - - .inline-group[data-inline-type="stacked"] .inline-related h3 { - padding: 10px; - border-top-width: 0; - border-bottom-width: 2px; - display: flex; - flex-wrap: wrap; - align-items: center; - } - - .inline-group[data-inline-type="stacked"] .inline-related h3 .inline_label { - margin-right: auto; - } - - .inline-group[data-inline-type="stacked"] .inline-related h3 span.delete { - float: none; - flex: 1 1 100%; - margin-top: 5px; - } - - .inline-group[data-inline-type="stacked"] .aligned .form-row > div:not([class]) { - width: 100%; - } - - .inline-group[data-inline-type="stacked"] .aligned label { - width: 100%; - } - - .inline-group[data-inline-type="stacked"] div.add-row { - margin-top: 15px; - border: 1px solid var(--hairline-color); - border-radius: 4px; - } - - .inline-group div.add-row, - .inline-group .tabular tr.add-row td { - padding: 0; - } - - .inline-group div.add-row a, - .inline-group .tabular tr.add-row td a { - display: block; - padding: 8px 10px 8px 26px; - background-position: 8px 9px; - } - - /* Submit row */ - - .submit-row { - padding: 10px; - margin: 0 0 15px; - flex-direction: column; - gap: 8px; - } - - .submit-row input, .submit-row input.default, .submit-row a { - text-align: center; - } - - .submit-row a.closelink { - padding: 10px 0; - text-align: center; - } - - .submit-row a.deletelink { - margin: 0; - } - - /* Messages */ - - ul.messagelist li { - padding-left: 40px; - background-position: 15px 12px; - } - - ul.messagelist li.error { - background-position: 15px 12px; - } - - ul.messagelist li.warning { - background-position: 15px 14px; - } - - /* Paginator */ - - .paginator .this-page, .paginator a:link, .paginator a:visited { - padding: 4px 10px; - } - - /* Login */ - - body.login { - padding: 0 15px; - } - - .login #container { - width: auto; - max-width: 480px; - margin: 50px auto; - } - - .login #header, - .login #content { - padding: 15px; - } - - .login #content-main { - float: none; - } - - .login .form-row { - padding: 0; - } - - .login .form-row + .form-row { - margin-top: 15px; - } - - .login .form-row label { - margin: 0 0 5px; - line-height: 1.2; - } - - .login .submit-row { - padding: 15px 0 0; - } - - .login br { - display: none; - } - - .login .submit-row input { - margin: 0; - text-transform: uppercase; - } - - .errornote { - margin: 0 0 20px; - padding: 8px 12px; - font-size: 0.8125rem; - } - - /* Calendar and clock */ - - .calendarbox, .clockbox { - position: fixed !important; - top: 50% !important; - left: 50% !important; - transform: translate(-50%, -50%); - margin: 0; - border: none; - overflow: visible; - } - - .calendarbox:before, .clockbox:before { - content: ''; - position: fixed; - top: 50%; - left: 50%; - width: 100vw; - height: 100vh; - background: rgba(0, 0, 0, 0.75); - transform: translate(-50%, -50%); - } - - .calendarbox > *, .clockbox > * { - position: relative; - z-index: 1; - } - - .calendarbox > div:first-child { - z-index: 2; - } - - .calendarbox .calendar, .clockbox h2 { - border-radius: 4px 4px 0 0; - overflow: hidden; - } - - .calendarbox .calendar-cancel, .clockbox .calendar-cancel { - border-radius: 0 0 4px 4px; - overflow: hidden; - } - - .calendar-shortcuts { - padding: 10px 0; - font-size: 0.75rem; - line-height: 0.75rem; - } - - .calendar-shortcuts a { - margin: 0 4px; - } - - .timelist a { - background: var(--body-bg); - padding: 4px; - } - - .calendar-cancel { - padding: 8px 10px; - } - - .clockbox h2 { - padding: 8px 15px; - } - - .calendar caption { - padding: 10px; - } - - .calendarbox .calendarnav-previous, .calendarbox .calendarnav-next { - z-index: 1; - top: 10px; - } - - /* History */ - - table#change-history tbody th, table#change-history tbody td { - font-size: 0.8125rem; - word-break: break-word; - } - - table#change-history tbody th { - width: auto; - } - - /* Docs */ - - table.model tbody th, table.model tbody td { - font-size: 0.8125rem; - word-break: break-word; - } -} diff --git a/static/admin/css/responsive_rtl.css b/static/admin/css/responsive_rtl.css deleted file mode 100644 index 33b5784..0000000 --- a/static/admin/css/responsive_rtl.css +++ /dev/null @@ -1,111 +0,0 @@ -/* TABLETS */ - -@media (max-width: 1024px) { - [dir="rtl"] .colMS { - margin-right: 0; - } - - [dir="rtl"] #user-tools { - text-align: right; - } - - [dir="rtl"] #changelist .actions label { - padding-left: 10px; - padding-right: 0; - } - - [dir="rtl"] #changelist .actions select { - margin-left: 0; - margin-right: 15px; - } - - [dir="rtl"] .change-list .filtered .results, - [dir="rtl"] .change-list .filtered .paginator, - [dir="rtl"] .filtered #toolbar, - [dir="rtl"] .filtered div.xfull, - [dir="rtl"] .filtered .actions, - [dir="rtl"] #changelist-filter { - margin-left: 0; - } - - [dir="rtl"] .inline-group ul.tools a.add, - [dir="rtl"] .inline-group div.add-row a, - [dir="rtl"] .inline-group .tabular tr.add-row td a { - padding: 8px 26px 8px 10px; - background-position: calc(100% - 8px) 9px; - } - - [dir="rtl"] .selector .selector-filter label { - margin-right: 0; - margin-left: 8px; - } - - [dir="rtl"] .object-tools li { - float: right; - } - - [dir="rtl"] .object-tools li + li { - margin-left: 0; - margin-right: 15px; - } - - [dir="rtl"] .dashboard .module table td a { - padding-left: 0; - padding-right: 16px; - } - - [dir="rtl"] .selector-add { - background-position: 0 -80px; - } - - [dir="rtl"] .selector-remove { - background-position: 0 -120px; - } - - [dir="rtl"] .active.selector-add:focus, .active.selector-add:hover { - background-position: 0 -100px; - } - - [dir="rtl"] .active.selector-remove:focus, .active.selector-remove:hover { - background-position: 0 -140px; - } -} - -/* MOBILE */ - -@media (max-width: 767px) { - [dir="rtl"] .aligned .related-lookup, - [dir="rtl"] .aligned .datetimeshortcuts { - margin-left: 0; - margin-right: 15px; - } - - [dir="rtl"] .aligned ul, - [dir="rtl"] form .aligned ul.errorlist { - margin-right: 0; - } - - [dir="rtl"] #changelist-filter { - margin-left: 0; - margin-right: 0; - } - [dir="rtl"] .aligned .vCheckboxLabel { - padding: 1px 5px 0 0; - } - - [dir="rtl"] .selector-remove { - background-position: 0 0; - } - - [dir="rtl"] .active.selector-remove:focus, .active.selector-remove:hover { - background-position: 0 -20px; - } - - [dir="rtl"] .selector-add { - background-position: 0 -40px; - } - - [dir="rtl"] .active.selector-add:focus, .active.selector-add:hover { - background-position: 0 -60px; - } -} diff --git a/static/admin/css/rtl.css b/static/admin/css/rtl.css deleted file mode 100644 index b8f60e0..0000000 --- a/static/admin/css/rtl.css +++ /dev/null @@ -1,291 +0,0 @@ -/* GLOBAL */ - -th { - text-align: right; -} - -.module h2, .module caption { - text-align: right; -} - -.module ul, .module ol { - margin-left: 0; - margin-right: 1.5em; -} - -.viewlink, .addlink, .changelink, .hidelink { - padding-left: 0; - padding-right: 16px; - background-position: 100% 1px; -} - -.deletelink { - padding-left: 0; - padding-right: 16px; - background-position: 100% 1px; -} - -.object-tools { - float: left; -} - -thead th:first-child, -tfoot td:first-child { - border-left: none; -} - -/* LAYOUT */ - -#user-tools { - right: auto; - left: 0; - text-align: left; -} - -div.breadcrumbs { - text-align: right; -} - -#content-main { - float: right; -} - -#content-related { - float: left; - margin-left: -300px; - margin-right: auto; -} - -.colMS { - margin-left: 300px; - margin-right: 0; -} - -/* SORTABLE TABLES */ - -table thead th.sorted .sortoptions { - float: left; -} - -thead th.sorted .text { - padding-right: 0; - padding-left: 42px; -} - -/* dashboard styles */ - -.dashboard .module table td a { - padding-left: .6em; - padding-right: 16px; -} - -/* changelists styles */ - -.change-list .filtered table { - border-left: none; - border-right: 0px none; -} - -#changelist-filter { - border-left: none; - border-right: none; - margin-left: 0; - margin-right: 30px; -} - -#changelist-filter li.selected { - border-left: none; - padding-left: 10px; - margin-left: 0; - border-right: 5px solid var(--hairline-color); - padding-right: 10px; - margin-right: -15px; -} - -#changelist table tbody td:first-child, #changelist table tbody th:first-child { - border-right: none; - border-left: none; -} - -.paginator .end { - margin-left: 6px; - margin-right: 0; -} - -.paginator input { - margin-left: 0; - margin-right: auto; -} - -/* FORMS */ - -.aligned label { - padding: 0 0 3px 1em; -} - -.submit-row a.deletelink { - margin-left: 0; - margin-right: auto; -} - -.vDateField, .vTimeField { - margin-left: 2px; -} - -.aligned .form-row input { - margin-left: 5px; -} - -form .aligned ul { - margin-right: 163px; - padding-right: 10px; - margin-left: 0; - padding-left: 0; -} - -form ul.inline li { - float: right; - padding-right: 0; - padding-left: 7px; -} - -form .aligned p.help, -form .aligned div.help { - margin-left: 0; - margin-right: 160px; - padding-right: 10px; -} - -form div.help ul, -form .aligned .checkbox-row + .help, -form .aligned p.date div.help.timezonewarning, -form .aligned p.datetime div.help.timezonewarning, -form .aligned p.time div.help.timezonewarning { - margin-right: 0; - padding-right: 0; -} - -form .wide p.help, -form .wide ul.errorlist, -form .wide div.help { - padding-left: 0; - padding-right: 50px; -} - -.submit-row { - text-align: right; -} - -fieldset .fieldBox { - margin-left: 20px; - margin-right: 0; -} - -.errorlist li { - background-position: 100% 12px; - padding: 0; -} - -.errornote { - background-position: 100% 12px; - padding: 10px 12px; -} - -/* WIDGETS */ - -.calendarnav-previous { - top: 0; - left: auto; - right: 10px; - background: url(../img/calendar-icons.svg) 0 -15px no-repeat; -} - -.calendarnav-next { - top: 0; - right: auto; - left: 10px; - background: url(../img/calendar-icons.svg) 0 0 no-repeat; -} - -.calendar caption, .calendarbox h2 { - text-align: center; -} - -.selector { - float: right; -} - -.selector .selector-filter { - text-align: right; -} - -.selector-add { - background: url(../img/selector-icons.svg) 0 -64px no-repeat; -} - -.active.selector-add:focus, .active.selector-add:hover { - background-position: 0 -80px; -} - -.selector-remove { - background: url(../img/selector-icons.svg) 0 -96px no-repeat; -} - -.active.selector-remove:focus, .active.selector-remove:hover { - background-position: 0 -112px; -} - -a.selector-chooseall { - background: url(../img/selector-icons.svg) right -128px no-repeat; -} - -a.active.selector-chooseall:focus, a.active.selector-chooseall:hover { - background-position: 100% -144px; -} - -a.selector-clearall { - background: url(../img/selector-icons.svg) 0 -160px no-repeat; -} - -a.active.selector-clearall:focus, a.active.selector-clearall:hover { - background-position: 0 -176px; -} - -.inline-deletelink { - float: left; -} - -form .form-row p.datetime { - overflow: hidden; -} - -.related-widget-wrapper { - float: right; -} - -/* MISC */ - -.inline-related h2, .inline-group h2 { - text-align: right -} - -.inline-related h3 span.delete { - padding-right: 20px; - padding-left: inherit; - left: 10px; - right: inherit; - float:left; -} - -.inline-related h3 span.delete label { - margin-left: inherit; - margin-right: 2px; -} - -.inline-group .tabular td.original p { - right: 0; -} - -.selector .selector-chooser { - margin: 0; -} diff --git a/static/admin/css/unusable_password_field.css b/static/admin/css/unusable_password_field.css deleted file mode 100644 index d46eb03..0000000 --- a/static/admin/css/unusable_password_field.css +++ /dev/null @@ -1,19 +0,0 @@ -/* Hide warnings fields if usable password is selected */ -form:has(#id_usable_password input[value="true"]:checked) .messagelist { - display: none; -} - -/* Hide password fields if unusable password is selected */ -form:has(#id_usable_password input[value="false"]:checked) .field-password1, -form:has(#id_usable_password input[value="false"]:checked) .field-password2 { - display: none; -} - -/* Select appropriate submit button */ -form:has(#id_usable_password input[value="true"]:checked) input[type="submit"].unset-password { - display: none; -} - -form:has(#id_usable_password input[value="false"]:checked) input[type="submit"].set-password { - display: none; -} diff --git a/static/admin/css/vendor/select2/LICENSE-SELECT2.md b/static/admin/css/vendor/select2/LICENSE-SELECT2.md deleted file mode 100644 index 8cb8a2b..0000000 --- a/static/admin/css/vendor/select2/LICENSE-SELECT2.md +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2012-2017 Kevin Brown, Igor Vaynberg, and Select2 contributors - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/static/admin/css/vendor/select2/select2.css b/static/admin/css/vendor/select2/select2.css deleted file mode 100644 index 750b320..0000000 --- a/static/admin/css/vendor/select2/select2.css +++ /dev/null @@ -1,481 +0,0 @@ -.select2-container { - box-sizing: border-box; - display: inline-block; - margin: 0; - position: relative; - vertical-align: middle; } - .select2-container .select2-selection--single { - box-sizing: border-box; - cursor: pointer; - display: block; - height: 28px; - user-select: none; - -webkit-user-select: none; } - .select2-container .select2-selection--single .select2-selection__rendered { - display: block; - padding-left: 8px; - padding-right: 20px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; } - .select2-container .select2-selection--single .select2-selection__clear { - position: relative; } - .select2-container[dir="rtl"] .select2-selection--single .select2-selection__rendered { - padding-right: 8px; - padding-left: 20px; } - .select2-container .select2-selection--multiple { - box-sizing: border-box; - cursor: pointer; - display: block; - min-height: 32px; - user-select: none; - -webkit-user-select: none; } - .select2-container .select2-selection--multiple .select2-selection__rendered { - display: inline-block; - overflow: hidden; - padding-left: 8px; - text-overflow: ellipsis; - white-space: nowrap; } - .select2-container .select2-search--inline { - float: left; } - .select2-container .select2-search--inline .select2-search__field { - box-sizing: border-box; - border: none; - font-size: 100%; - margin-top: 5px; - padding: 0; } - .select2-container .select2-search--inline .select2-search__field::-webkit-search-cancel-button { - -webkit-appearance: none; } - -.select2-dropdown { - background-color: white; - border: 1px solid #aaa; - border-radius: 4px; - box-sizing: border-box; - display: block; - position: absolute; - left: -100000px; - width: 100%; - z-index: 1051; } - -.select2-results { - display: block; } - -.select2-results__options { - list-style: none; - margin: 0; - padding: 0; } - -.select2-results__option { - padding: 6px; - user-select: none; - -webkit-user-select: none; } - .select2-results__option[aria-selected] { - cursor: pointer; } - -.select2-container--open .select2-dropdown { - left: 0; } - -.select2-container--open .select2-dropdown--above { - border-bottom: none; - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; } - -.select2-container--open .select2-dropdown--below { - border-top: none; - border-top-left-radius: 0; - border-top-right-radius: 0; } - -.select2-search--dropdown { - display: block; - padding: 4px; } - .select2-search--dropdown .select2-search__field { - padding: 4px; - width: 100%; - box-sizing: border-box; } - .select2-search--dropdown .select2-search__field::-webkit-search-cancel-button { - -webkit-appearance: none; } - .select2-search--dropdown.select2-search--hide { - display: none; } - -.select2-close-mask { - border: 0; - margin: 0; - padding: 0; - display: block; - position: fixed; - left: 0; - top: 0; - min-height: 100%; - min-width: 100%; - height: auto; - width: auto; - opacity: 0; - z-index: 99; - background-color: #fff; - filter: alpha(opacity=0); } - -.select2-hidden-accessible { - border: 0 !important; - clip: rect(0 0 0 0) !important; - -webkit-clip-path: inset(50%) !important; - clip-path: inset(50%) !important; - height: 1px !important; - overflow: hidden !important; - padding: 0 !important; - position: absolute !important; - width: 1px !important; - white-space: nowrap !important; } - -.select2-container--default .select2-selection--single { - background-color: #fff; - border: 1px solid #aaa; - border-radius: 4px; } - .select2-container--default .select2-selection--single .select2-selection__rendered { - color: #444; - line-height: 28px; } - .select2-container--default .select2-selection--single .select2-selection__clear { - cursor: pointer; - float: right; - font-weight: bold; } - .select2-container--default .select2-selection--single .select2-selection__placeholder { - color: #999; } - .select2-container--default .select2-selection--single .select2-selection__arrow { - height: 26px; - position: absolute; - top: 1px; - right: 1px; - width: 20px; } - .select2-container--default .select2-selection--single .select2-selection__arrow b { - border-color: #888 transparent transparent transparent; - border-style: solid; - border-width: 5px 4px 0 4px; - height: 0; - left: 50%; - margin-left: -4px; - margin-top: -2px; - position: absolute; - top: 50%; - width: 0; } - -.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__clear { - float: left; } - -.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__arrow { - left: 1px; - right: auto; } - -.select2-container--default.select2-container--disabled .select2-selection--single { - background-color: #eee; - cursor: default; } - .select2-container--default.select2-container--disabled .select2-selection--single .select2-selection__clear { - display: none; } - -.select2-container--default.select2-container--open .select2-selection--single .select2-selection__arrow b { - border-color: transparent transparent #888 transparent; - border-width: 0 4px 5px 4px; } - -.select2-container--default .select2-selection--multiple { - background-color: white; - border: 1px solid #aaa; - border-radius: 4px; - cursor: text; } - .select2-container--default .select2-selection--multiple .select2-selection__rendered { - box-sizing: border-box; - list-style: none; - margin: 0; - padding: 0 5px; - width: 100%; } - .select2-container--default .select2-selection--multiple .select2-selection__rendered li { - list-style: none; } - .select2-container--default .select2-selection--multiple .select2-selection__clear { - cursor: pointer; - float: right; - font-weight: bold; - margin-top: 5px; - margin-right: 10px; - padding: 1px; } - .select2-container--default .select2-selection--multiple .select2-selection__choice { - background-color: #e4e4e4; - border: 1px solid #aaa; - border-radius: 4px; - cursor: default; - float: left; - margin-right: 5px; - margin-top: 5px; - padding: 0 5px; } - .select2-container--default .select2-selection--multiple .select2-selection__choice__remove { - color: #999; - cursor: pointer; - display: inline-block; - font-weight: bold; - margin-right: 2px; } - .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover { - color: #333; } - -.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice, .select2-container--default[dir="rtl"] .select2-selection--multiple .select2-search--inline { - float: right; } - -.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice { - margin-left: 5px; - margin-right: auto; } - -.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove { - margin-left: 2px; - margin-right: auto; } - -.select2-container--default.select2-container--focus .select2-selection--multiple { - border: solid black 1px; - outline: 0; } - -.select2-container--default.select2-container--disabled .select2-selection--multiple { - background-color: #eee; - cursor: default; } - -.select2-container--default.select2-container--disabled .select2-selection__choice__remove { - display: none; } - -.select2-container--default.select2-container--open.select2-container--above .select2-selection--single, .select2-container--default.select2-container--open.select2-container--above .select2-selection--multiple { - border-top-left-radius: 0; - border-top-right-radius: 0; } - -.select2-container--default.select2-container--open.select2-container--below .select2-selection--single, .select2-container--default.select2-container--open.select2-container--below .select2-selection--multiple { - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; } - -.select2-container--default .select2-search--dropdown .select2-search__field { - border: 1px solid #aaa; } - -.select2-container--default .select2-search--inline .select2-search__field { - background: transparent; - border: none; - outline: 0; - box-shadow: none; - -webkit-appearance: textfield; } - -.select2-container--default .select2-results > .select2-results__options { - max-height: 200px; - overflow-y: auto; } - -.select2-container--default .select2-results__option[role=group] { - padding: 0; } - -.select2-container--default .select2-results__option[aria-disabled=true] { - color: #999; } - -.select2-container--default .select2-results__option[aria-selected=true] { - background-color: #ddd; } - -.select2-container--default .select2-results__option .select2-results__option { - padding-left: 1em; } - .select2-container--default .select2-results__option .select2-results__option .select2-results__group { - padding-left: 0; } - .select2-container--default .select2-results__option .select2-results__option .select2-results__option { - margin-left: -1em; - padding-left: 2em; } - .select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option { - margin-left: -2em; - padding-left: 3em; } - .select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option { - margin-left: -3em; - padding-left: 4em; } - .select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option { - margin-left: -4em; - padding-left: 5em; } - .select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option { - margin-left: -5em; - padding-left: 6em; } - -.select2-container--default .select2-results__option--highlighted[aria-selected] { - background-color: #5897fb; - color: white; } - -.select2-container--default .select2-results__group { - cursor: default; - display: block; - padding: 6px; } - -.select2-container--classic .select2-selection--single { - background-color: #f7f7f7; - border: 1px solid #aaa; - border-radius: 4px; - outline: 0; - background-image: -webkit-linear-gradient(top, white 50%, #eeeeee 100%); - background-image: -o-linear-gradient(top, white 50%, #eeeeee 100%); - background-image: linear-gradient(to bottom, white 50%, #eeeeee 100%); - background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0); } - .select2-container--classic .select2-selection--single:focus { - border: 1px solid #5897fb; } - .select2-container--classic .select2-selection--single .select2-selection__rendered { - color: #444; - line-height: 28px; } - .select2-container--classic .select2-selection--single .select2-selection__clear { - cursor: pointer; - float: right; - font-weight: bold; - margin-right: 10px; } - .select2-container--classic .select2-selection--single .select2-selection__placeholder { - color: #999; } - .select2-container--classic .select2-selection--single .select2-selection__arrow { - background-color: #ddd; - border: none; - border-left: 1px solid #aaa; - border-top-right-radius: 4px; - border-bottom-right-radius: 4px; - height: 26px; - position: absolute; - top: 1px; - right: 1px; - width: 20px; - background-image: -webkit-linear-gradient(top, #eeeeee 50%, #cccccc 100%); - background-image: -o-linear-gradient(top, #eeeeee 50%, #cccccc 100%); - background-image: linear-gradient(to bottom, #eeeeee 50%, #cccccc 100%); - background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFCCCCCC', GradientType=0); } - .select2-container--classic .select2-selection--single .select2-selection__arrow b { - border-color: #888 transparent transparent transparent; - border-style: solid; - border-width: 5px 4px 0 4px; - height: 0; - left: 50%; - margin-left: -4px; - margin-top: -2px; - position: absolute; - top: 50%; - width: 0; } - -.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__clear { - float: left; } - -.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__arrow { - border: none; - border-right: 1px solid #aaa; - border-radius: 0; - border-top-left-radius: 4px; - border-bottom-left-radius: 4px; - left: 1px; - right: auto; } - -.select2-container--classic.select2-container--open .select2-selection--single { - border: 1px solid #5897fb; } - .select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow { - background: transparent; - border: none; } - .select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow b { - border-color: transparent transparent #888 transparent; - border-width: 0 4px 5px 4px; } - -.select2-container--classic.select2-container--open.select2-container--above .select2-selection--single { - border-top: none; - border-top-left-radius: 0; - border-top-right-radius: 0; - background-image: -webkit-linear-gradient(top, white 0%, #eeeeee 50%); - background-image: -o-linear-gradient(top, white 0%, #eeeeee 50%); - background-image: linear-gradient(to bottom, white 0%, #eeeeee 50%); - background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0); } - -.select2-container--classic.select2-container--open.select2-container--below .select2-selection--single { - border-bottom: none; - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; - background-image: -webkit-linear-gradient(top, #eeeeee 50%, white 100%); - background-image: -o-linear-gradient(top, #eeeeee 50%, white 100%); - background-image: linear-gradient(to bottom, #eeeeee 50%, white 100%); - background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFFFFFFF', GradientType=0); } - -.select2-container--classic .select2-selection--multiple { - background-color: white; - border: 1px solid #aaa; - border-radius: 4px; - cursor: text; - outline: 0; } - .select2-container--classic .select2-selection--multiple:focus { - border: 1px solid #5897fb; } - .select2-container--classic .select2-selection--multiple .select2-selection__rendered { - list-style: none; - margin: 0; - padding: 0 5px; } - .select2-container--classic .select2-selection--multiple .select2-selection__clear { - display: none; } - .select2-container--classic .select2-selection--multiple .select2-selection__choice { - background-color: #e4e4e4; - border: 1px solid #aaa; - border-radius: 4px; - cursor: default; - float: left; - margin-right: 5px; - margin-top: 5px; - padding: 0 5px; } - .select2-container--classic .select2-selection--multiple .select2-selection__choice__remove { - color: #888; - cursor: pointer; - display: inline-block; - font-weight: bold; - margin-right: 2px; } - .select2-container--classic .select2-selection--multiple .select2-selection__choice__remove:hover { - color: #555; } - -.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice { - float: right; - margin-left: 5px; - margin-right: auto; } - -.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove { - margin-left: 2px; - margin-right: auto; } - -.select2-container--classic.select2-container--open .select2-selection--multiple { - border: 1px solid #5897fb; } - -.select2-container--classic.select2-container--open.select2-container--above .select2-selection--multiple { - border-top: none; - border-top-left-radius: 0; - border-top-right-radius: 0; } - -.select2-container--classic.select2-container--open.select2-container--below .select2-selection--multiple { - border-bottom: none; - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; } - -.select2-container--classic .select2-search--dropdown .select2-search__field { - border: 1px solid #aaa; - outline: 0; } - -.select2-container--classic .select2-search--inline .select2-search__field { - outline: 0; - box-shadow: none; } - -.select2-container--classic .select2-dropdown { - background-color: white; - border: 1px solid transparent; } - -.select2-container--classic .select2-dropdown--above { - border-bottom: none; } - -.select2-container--classic .select2-dropdown--below { - border-top: none; } - -.select2-container--classic .select2-results > .select2-results__options { - max-height: 200px; - overflow-y: auto; } - -.select2-container--classic .select2-results__option[role=group] { - padding: 0; } - -.select2-container--classic .select2-results__option[aria-disabled=true] { - color: grey; } - -.select2-container--classic .select2-results__option--highlighted[aria-selected] { - background-color: #3875d7; - color: white; } - -.select2-container--classic .select2-results__group { - cursor: default; - display: block; - padding: 6px; } - -.select2-container--classic.select2-container--open .select2-dropdown { - border-color: #5897fb; } diff --git a/static/admin/css/vendor/select2/select2.min.css b/static/admin/css/vendor/select2/select2.min.css deleted file mode 100644 index 7c18ad5..0000000 --- a/static/admin/css/vendor/select2/select2.min.css +++ /dev/null @@ -1 +0,0 @@ -.select2-container{box-sizing:border-box;display:inline-block;margin:0;position:relative;vertical-align:middle}.select2-container .select2-selection--single{box-sizing:border-box;cursor:pointer;display:block;height:28px;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--single .select2-selection__rendered{display:block;padding-left:8px;padding-right:20px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.select2-container .select2-selection--single .select2-selection__clear{position:relative}.select2-container[dir="rtl"] .select2-selection--single .select2-selection__rendered{padding-right:8px;padding-left:20px}.select2-container .select2-selection--multiple{box-sizing:border-box;cursor:pointer;display:block;min-height:32px;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--multiple .select2-selection__rendered{display:inline-block;overflow:hidden;padding-left:8px;text-overflow:ellipsis;white-space:nowrap}.select2-container .select2-search--inline{float:left}.select2-container .select2-search--inline .select2-search__field{box-sizing:border-box;border:none;font-size:100%;margin-top:5px;padding:0}.select2-container .select2-search--inline .select2-search__field::-webkit-search-cancel-button{-webkit-appearance:none}.select2-dropdown{background-color:white;border:1px solid #aaa;border-radius:4px;box-sizing:border-box;display:block;position:absolute;left:-100000px;width:100%;z-index:1051}.select2-results{display:block}.select2-results__options{list-style:none;margin:0;padding:0}.select2-results__option{padding:6px;user-select:none;-webkit-user-select:none}.select2-results__option[aria-selected]{cursor:pointer}.select2-container--open .select2-dropdown{left:0}.select2-container--open .select2-dropdown--above{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--open .select2-dropdown--below{border-top:none;border-top-left-radius:0;border-top-right-radius:0}.select2-search--dropdown{display:block;padding:4px}.select2-search--dropdown .select2-search__field{padding:4px;width:100%;box-sizing:border-box}.select2-search--dropdown .select2-search__field::-webkit-search-cancel-button{-webkit-appearance:none}.select2-search--dropdown.select2-search--hide{display:none}.select2-close-mask{border:0;margin:0;padding:0;display:block;position:fixed;left:0;top:0;min-height:100%;min-width:100%;height:auto;width:auto;opacity:0;z-index:99;background-color:#fff;filter:alpha(opacity=0)}.select2-hidden-accessible{border:0 !important;clip:rect(0 0 0 0) !important;-webkit-clip-path:inset(50%) !important;clip-path:inset(50%) !important;height:1px !important;overflow:hidden !important;padding:0 !important;position:absolute !important;width:1px !important;white-space:nowrap !important}.select2-container--default .select2-selection--single{background-color:#fff;border:1px solid #aaa;border-radius:4px}.select2-container--default .select2-selection--single .select2-selection__rendered{color:#444;line-height:28px}.select2-container--default .select2-selection--single .select2-selection__clear{cursor:pointer;float:right;font-weight:bold}.select2-container--default .select2-selection--single .select2-selection__placeholder{color:#999}.select2-container--default .select2-selection--single .select2-selection__arrow{height:26px;position:absolute;top:1px;right:1px;width:20px}.select2-container--default .select2-selection--single .select2-selection__arrow b{border-color:#888 transparent transparent transparent;border-style:solid;border-width:5px 4px 0 4px;height:0;left:50%;margin-left:-4px;margin-top:-2px;position:absolute;top:50%;width:0}.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__clear{float:left}.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__arrow{left:1px;right:auto}.select2-container--default.select2-container--disabled .select2-selection--single{background-color:#eee;cursor:default}.select2-container--default.select2-container--disabled .select2-selection--single .select2-selection__clear{display:none}.select2-container--default.select2-container--open .select2-selection--single .select2-selection__arrow b{border-color:transparent transparent #888 transparent;border-width:0 4px 5px 4px}.select2-container--default .select2-selection--multiple{background-color:white;border:1px solid #aaa;border-radius:4px;cursor:text}.select2-container--default .select2-selection--multiple .select2-selection__rendered{box-sizing:border-box;list-style:none;margin:0;padding:0 5px;width:100%}.select2-container--default .select2-selection--multiple .select2-selection__rendered li{list-style:none}.select2-container--default .select2-selection--multiple .select2-selection__clear{cursor:pointer;float:right;font-weight:bold;margin-top:5px;margin-right:10px;padding:1px}.select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#e4e4e4;border:1px solid #aaa;border-radius:4px;cursor:default;float:left;margin-right:5px;margin-top:5px;padding:0 5px}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:#999;cursor:pointer;display:inline-block;font-weight:bold;margin-right:2px}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#333}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice,.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-search--inline{float:right}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice{margin-left:5px;margin-right:auto}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove{margin-left:2px;margin-right:auto}.select2-container--default.select2-container--focus .select2-selection--multiple{border:solid black 1px;outline:0}.select2-container--default.select2-container--disabled .select2-selection--multiple{background-color:#eee;cursor:default}.select2-container--default.select2-container--disabled .select2-selection__choice__remove{display:none}.select2-container--default.select2-container--open.select2-container--above .select2-selection--single,.select2-container--default.select2-container--open.select2-container--above .select2-selection--multiple{border-top-left-radius:0;border-top-right-radius:0}.select2-container--default.select2-container--open.select2-container--below .select2-selection--single,.select2-container--default.select2-container--open.select2-container--below .select2-selection--multiple{border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--default .select2-search--dropdown .select2-search__field{border:1px solid #aaa}.select2-container--default .select2-search--inline .select2-search__field{background:transparent;border:none;outline:0;box-shadow:none;-webkit-appearance:textfield}.select2-container--default .select2-results>.select2-results__options{max-height:200px;overflow-y:auto}.select2-container--default .select2-results__option[role=group]{padding:0}.select2-container--default .select2-results__option[aria-disabled=true]{color:#999}.select2-container--default .select2-results__option[aria-selected=true]{background-color:#ddd}.select2-container--default .select2-results__option .select2-results__option{padding-left:1em}.select2-container--default .select2-results__option .select2-results__option .select2-results__group{padding-left:0}.select2-container--default .select2-results__option .select2-results__option .select2-results__option{margin-left:-1em;padding-left:2em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-2em;padding-left:3em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-3em;padding-left:4em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-4em;padding-left:5em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-5em;padding-left:6em}.select2-container--default .select2-results__option--highlighted[aria-selected]{background-color:#5897fb;color:white}.select2-container--default .select2-results__group{cursor:default;display:block;padding:6px}.select2-container--classic .select2-selection--single{background-color:#f7f7f7;border:1px solid #aaa;border-radius:4px;outline:0;background-image:-webkit-linear-gradient(top, #fff 50%, #eee 100%);background-image:-o-linear-gradient(top, #fff 50%, #eee 100%);background-image:linear-gradient(to bottom, #fff 50%, #eee 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0)}.select2-container--classic .select2-selection--single:focus{border:1px solid #5897fb}.select2-container--classic .select2-selection--single .select2-selection__rendered{color:#444;line-height:28px}.select2-container--classic .select2-selection--single .select2-selection__clear{cursor:pointer;float:right;font-weight:bold;margin-right:10px}.select2-container--classic .select2-selection--single .select2-selection__placeholder{color:#999}.select2-container--classic .select2-selection--single .select2-selection__arrow{background-color:#ddd;border:none;border-left:1px solid #aaa;border-top-right-radius:4px;border-bottom-right-radius:4px;height:26px;position:absolute;top:1px;right:1px;width:20px;background-image:-webkit-linear-gradient(top, #eee 50%, #ccc 100%);background-image:-o-linear-gradient(top, #eee 50%, #ccc 100%);background-image:linear-gradient(to bottom, #eee 50%, #ccc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFCCCCCC', GradientType=0)}.select2-container--classic .select2-selection--single .select2-selection__arrow b{border-color:#888 transparent transparent transparent;border-style:solid;border-width:5px 4px 0 4px;height:0;left:50%;margin-left:-4px;margin-top:-2px;position:absolute;top:50%;width:0}.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__clear{float:left}.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__arrow{border:none;border-right:1px solid #aaa;border-radius:0;border-top-left-radius:4px;border-bottom-left-radius:4px;left:1px;right:auto}.select2-container--classic.select2-container--open .select2-selection--single{border:1px solid #5897fb}.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow{background:transparent;border:none}.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow b{border-color:transparent transparent #888 transparent;border-width:0 4px 5px 4px}.select2-container--classic.select2-container--open.select2-container--above .select2-selection--single{border-top:none;border-top-left-radius:0;border-top-right-radius:0;background-image:-webkit-linear-gradient(top, #fff 0%, #eee 50%);background-image:-o-linear-gradient(top, #fff 0%, #eee 50%);background-image:linear-gradient(to bottom, #fff 0%, #eee 50%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0)}.select2-container--classic.select2-container--open.select2-container--below .select2-selection--single{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0;background-image:-webkit-linear-gradient(top, #eee 50%, #fff 100%);background-image:-o-linear-gradient(top, #eee 50%, #fff 100%);background-image:linear-gradient(to bottom, #eee 50%, #fff 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFFFFFFF', GradientType=0)}.select2-container--classic .select2-selection--multiple{background-color:white;border:1px solid #aaa;border-radius:4px;cursor:text;outline:0}.select2-container--classic .select2-selection--multiple:focus{border:1px solid #5897fb}.select2-container--classic .select2-selection--multiple .select2-selection__rendered{list-style:none;margin:0;padding:0 5px}.select2-container--classic .select2-selection--multiple .select2-selection__clear{display:none}.select2-container--classic .select2-selection--multiple .select2-selection__choice{background-color:#e4e4e4;border:1px solid #aaa;border-radius:4px;cursor:default;float:left;margin-right:5px;margin-top:5px;padding:0 5px}.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove{color:#888;cursor:pointer;display:inline-block;font-weight:bold;margin-right:2px}.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove:hover{color:#555}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice{float:right;margin-left:5px;margin-right:auto}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove{margin-left:2px;margin-right:auto}.select2-container--classic.select2-container--open .select2-selection--multiple{border:1px solid #5897fb}.select2-container--classic.select2-container--open.select2-container--above .select2-selection--multiple{border-top:none;border-top-left-radius:0;border-top-right-radius:0}.select2-container--classic.select2-container--open.select2-container--below .select2-selection--multiple{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--classic .select2-search--dropdown .select2-search__field{border:1px solid #aaa;outline:0}.select2-container--classic .select2-search--inline .select2-search__field{outline:0;box-shadow:none}.select2-container--classic .select2-dropdown{background-color:#fff;border:1px solid transparent}.select2-container--classic .select2-dropdown--above{border-bottom:none}.select2-container--classic .select2-dropdown--below{border-top:none}.select2-container--classic .select2-results>.select2-results__options{max-height:200px;overflow-y:auto}.select2-container--classic .select2-results__option[role=group]{padding:0}.select2-container--classic .select2-results__option[aria-disabled=true]{color:grey}.select2-container--classic .select2-results__option--highlighted[aria-selected]{background-color:#3875d7;color:#fff}.select2-container--classic .select2-results__group{cursor:default;display:block;padding:6px}.select2-container--classic.select2-container--open .select2-dropdown{border-color:#5897fb} diff --git a/static/admin/css/widgets.css b/static/admin/css/widgets.css deleted file mode 100644 index cc64811..0000000 --- a/static/admin/css/widgets.css +++ /dev/null @@ -1,593 +0,0 @@ -/* SELECTOR (FILTER INTERFACE) */ - -.selector { - display: flex; - flex-grow: 1; - gap: 0 10px; -} - -.selector select { - height: 17.2em; - flex: 1 0 auto; - overflow: scroll; - width: 100%; -} - -.selector-available, .selector-chosen { - text-align: center; - display: flex; - flex-direction: column; - flex: 1 1; -} - -.selector-available h2, .selector-chosen h2 { - border: 1px solid var(--border-color); - border-radius: 4px 4px 0 0; -} - -.selector-chosen .list-footer-display { - border: 1px solid var(--border-color); - border-top: none; - border-radius: 0 0 4px 4px; - margin: 0 0 10px; - padding: 8px; - text-align: center; - background: var(--primary); - color: var(--header-link-color); - cursor: pointer; -} -.selector-chosen .list-footer-display__clear { - color: var(--breadcrumbs-fg); -} - -.selector-chosen h2 { - background: var(--secondary); - color: var(--header-link-color); -} - -.selector .selector-available h2 { - background: var(--darkened-bg); - color: var(--body-quiet-color); -} - -.selector .selector-filter { - border: 1px solid var(--border-color); - border-width: 0 1px; - padding: 8px; - color: var(--body-quiet-color); - font-size: 0.625rem; - margin: 0; - text-align: left; - display: flex; -} - -.selector .selector-filter label, -.inline-group .aligned .selector .selector-filter label { - float: left; - margin: 7px 0 0; - width: 18px; - height: 18px; - padding: 0; - overflow: hidden; - line-height: 1; - min-width: auto; -} - -.selector-filter input { - flex-grow: 1; -} - -.selector .selector-available input, -.selector .selector-chosen input { - margin-left: 8px; -} - -.selector ul.selector-chooser { - align-self: center; - width: 22px; - background-color: var(--selected-bg); - border-radius: 10px; - margin: 0; - padding: 0; - transform: translateY(-17px); -} - -.selector-chooser li { - margin: 0; - padding: 3px; - list-style-type: none; -} - -.selector select { - padding: 0 10px; - margin: 0 0 10px; - border-radius: 0 0 4px 4px; -} -.selector .selector-chosen--with-filtered select { - margin: 0; - border-radius: 0; - height: 14em; -} - -.selector .selector-chosen:not(.selector-chosen--with-filtered) .list-footer-display { - display: none; -} - -.selector-add, .selector-remove { - width: 16px; - height: 16px; - display: block; - text-indent: -3000px; - overflow: hidden; - cursor: default; - opacity: 0.55; -} - -.active.selector-add, .active.selector-remove { - opacity: 1; -} - -.active.selector-add:hover, .active.selector-remove:hover { - cursor: pointer; -} - -.selector-add { - background: url(../img/selector-icons.svg) 0 -96px no-repeat; -} - -.active.selector-add:focus, .active.selector-add:hover { - background-position: 0 -112px; -} - -.selector-remove { - background: url(../img/selector-icons.svg) 0 -64px no-repeat; -} - -.active.selector-remove:focus, .active.selector-remove:hover { - background-position: 0 -80px; -} - -a.selector-chooseall, a.selector-clearall { - display: inline-block; - height: 16px; - text-align: left; - margin: 0 auto; - overflow: hidden; - font-weight: bold; - line-height: 16px; - color: var(--body-quiet-color); - text-decoration: none; - opacity: 0.55; -} - -a.active.selector-chooseall:focus, a.active.selector-clearall:focus, -a.active.selector-chooseall:hover, a.active.selector-clearall:hover { - color: var(--link-fg); -} - -a.active.selector-chooseall, a.active.selector-clearall { - opacity: 1; -} - -a.active.selector-chooseall:hover, a.active.selector-clearall:hover { - cursor: pointer; -} - -a.selector-chooseall { - padding: 0 18px 0 0; - background: url(../img/selector-icons.svg) right -160px no-repeat; - cursor: default; -} - -a.active.selector-chooseall:focus, a.active.selector-chooseall:hover { - background-position: 100% -176px; -} - -a.selector-clearall { - padding: 0 0 0 18px; - background: url(../img/selector-icons.svg) 0 -128px no-repeat; - cursor: default; -} - -a.active.selector-clearall:focus, a.active.selector-clearall:hover { - background-position: 0 -144px; -} - -/* STACKED SELECTORS */ - -.stacked { - float: left; - width: 490px; - display: block; -} - -.stacked select { - width: 480px; - height: 10.1em; -} - -.stacked .selector-available, .stacked .selector-chosen { - width: 480px; -} - -.stacked .selector-available { - margin-bottom: 0; -} - -.stacked .selector-available input { - width: 422px; -} - -.stacked ul.selector-chooser { - height: 22px; - width: 50px; - margin: 0 0 10px 40%; - background-color: #eee; - border-radius: 10px; - transform: none; -} - -.stacked .selector-chooser li { - float: left; - padding: 3px 3px 3px 5px; -} - -.stacked .selector-chooseall, .stacked .selector-clearall { - display: none; -} - -.stacked .selector-add { - background: url(../img/selector-icons.svg) 0 -32px no-repeat; - cursor: default; -} - -.stacked .active.selector-add { - background-position: 0 -32px; - cursor: pointer; -} - -.stacked .active.selector-add:focus, .stacked .active.selector-add:hover { - background-position: 0 -48px; - cursor: pointer; -} - -.stacked .selector-remove { - background: url(../img/selector-icons.svg) 0 0 no-repeat; - cursor: default; -} - -.stacked .active.selector-remove { - background-position: 0 0px; - cursor: pointer; -} - -.stacked .active.selector-remove:focus, .stacked .active.selector-remove:hover { - background-position: 0 -16px; - cursor: pointer; -} - -.selector .help-icon { - background: url(../img/icon-unknown.svg) 0 0 no-repeat; - display: inline-block; - vertical-align: middle; - margin: -2px 0 0 2px; - width: 13px; - height: 13px; -} - -.selector .selector-chosen .help-icon { - background: url(../img/icon-unknown-alt.svg) 0 0 no-repeat; -} - -.selector .search-label-icon { - background: url(../img/search.svg) 0 0 no-repeat; - display: inline-block; - height: 1.125rem; - width: 1.125rem; -} - -/* DATE AND TIME */ - -p.datetime { - line-height: 20px; - margin: 0; - padding: 0; - color: var(--body-quiet-color); - font-weight: bold; -} - -.datetime span { - white-space: nowrap; - font-weight: normal; - font-size: 0.6875rem; - color: var(--body-quiet-color); -} - -.datetime input, .form-row .datetime input.vDateField, .form-row .datetime input.vTimeField { - margin-left: 5px; - margin-bottom: 4px; -} - -table p.datetime { - font-size: 0.6875rem; - margin-left: 0; - padding-left: 0; -} - -.datetimeshortcuts .clock-icon, .datetimeshortcuts .date-icon { - position: relative; - display: inline-block; - vertical-align: middle; - height: 16px; - width: 16px; - overflow: hidden; -} - -.datetimeshortcuts .clock-icon { - background: url(../img/icon-clock.svg) 0 0 no-repeat; -} - -.datetimeshortcuts a:focus .clock-icon, -.datetimeshortcuts a:hover .clock-icon { - background-position: 0 -16px; -} - -.datetimeshortcuts .date-icon { - background: url(../img/icon-calendar.svg) 0 0 no-repeat; - top: -1px; -} - -.datetimeshortcuts a:focus .date-icon, -.datetimeshortcuts a:hover .date-icon { - background-position: 0 -16px; -} - -.timezonewarning { - font-size: 0.6875rem; - color: var(--body-quiet-color); -} - -/* URL */ - -p.url { - line-height: 20px; - margin: 0; - padding: 0; - color: var(--body-quiet-color); - font-size: 0.6875rem; - font-weight: bold; -} - -.url a { - font-weight: normal; -} - -/* FILE UPLOADS */ - -p.file-upload { - line-height: 20px; - margin: 0; - padding: 0; - color: var(--body-quiet-color); - font-size: 0.6875rem; - font-weight: bold; -} - -.file-upload a { - font-weight: normal; -} - -.file-upload .deletelink { - margin-left: 5px; -} - -span.clearable-file-input label { - color: var(--body-fg); - font-size: 0.6875rem; - display: inline; - float: none; -} - -/* CALENDARS & CLOCKS */ - -.calendarbox, .clockbox { - margin: 5px auto; - font-size: 0.75rem; - width: 19em; - text-align: center; - background: var(--body-bg); - color: var(--body-fg); - border: 1px solid var(--hairline-color); - border-radius: 4px; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15); - overflow: hidden; - position: relative; -} - -.clockbox { - width: auto; -} - -.calendar { - margin: 0; - padding: 0; -} - -.calendar table { - margin: 0; - padding: 0; - border-collapse: collapse; - background: white; - width: 100%; -} - -.calendar caption, .calendarbox h2 { - margin: 0; - text-align: center; - border-top: none; - font-weight: 700; - font-size: 0.75rem; - color: #333; - background: var(--accent); -} - -.calendar th { - padding: 8px 5px; - background: var(--darkened-bg); - border-bottom: 1px solid var(--border-color); - font-weight: 400; - font-size: 0.75rem; - text-align: center; - color: var(--body-quiet-color); -} - -.calendar td { - font-weight: 400; - font-size: 0.75rem; - text-align: center; - padding: 0; - border-top: 1px solid var(--hairline-color); - border-bottom: none; -} - -.calendar td.selected a { - background: var(--secondary); - color: var(--button-fg); -} - -.calendar td.nonday { - background: var(--darkened-bg); -} - -.calendar td.today a { - font-weight: 700; -} - -.calendar td a, .timelist a { - display: block; - font-weight: 400; - padding: 6px; - text-decoration: none; - color: var(--body-quiet-color); -} - -.calendar td a:focus, .timelist a:focus, -.calendar td a:hover, .timelist a:hover { - background: var(--primary); - color: white; -} - -.calendar td a:active, .timelist a:active { - background: var(--header-bg); - color: white; -} - -.calendarnav { - font-size: 0.625rem; - text-align: center; - color: #ccc; - margin: 0; - padding: 1px 3px; -} - -.calendarnav a:link, #calendarnav a:visited, -#calendarnav a:focus, #calendarnav a:hover { - color: var(--body-quiet-color); -} - -.calendar-shortcuts { - background: var(--body-bg); - color: var(--body-quiet-color); - font-size: 0.6875rem; - line-height: 0.6875rem; - border-top: 1px solid var(--hairline-color); - padding: 8px 0; -} - -.calendarbox .calendarnav-previous, .calendarbox .calendarnav-next { - display: block; - position: absolute; - top: 8px; - width: 15px; - height: 15px; - text-indent: -9999px; - padding: 0; -} - -.calendarnav-previous { - left: 10px; - background: url(../img/calendar-icons.svg) 0 0 no-repeat; -} - -.calendarnav-next { - right: 10px; - background: url(../img/calendar-icons.svg) 0 -15px no-repeat; -} - -.calendar-cancel { - margin: 0; - padding: 4px 0; - font-size: 0.75rem; - background: var(--close-button-bg); - border-top: 1px solid var(--border-color); - color: var(--button-fg); -} - -.calendar-cancel:focus, .calendar-cancel:hover { - background: var(--close-button-hover-bg); -} - -.calendar-cancel a { - color: var(--button-fg); - display: block; -} - -ul.timelist, .timelist li { - list-style-type: none; - margin: 0; - padding: 0; -} - -.timelist a { - padding: 2px; -} - -/* EDIT INLINE */ - -.inline-deletelink { - float: right; - text-indent: -9999px; - background: url(../img/inline-delete.svg) 0 0 no-repeat; - width: 16px; - height: 16px; - border: 0px none; -} - -.inline-deletelink:focus, .inline-deletelink:hover { - cursor: pointer; -} - -/* RELATED WIDGET WRAPPER */ -.related-widget-wrapper { - display: flex; - gap: 0 10px; - flex-grow: 1; - flex-wrap: wrap; - margin-bottom: 5px; -} - -.related-widget-wrapper-link { - opacity: .6; - filter: grayscale(1); -} - -.related-widget-wrapper-link:link { - opacity: 1; - filter: grayscale(0); -} - -/* GIS MAPS */ -.dj_map { - width: 600px; - height: 400px; -} diff --git a/static/admin/img/LICENSE b/static/admin/img/LICENSE deleted file mode 100644 index a4faaa1..0000000 --- a/static/admin/img/LICENSE +++ /dev/null @@ -1,20 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2014 Code Charm Ltd - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/static/admin/img/README.txt b/static/admin/img/README.txt deleted file mode 100644 index bf81f35..0000000 --- a/static/admin/img/README.txt +++ /dev/null @@ -1,7 +0,0 @@ -All icons are taken from Font Awesome (https://fontawesome.com/) project. -The Font Awesome font is licensed under the SIL OFL 1.1: -- https://scripts.sil.org/OFL - -SVG icons source: https://github.com/encharm/Font-Awesome-SVG-PNG -Font-Awesome-SVG-PNG is licensed under the MIT license (see file license -in current folder). diff --git a/static/admin/img/calendar-icons.svg b/static/admin/img/calendar-icons.svg deleted file mode 100644 index 04c0274..0000000 --- a/static/admin/img/calendar-icons.svg +++ /dev/null @@ -1,63 +0,0 @@ - - - - - - - - - - - - - - diff --git a/static/admin/img/gis/move_vertex_off.svg b/static/admin/img/gis/move_vertex_off.svg deleted file mode 100644 index 228854f..0000000 --- a/static/admin/img/gis/move_vertex_off.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/static/admin/img/gis/move_vertex_on.svg b/static/admin/img/gis/move_vertex_on.svg deleted file mode 100644 index 96b87fd..0000000 --- a/static/admin/img/gis/move_vertex_on.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/static/admin/img/icon-addlink.svg b/static/admin/img/icon-addlink.svg deleted file mode 100644 index 8d5c6a3..0000000 --- a/static/admin/img/icon-addlink.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/static/admin/img/icon-alert.svg b/static/admin/img/icon-alert.svg deleted file mode 100644 index e51ea83..0000000 --- a/static/admin/img/icon-alert.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/static/admin/img/icon-calendar.svg b/static/admin/img/icon-calendar.svg deleted file mode 100644 index 97910a9..0000000 --- a/static/admin/img/icon-calendar.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/static/admin/img/icon-changelink.svg b/static/admin/img/icon-changelink.svg deleted file mode 100644 index 592b093..0000000 --- a/static/admin/img/icon-changelink.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/static/admin/img/icon-clock.svg b/static/admin/img/icon-clock.svg deleted file mode 100644 index bf9985d..0000000 --- a/static/admin/img/icon-clock.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/static/admin/img/icon-deletelink.svg b/static/admin/img/icon-deletelink.svg deleted file mode 100644 index 4059b15..0000000 --- a/static/admin/img/icon-deletelink.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/static/admin/img/icon-hidelink.svg b/static/admin/img/icon-hidelink.svg deleted file mode 100644 index 2a8b404..0000000 --- a/static/admin/img/icon-hidelink.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/static/admin/img/icon-no.svg b/static/admin/img/icon-no.svg deleted file mode 100644 index 2e0d383..0000000 --- a/static/admin/img/icon-no.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/static/admin/img/icon-unknown-alt.svg b/static/admin/img/icon-unknown-alt.svg deleted file mode 100644 index 1c6b99f..0000000 --- a/static/admin/img/icon-unknown-alt.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/static/admin/img/icon-unknown.svg b/static/admin/img/icon-unknown.svg deleted file mode 100644 index 50b4f97..0000000 --- a/static/admin/img/icon-unknown.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/static/admin/img/icon-viewlink.svg b/static/admin/img/icon-viewlink.svg deleted file mode 100644 index a1ca1d3..0000000 --- a/static/admin/img/icon-viewlink.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/static/admin/img/icon-yes.svg b/static/admin/img/icon-yes.svg deleted file mode 100644 index 5883d87..0000000 --- a/static/admin/img/icon-yes.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/static/admin/img/inline-delete.svg b/static/admin/img/inline-delete.svg deleted file mode 100644 index 17d1ad6..0000000 --- a/static/admin/img/inline-delete.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/static/admin/img/search.svg b/static/admin/img/search.svg deleted file mode 100644 index c8c69b2..0000000 --- a/static/admin/img/search.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/static/admin/img/selector-icons.svg b/static/admin/img/selector-icons.svg deleted file mode 100644 index 926b8e2..0000000 --- a/static/admin/img/selector-icons.svg +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/static/admin/img/sorting-icons.svg b/static/admin/img/sorting-icons.svg deleted file mode 100644 index 7c31ec9..0000000 --- a/static/admin/img/sorting-icons.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/static/admin/img/tooltag-add.svg b/static/admin/img/tooltag-add.svg deleted file mode 100644 index 1ca64ae..0000000 --- a/static/admin/img/tooltag-add.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/static/admin/img/tooltag-arrowright.svg b/static/admin/img/tooltag-arrowright.svg deleted file mode 100644 index b664d61..0000000 --- a/static/admin/img/tooltag-arrowright.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/static/admin/js/SelectBox.js b/static/admin/js/SelectBox.js deleted file mode 100644 index 3db4ec7..0000000 --- a/static/admin/js/SelectBox.js +++ /dev/null @@ -1,116 +0,0 @@ -'use strict'; -{ - const SelectBox = { - cache: {}, - init: function(id) { - const box = document.getElementById(id); - SelectBox.cache[id] = []; - const cache = SelectBox.cache[id]; - for (const node of box.options) { - cache.push({value: node.value, text: node.text, displayed: 1}); - } - }, - redisplay: function(id) { - // Repopulate HTML select box from cache - const box = document.getElementById(id); - const scroll_value_from_top = box.scrollTop; - box.innerHTML = ''; - for (const node of SelectBox.cache[id]) { - if (node.displayed) { - const new_option = new Option(node.text, node.value, false, false); - // Shows a tooltip when hovering over the option - new_option.title = node.text; - box.appendChild(new_option); - } - } - box.scrollTop = scroll_value_from_top; - }, - filter: function(id, text) { - // Redisplay the HTML select box, displaying only the choices containing ALL - // the words in text. (It's an AND search.) - const tokens = text.toLowerCase().split(/\s+/); - for (const node of SelectBox.cache[id]) { - node.displayed = 1; - const node_text = node.text.toLowerCase(); - for (const token of tokens) { - if (!node_text.includes(token)) { - node.displayed = 0; - break; // Once the first token isn't found we're done - } - } - } - SelectBox.redisplay(id); - }, - get_hidden_node_count(id) { - const cache = SelectBox.cache[id] || []; - return cache.filter(node => node.displayed === 0).length; - }, - delete_from_cache: function(id, value) { - let delete_index = null; - const cache = SelectBox.cache[id]; - for (const [i, node] of cache.entries()) { - if (node.value === value) { - delete_index = i; - break; - } - } - cache.splice(delete_index, 1); - }, - add_to_cache: function(id, option) { - SelectBox.cache[id].push({value: option.value, text: option.text, displayed: 1}); - }, - cache_contains: function(id, value) { - // Check if an item is contained in the cache - for (const node of SelectBox.cache[id]) { - if (node.value === value) { - return true; - } - } - return false; - }, - move: function(from, to) { - const from_box = document.getElementById(from); - for (const option of from_box.options) { - const option_value = option.value; - if (option.selected && SelectBox.cache_contains(from, option_value)) { - SelectBox.add_to_cache(to, {value: option_value, text: option.text, displayed: 1}); - SelectBox.delete_from_cache(from, option_value); - } - } - SelectBox.redisplay(from); - SelectBox.redisplay(to); - }, - move_all: function(from, to) { - const from_box = document.getElementById(from); - for (const option of from_box.options) { - const option_value = option.value; - if (SelectBox.cache_contains(from, option_value)) { - SelectBox.add_to_cache(to, {value: option_value, text: option.text, displayed: 1}); - SelectBox.delete_from_cache(from, option_value); - } - } - SelectBox.redisplay(from); - SelectBox.redisplay(to); - }, - sort: function(id) { - SelectBox.cache[id].sort(function(a, b) { - a = a.text.toLowerCase(); - b = b.text.toLowerCase(); - if (a > b) { - return 1; - } - if (a < b) { - return -1; - } - return 0; - } ); - }, - select_all: function(id) { - const box = document.getElementById(id); - for (const option of box.options) { - option.selected = true; - } - } - }; - window.SelectBox = SelectBox; -} diff --git a/static/admin/js/SelectFilter2.js b/static/admin/js/SelectFilter2.js deleted file mode 100644 index 6957412..0000000 --- a/static/admin/js/SelectFilter2.js +++ /dev/null @@ -1,286 +0,0 @@ -/*global SelectBox, gettext, ngettext, interpolate, quickElement, SelectFilter*/ -/* -SelectFilter2 - Turns a multiple-select box into a filter interface. - -Requires core.js and SelectBox.js. -*/ -'use strict'; -{ - window.SelectFilter = { - init: function(field_id, field_name, is_stacked) { - if (field_id.match(/__prefix__/)) { - // Don't initialize on empty forms. - return; - } - const from_box = document.getElementById(field_id); - from_box.id += '_from'; // change its ID - from_box.className = 'filtered'; - - for (const p of from_box.parentNode.getElementsByTagName('p')) { - if (p.classList.contains("info")) { - // Remove

    , because it just gets in the way. - from_box.parentNode.removeChild(p); - } else if (p.classList.contains("help")) { - // Move help text up to the top so it isn't below the select - // boxes or wrapped off on the side to the right of the add - // button: - from_box.parentNode.insertBefore(p, from_box.parentNode.firstChild); - } - } - - //

    or
    - const selector_div = quickElement('div', from_box.parentNode); - // Make sure the selector div is at the beginning so that the - // add link would be displayed to the right of the widget. - from_box.parentNode.prepend(selector_div); - selector_div.className = is_stacked ? 'selector stacked' : 'selector'; - - //
    - const selector_available = quickElement('div', selector_div); - selector_available.className = 'selector-available'; - const title_available = quickElement('h2', selector_available, interpolate(gettext('Available %s') + ' ', [field_name])); - quickElement( - 'span', title_available, '', - 'class', 'help help-tooltip help-icon', - 'title', interpolate( - gettext( - 'This is the list of available %s. You may choose some by ' + - 'selecting them in the box below and then clicking the ' + - '"Choose" arrow between the two boxes.' - ), - [field_name] - ) - ); - - const filter_p = quickElement('p', selector_available, '', 'id', field_id + '_filter'); - filter_p.className = 'selector-filter'; - - const search_filter_label = quickElement('label', filter_p, '', 'for', field_id + '_input'); - - quickElement( - 'span', search_filter_label, '', - 'class', 'help-tooltip search-label-icon', - 'title', interpolate(gettext("Type into this box to filter down the list of available %s."), [field_name]) - ); - - filter_p.appendChild(document.createTextNode(' ')); - - const filter_input = quickElement('input', filter_p, '', 'type', 'text', 'placeholder', gettext("Filter")); - filter_input.id = field_id + '_input'; - - selector_available.appendChild(from_box); - const choose_all = quickElement('a', selector_available, gettext('Choose all'), 'title', interpolate(gettext('Click to choose all %s at once.'), [field_name]), 'href', '#', 'id', field_id + '_add_all_link'); - choose_all.className = 'selector-chooseall'; - - //
      - const selector_chooser = quickElement('ul', selector_div); - selector_chooser.className = 'selector-chooser'; - const add_link = quickElement('a', quickElement('li', selector_chooser), gettext('Choose'), 'title', gettext('Choose'), 'href', '#', 'id', field_id + '_add_link'); - add_link.className = 'selector-add'; - const remove_link = quickElement('a', quickElement('li', selector_chooser), gettext('Remove'), 'title', gettext('Remove'), 'href', '#', 'id', field_id + '_remove_link'); - remove_link.className = 'selector-remove'; - - //
      - const selector_chosen = quickElement('div', selector_div, '', 'id', field_id + '_selector_chosen'); - selector_chosen.className = 'selector-chosen'; - const title_chosen = quickElement('h2', selector_chosen, interpolate(gettext('Chosen %s') + ' ', [field_name])); - quickElement( - 'span', title_chosen, '', - 'class', 'help help-tooltip help-icon', - 'title', interpolate( - gettext( - 'This is the list of chosen %s. You may remove some by ' + - 'selecting them in the box below and then clicking the ' + - '"Remove" arrow between the two boxes.' - ), - [field_name] - ) - ); - - const filter_selected_p = quickElement('p', selector_chosen, '', 'id', field_id + '_filter_selected'); - filter_selected_p.className = 'selector-filter'; - - const search_filter_selected_label = quickElement('label', filter_selected_p, '', 'for', field_id + '_selected_input'); - - quickElement( - 'span', search_filter_selected_label, '', - 'class', 'help-tooltip search-label-icon', - 'title', interpolate(gettext("Type into this box to filter down the list of selected %s."), [field_name]) - ); - - filter_selected_p.appendChild(document.createTextNode(' ')); - - const filter_selected_input = quickElement('input', filter_selected_p, '', 'type', 'text', 'placeholder', gettext("Filter")); - filter_selected_input.id = field_id + '_selected_input'; - - const to_box = quickElement('select', selector_chosen, '', 'id', field_id + '_to', 'multiple', '', 'size', from_box.size, 'name', from_box.name); - to_box.className = 'filtered'; - - const warning_footer = quickElement('div', selector_chosen, '', 'class', 'list-footer-display'); - quickElement('span', warning_footer, '', 'id', field_id + '_list-footer-display-text'); - quickElement('span', warning_footer, ' (click to clear)', 'class', 'list-footer-display__clear'); - - const clear_all = quickElement('a', selector_chosen, gettext('Remove all'), 'title', interpolate(gettext('Click to remove all chosen %s at once.'), [field_name]), 'href', '#', 'id', field_id + '_remove_all_link'); - clear_all.className = 'selector-clearall'; - - from_box.name = from_box.name + '_old'; - - // Set up the JavaScript event handlers for the select box filter interface - const move_selection = function(e, elem, move_func, from, to) { - if (elem.classList.contains('active')) { - move_func(from, to); - SelectFilter.refresh_icons(field_id); - SelectFilter.refresh_filtered_selects(field_id); - SelectFilter.refresh_filtered_warning(field_id); - } - e.preventDefault(); - }; - choose_all.addEventListener('click', function(e) { - move_selection(e, this, SelectBox.move_all, field_id + '_from', field_id + '_to'); - }); - add_link.addEventListener('click', function(e) { - move_selection(e, this, SelectBox.move, field_id + '_from', field_id + '_to'); - }); - remove_link.addEventListener('click', function(e) { - move_selection(e, this, SelectBox.move, field_id + '_to', field_id + '_from'); - }); - clear_all.addEventListener('click', function(e) { - move_selection(e, this, SelectBox.move_all, field_id + '_to', field_id + '_from'); - }); - warning_footer.addEventListener('click', function(e) { - filter_selected_input.value = ''; - SelectBox.filter(field_id + '_to', ''); - SelectFilter.refresh_filtered_warning(field_id); - SelectFilter.refresh_icons(field_id); - }); - filter_input.addEventListener('keypress', function(e) { - SelectFilter.filter_key_press(e, field_id, '_from', '_to'); - }); - filter_input.addEventListener('keyup', function(e) { - SelectFilter.filter_key_up(e, field_id, '_from'); - }); - filter_input.addEventListener('keydown', function(e) { - SelectFilter.filter_key_down(e, field_id, '_from', '_to'); - }); - filter_selected_input.addEventListener('keypress', function(e) { - SelectFilter.filter_key_press(e, field_id, '_to', '_from'); - }); - filter_selected_input.addEventListener('keyup', function(e) { - SelectFilter.filter_key_up(e, field_id, '_to', '_selected_input'); - }); - filter_selected_input.addEventListener('keydown', function(e) { - SelectFilter.filter_key_down(e, field_id, '_to', '_from'); - }); - selector_div.addEventListener('change', function(e) { - if (e.target.tagName === 'SELECT') { - SelectFilter.refresh_icons(field_id); - } - }); - selector_div.addEventListener('dblclick', function(e) { - if (e.target.tagName === 'OPTION') { - if (e.target.closest('select').id === field_id + '_to') { - SelectBox.move(field_id + '_to', field_id + '_from'); - } else { - SelectBox.move(field_id + '_from', field_id + '_to'); - } - SelectFilter.refresh_icons(field_id); - } - }); - from_box.closest('form').addEventListener('submit', function() { - SelectBox.filter(field_id + '_to', ''); - SelectBox.select_all(field_id + '_to'); - }); - SelectBox.init(field_id + '_from'); - SelectBox.init(field_id + '_to'); - // Move selected from_box options to to_box - SelectBox.move(field_id + '_from', field_id + '_to'); - - // Initial icon refresh - SelectFilter.refresh_icons(field_id); - }, - any_selected: function(field) { - // Temporarily add the required attribute and check validity. - field.required = true; - const any_selected = field.checkValidity(); - field.required = false; - return any_selected; - }, - refresh_filtered_warning: function(field_id) { - const count = SelectBox.get_hidden_node_count(field_id + '_to'); - const selector = document.getElementById(field_id + '_selector_chosen'); - const warning = document.getElementById(field_id + '_list-footer-display-text'); - selector.className = selector.className.replace('selector-chosen--with-filtered', ''); - warning.textContent = interpolate(ngettext( - '%s selected option not visible', - '%s selected options not visible', - count - ), [count]); - if(count > 0) { - selector.className += ' selector-chosen--with-filtered'; - } - }, - refresh_filtered_selects: function(field_id) { - SelectBox.filter(field_id + '_from', document.getElementById(field_id + "_input").value); - SelectBox.filter(field_id + '_to', document.getElementById(field_id + "_selected_input").value); - }, - refresh_icons: function(field_id) { - const from = document.getElementById(field_id + '_from'); - const to = document.getElementById(field_id + '_to'); - // Active if at least one item is selected - document.getElementById(field_id + '_add_link').classList.toggle('active', SelectFilter.any_selected(from)); - document.getElementById(field_id + '_remove_link').classList.toggle('active', SelectFilter.any_selected(to)); - // Active if the corresponding box isn't empty - document.getElementById(field_id + '_add_all_link').classList.toggle('active', from.querySelector('option')); - document.getElementById(field_id + '_remove_all_link').classList.toggle('active', to.querySelector('option')); - SelectFilter.refresh_filtered_warning(field_id); - }, - filter_key_press: function(event, field_id, source, target) { - const source_box = document.getElementById(field_id + source); - // don't submit form if user pressed Enter - if ((event.which && event.which === 13) || (event.keyCode && event.keyCode === 13)) { - source_box.selectedIndex = 0; - SelectBox.move(field_id + source, field_id + target); - source_box.selectedIndex = 0; - event.preventDefault(); - } - }, - filter_key_up: function(event, field_id, source, filter_input) { - const input = filter_input || '_input'; - const source_box = document.getElementById(field_id + source); - const temp = source_box.selectedIndex; - SelectBox.filter(field_id + source, document.getElementById(field_id + input).value); - source_box.selectedIndex = temp; - SelectFilter.refresh_filtered_warning(field_id); - SelectFilter.refresh_icons(field_id); - }, - filter_key_down: function(event, field_id, source, target) { - const source_box = document.getElementById(field_id + source); - // right key (39) or left key (37) - const direction = source === '_from' ? 39 : 37; - // right arrow -- move across - if ((event.which && event.which === direction) || (event.keyCode && event.keyCode === direction)) { - const old_index = source_box.selectedIndex; - SelectBox.move(field_id + source, field_id + target); - SelectFilter.refresh_filtered_selects(field_id); - SelectFilter.refresh_filtered_warning(field_id); - source_box.selectedIndex = (old_index === source_box.length) ? source_box.length - 1 : old_index; - return; - } - // down arrow -- wrap around - if ((event.which && event.which === 40) || (event.keyCode && event.keyCode === 40)) { - source_box.selectedIndex = (source_box.length === source_box.selectedIndex + 1) ? 0 : source_box.selectedIndex + 1; - } - // up arrow -- wrap around - if ((event.which && event.which === 38) || (event.keyCode && event.keyCode === 38)) { - source_box.selectedIndex = (source_box.selectedIndex === 0) ? source_box.length - 1 : source_box.selectedIndex - 1; - } - } - }; - - window.addEventListener('load', function(e) { - document.querySelectorAll('select.selectfilter, select.selectfilterstacked').forEach(function(el) { - const data = el.dataset; - SelectFilter.init(el.id, data.fieldName, parseInt(data.isStacked, 10)); - }); - }); -} diff --git a/static/admin/js/actions.js b/static/admin/js/actions.js deleted file mode 100644 index 04b25e9..0000000 --- a/static/admin/js/actions.js +++ /dev/null @@ -1,204 +0,0 @@ -/*global gettext, interpolate, ngettext, Actions*/ -'use strict'; -{ - function show(selector) { - document.querySelectorAll(selector).forEach(function(el) { - el.classList.remove('hidden'); - }); - } - - function hide(selector) { - document.querySelectorAll(selector).forEach(function(el) { - el.classList.add('hidden'); - }); - } - - function showQuestion(options) { - hide(options.acrossClears); - show(options.acrossQuestions); - hide(options.allContainer); - } - - function showClear(options) { - show(options.acrossClears); - hide(options.acrossQuestions); - document.querySelector(options.actionContainer).classList.remove(options.selectedClass); - show(options.allContainer); - hide(options.counterContainer); - } - - function reset(options) { - hide(options.acrossClears); - hide(options.acrossQuestions); - hide(options.allContainer); - show(options.counterContainer); - } - - function clearAcross(options) { - reset(options); - const acrossInputs = document.querySelectorAll(options.acrossInput); - acrossInputs.forEach(function(acrossInput) { - acrossInput.value = 0; - }); - document.querySelector(options.actionContainer).classList.remove(options.selectedClass); - } - - function checker(actionCheckboxes, options, checked) { - if (checked) { - showQuestion(options); - } else { - reset(options); - } - actionCheckboxes.forEach(function(el) { - el.checked = checked; - el.closest('tr').classList.toggle(options.selectedClass, checked); - }); - } - - function updateCounter(actionCheckboxes, options) { - const sel = Array.from(actionCheckboxes).filter(function(el) { - return el.checked; - }).length; - const counter = document.querySelector(options.counterContainer); - // data-actions-icnt is defined in the generated HTML - // and contains the total amount of objects in the queryset - const actions_icnt = Number(counter.dataset.actionsIcnt); - counter.textContent = interpolate( - ngettext('%(sel)s of %(cnt)s selected', '%(sel)s of %(cnt)s selected', sel), { - sel: sel, - cnt: actions_icnt - }, true); - const allToggle = document.getElementById(options.allToggleId); - allToggle.checked = sel === actionCheckboxes.length; - if (allToggle.checked) { - showQuestion(options); - } else { - clearAcross(options); - } - } - - const defaults = { - actionContainer: "div.actions", - counterContainer: "span.action-counter", - allContainer: "div.actions span.all", - acrossInput: "div.actions input.select-across", - acrossQuestions: "div.actions span.question", - acrossClears: "div.actions span.clear", - allToggleId: "action-toggle", - selectedClass: "selected" - }; - - window.Actions = function(actionCheckboxes, options) { - options = Object.assign({}, defaults, options); - let list_editable_changed = false; - let lastChecked = null; - let shiftPressed = false; - - document.addEventListener('keydown', (event) => { - shiftPressed = event.shiftKey; - }); - - document.addEventListener('keyup', (event) => { - shiftPressed = event.shiftKey; - }); - - document.getElementById(options.allToggleId).addEventListener('click', function(event) { - checker(actionCheckboxes, options, this.checked); - updateCounter(actionCheckboxes, options); - }); - - document.querySelectorAll(options.acrossQuestions + " a").forEach(function(el) { - el.addEventListener('click', function(event) { - event.preventDefault(); - const acrossInputs = document.querySelectorAll(options.acrossInput); - acrossInputs.forEach(function(acrossInput) { - acrossInput.value = 1; - }); - showClear(options); - }); - }); - - document.querySelectorAll(options.acrossClears + " a").forEach(function(el) { - el.addEventListener('click', function(event) { - event.preventDefault(); - document.getElementById(options.allToggleId).checked = false; - clearAcross(options); - checker(actionCheckboxes, options, false); - updateCounter(actionCheckboxes, options); - }); - }); - - function affectedCheckboxes(target, withModifier) { - const multiSelect = (lastChecked && withModifier && lastChecked !== target); - if (!multiSelect) { - return [target]; - } - const checkboxes = Array.from(actionCheckboxes); - const targetIndex = checkboxes.findIndex(el => el === target); - const lastCheckedIndex = checkboxes.findIndex(el => el === lastChecked); - const startIndex = Math.min(targetIndex, lastCheckedIndex); - const endIndex = Math.max(targetIndex, lastCheckedIndex); - const filtered = checkboxes.filter((el, index) => (startIndex <= index) && (index <= endIndex)); - return filtered; - }; - - Array.from(document.getElementById('result_list').tBodies).forEach(function(el) { - el.addEventListener('change', function(event) { - const target = event.target; - if (target.classList.contains('action-select')) { - const checkboxes = affectedCheckboxes(target, shiftPressed); - checker(checkboxes, options, target.checked); - updateCounter(actionCheckboxes, options); - lastChecked = target; - } else { - list_editable_changed = true; - } - }); - }); - - document.querySelector('#changelist-form button[name=index]').addEventListener('click', function(event) { - if (list_editable_changed) { - const confirmed = confirm(gettext("You have unsaved changes on individual editable fields. If you run an action, your unsaved changes will be lost.")); - if (!confirmed) { - event.preventDefault(); - } - } - }); - - const el = document.querySelector('#changelist-form input[name=_save]'); - // The button does not exist if no fields are editable. - if (el) { - el.addEventListener('click', function(event) { - if (document.querySelector('[name=action]').value) { - const text = list_editable_changed - ? gettext("You have selected an action, but you haven’t saved your changes to individual fields yet. Please click OK to save. You’ll need to re-run the action.") - : gettext("You have selected an action, and you haven’t made any changes on individual fields. You’re probably looking for the Go button rather than the Save button."); - if (!confirm(text)) { - event.preventDefault(); - } - } - }); - } - // Sync counter when navigating to the page, such as through the back - // button. - window.addEventListener('pageshow', (event) => updateCounter(actionCheckboxes, options)); - }; - - // Call function fn when the DOM is loaded and ready. If it is already - // loaded, call the function now. - // http://youmightnotneedjquery.com/#ready - function ready(fn) { - if (document.readyState !== 'loading') { - fn(); - } else { - document.addEventListener('DOMContentLoaded', fn); - } - } - - ready(function() { - const actionsEls = document.querySelectorAll('tr input.action-select'); - if (actionsEls.length > 0) { - Actions(actionsEls); - } - }); -} diff --git a/static/admin/js/admin/DateTimeShortcuts.js b/static/admin/js/admin/DateTimeShortcuts.js deleted file mode 100644 index aa1cae9..0000000 --- a/static/admin/js/admin/DateTimeShortcuts.js +++ /dev/null @@ -1,408 +0,0 @@ -/*global Calendar, findPosX, findPosY, get_format, gettext, gettext_noop, interpolate, ngettext, quickElement*/ -// Inserts shortcut buttons after all of the following: -// -// -'use strict'; -{ - const DateTimeShortcuts = { - calendars: [], - calendarInputs: [], - clockInputs: [], - clockHours: { - default_: [ - [gettext_noop('Now'), -1], - [gettext_noop('Midnight'), 0], - [gettext_noop('6 a.m.'), 6], - [gettext_noop('Noon'), 12], - [gettext_noop('6 p.m.'), 18] - ] - }, - dismissClockFunc: [], - dismissCalendarFunc: [], - calendarDivName1: 'calendarbox', // name of calendar
      that gets toggled - calendarDivName2: 'calendarin', // name of
      that contains calendar - calendarLinkName: 'calendarlink', // name of the link that is used to toggle - clockDivName: 'clockbox', // name of clock
      that gets toggled - clockLinkName: 'clocklink', // name of the link that is used to toggle - shortCutsClass: 'datetimeshortcuts', // class of the clock and cal shortcuts - timezoneWarningClass: 'timezonewarning', // class of the warning for timezone mismatch - timezoneOffset: 0, - init: function() { - const serverOffset = document.body.dataset.adminUtcOffset; - if (serverOffset) { - const localOffset = new Date().getTimezoneOffset() * -60; - DateTimeShortcuts.timezoneOffset = localOffset - serverOffset; - } - - for (const inp of document.getElementsByTagName('input')) { - if (inp.type === 'text' && inp.classList.contains('vTimeField')) { - DateTimeShortcuts.addClock(inp); - DateTimeShortcuts.addTimezoneWarning(inp); - } - else if (inp.type === 'text' && inp.classList.contains('vDateField')) { - DateTimeShortcuts.addCalendar(inp); - DateTimeShortcuts.addTimezoneWarning(inp); - } - } - }, - // Return the current time while accounting for the server timezone. - now: function() { - const serverOffset = document.body.dataset.adminUtcOffset; - if (serverOffset) { - const localNow = new Date(); - const localOffset = localNow.getTimezoneOffset() * -60; - localNow.setTime(localNow.getTime() + 1000 * (serverOffset - localOffset)); - return localNow; - } else { - return new Date(); - } - }, - // Add a warning when the time zone in the browser and backend do not match. - addTimezoneWarning: function(inp) { - const warningClass = DateTimeShortcuts.timezoneWarningClass; - let timezoneOffset = DateTimeShortcuts.timezoneOffset / 3600; - - // Only warn if there is a time zone mismatch. - if (!timezoneOffset) { - return; - } - - // Check if warning is already there. - if (inp.parentNode.querySelectorAll('.' + warningClass).length) { - return; - } - - let message; - if (timezoneOffset > 0) { - message = ngettext( - 'Note: You are %s hour ahead of server time.', - 'Note: You are %s hours ahead of server time.', - timezoneOffset - ); - } - else { - timezoneOffset *= -1; - message = ngettext( - 'Note: You are %s hour behind server time.', - 'Note: You are %s hours behind server time.', - timezoneOffset - ); - } - message = interpolate(message, [timezoneOffset]); - - const warning = document.createElement('div'); - warning.classList.add('help', warningClass); - warning.textContent = message; - inp.parentNode.appendChild(warning); - }, - // Add clock widget to a given field - addClock: function(inp) { - const num = DateTimeShortcuts.clockInputs.length; - DateTimeShortcuts.clockInputs[num] = inp; - DateTimeShortcuts.dismissClockFunc[num] = function() { DateTimeShortcuts.dismissClock(num); return true; }; - - // Shortcut links (clock icon and "Now" link) - const shortcuts_span = document.createElement('span'); - shortcuts_span.className = DateTimeShortcuts.shortCutsClass; - inp.parentNode.insertBefore(shortcuts_span, inp.nextSibling); - const now_link = document.createElement('a'); - now_link.href = "#"; - now_link.textContent = gettext('Now'); - now_link.addEventListener('click', function(e) { - e.preventDefault(); - DateTimeShortcuts.handleClockQuicklink(num, -1); - }); - const clock_link = document.createElement('a'); - clock_link.href = '#'; - clock_link.id = DateTimeShortcuts.clockLinkName + num; - clock_link.addEventListener('click', function(e) { - e.preventDefault(); - // avoid triggering the document click handler to dismiss the clock - e.stopPropagation(); - DateTimeShortcuts.openClock(num); - }); - - quickElement( - 'span', clock_link, '', - 'class', 'clock-icon', - 'title', gettext('Choose a Time') - ); - shortcuts_span.appendChild(document.createTextNode('\u00A0')); - shortcuts_span.appendChild(now_link); - shortcuts_span.appendChild(document.createTextNode('\u00A0|\u00A0')); - shortcuts_span.appendChild(clock_link); - - // Create clock link div - // - // Markup looks like: - //
      - //

      Choose a time

      - // - //

      Cancel

      - //
      - - const clock_box = document.createElement('div'); - clock_box.style.display = 'none'; - clock_box.style.position = 'absolute'; - clock_box.className = 'clockbox module'; - clock_box.id = DateTimeShortcuts.clockDivName + num; - document.body.appendChild(clock_box); - clock_box.addEventListener('click', function(e) { e.stopPropagation(); }); - - quickElement('h2', clock_box, gettext('Choose a time')); - const time_list = quickElement('ul', clock_box); - time_list.className = 'timelist'; - // The list of choices can be overridden in JavaScript like this: - // DateTimeShortcuts.clockHours.name = [['3 a.m.', 3]]; - // where name is the name attribute of the . - const name = typeof DateTimeShortcuts.clockHours[inp.name] === 'undefined' ? 'default_' : inp.name; - DateTimeShortcuts.clockHours[name].forEach(function(element) { - const time_link = quickElement('a', quickElement('li', time_list), gettext(element[0]), 'href', '#'); - time_link.addEventListener('click', function(e) { - e.preventDefault(); - DateTimeShortcuts.handleClockQuicklink(num, element[1]); - }); - }); - - const cancel_p = quickElement('p', clock_box); - cancel_p.className = 'calendar-cancel'; - const cancel_link = quickElement('a', cancel_p, gettext('Cancel'), 'href', '#'); - cancel_link.addEventListener('click', function(e) { - e.preventDefault(); - DateTimeShortcuts.dismissClock(num); - }); - - document.addEventListener('keyup', function(event) { - if (event.which === 27) { - // ESC key closes popup - DateTimeShortcuts.dismissClock(num); - event.preventDefault(); - } - }); - }, - openClock: function(num) { - const clock_box = document.getElementById(DateTimeShortcuts.clockDivName + num); - const clock_link = document.getElementById(DateTimeShortcuts.clockLinkName + num); - - // Recalculate the clockbox position - // is it left-to-right or right-to-left layout ? - if (window.getComputedStyle(document.body).direction !== 'rtl') { - clock_box.style.left = findPosX(clock_link) + 17 + 'px'; - } - else { - // since style's width is in em, it'd be tough to calculate - // px value of it. let's use an estimated px for now - clock_box.style.left = findPosX(clock_link) - 110 + 'px'; - } - clock_box.style.top = Math.max(0, findPosY(clock_link) - 30) + 'px'; - - // Show the clock box - clock_box.style.display = 'block'; - document.addEventListener('click', DateTimeShortcuts.dismissClockFunc[num]); - }, - dismissClock: function(num) { - document.getElementById(DateTimeShortcuts.clockDivName + num).style.display = 'none'; - document.removeEventListener('click', DateTimeShortcuts.dismissClockFunc[num]); - }, - handleClockQuicklink: function(num, val) { - let d; - if (val === -1) { - d = DateTimeShortcuts.now(); - } - else { - d = new Date(1970, 1, 1, val, 0, 0, 0); - } - DateTimeShortcuts.clockInputs[num].value = d.strftime(get_format('TIME_INPUT_FORMATS')[0]); - DateTimeShortcuts.clockInputs[num].focus(); - DateTimeShortcuts.dismissClock(num); - }, - // Add calendar widget to a given field. - addCalendar: function(inp) { - const num = DateTimeShortcuts.calendars.length; - - DateTimeShortcuts.calendarInputs[num] = inp; - DateTimeShortcuts.dismissCalendarFunc[num] = function() { DateTimeShortcuts.dismissCalendar(num); return true; }; - - // Shortcut links (calendar icon and "Today" link) - const shortcuts_span = document.createElement('span'); - shortcuts_span.className = DateTimeShortcuts.shortCutsClass; - inp.parentNode.insertBefore(shortcuts_span, inp.nextSibling); - const today_link = document.createElement('a'); - today_link.href = '#'; - today_link.appendChild(document.createTextNode(gettext('Today'))); - today_link.addEventListener('click', function(e) { - e.preventDefault(); - DateTimeShortcuts.handleCalendarQuickLink(num, 0); - }); - const cal_link = document.createElement('a'); - cal_link.href = '#'; - cal_link.id = DateTimeShortcuts.calendarLinkName + num; - cal_link.addEventListener('click', function(e) { - e.preventDefault(); - // avoid triggering the document click handler to dismiss the calendar - e.stopPropagation(); - DateTimeShortcuts.openCalendar(num); - }); - quickElement( - 'span', cal_link, '', - 'class', 'date-icon', - 'title', gettext('Choose a Date') - ); - shortcuts_span.appendChild(document.createTextNode('\u00A0')); - shortcuts_span.appendChild(today_link); - shortcuts_span.appendChild(document.createTextNode('\u00A0|\u00A0')); - shortcuts_span.appendChild(cal_link); - - // Create calendarbox div. - // - // Markup looks like: - // - //
      - //

      - // - // February 2003 - //

      - //
      - // - //
      - //
      - // Yesterday | Today | Tomorrow - //
      - //

      Cancel

      - //
      - const cal_box = document.createElement('div'); - cal_box.style.display = 'none'; - cal_box.style.position = 'absolute'; - cal_box.className = 'calendarbox module'; - cal_box.id = DateTimeShortcuts.calendarDivName1 + num; - document.body.appendChild(cal_box); - cal_box.addEventListener('click', function(e) { e.stopPropagation(); }); - - // next-prev links - const cal_nav = quickElement('div', cal_box); - const cal_nav_prev = quickElement('a', cal_nav, '<', 'href', '#'); - cal_nav_prev.className = 'calendarnav-previous'; - cal_nav_prev.addEventListener('click', function(e) { - e.preventDefault(); - DateTimeShortcuts.drawPrev(num); - }); - - const cal_nav_next = quickElement('a', cal_nav, '>', 'href', '#'); - cal_nav_next.className = 'calendarnav-next'; - cal_nav_next.addEventListener('click', function(e) { - e.preventDefault(); - DateTimeShortcuts.drawNext(num); - }); - - // main box - const cal_main = quickElement('div', cal_box, '', 'id', DateTimeShortcuts.calendarDivName2 + num); - cal_main.className = 'calendar'; - DateTimeShortcuts.calendars[num] = new Calendar(DateTimeShortcuts.calendarDivName2 + num, DateTimeShortcuts.handleCalendarCallback(num)); - DateTimeShortcuts.calendars[num].drawCurrent(); - - // calendar shortcuts - const shortcuts = quickElement('div', cal_box); - shortcuts.className = 'calendar-shortcuts'; - let day_link = quickElement('a', shortcuts, gettext('Yesterday'), 'href', '#'); - day_link.addEventListener('click', function(e) { - e.preventDefault(); - DateTimeShortcuts.handleCalendarQuickLink(num, -1); - }); - shortcuts.appendChild(document.createTextNode('\u00A0|\u00A0')); - day_link = quickElement('a', shortcuts, gettext('Today'), 'href', '#'); - day_link.addEventListener('click', function(e) { - e.preventDefault(); - DateTimeShortcuts.handleCalendarQuickLink(num, 0); - }); - shortcuts.appendChild(document.createTextNode('\u00A0|\u00A0')); - day_link = quickElement('a', shortcuts, gettext('Tomorrow'), 'href', '#'); - day_link.addEventListener('click', function(e) { - e.preventDefault(); - DateTimeShortcuts.handleCalendarQuickLink(num, +1); - }); - - // cancel bar - const cancel_p = quickElement('p', cal_box); - cancel_p.className = 'calendar-cancel'; - const cancel_link = quickElement('a', cancel_p, gettext('Cancel'), 'href', '#'); - cancel_link.addEventListener('click', function(e) { - e.preventDefault(); - DateTimeShortcuts.dismissCalendar(num); - }); - document.addEventListener('keyup', function(event) { - if (event.which === 27) { - // ESC key closes popup - DateTimeShortcuts.dismissCalendar(num); - event.preventDefault(); - } - }); - }, - openCalendar: function(num) { - const cal_box = document.getElementById(DateTimeShortcuts.calendarDivName1 + num); - const cal_link = document.getElementById(DateTimeShortcuts.calendarLinkName + num); - const inp = DateTimeShortcuts.calendarInputs[num]; - - // Determine if the current value in the input has a valid date. - // If so, draw the calendar with that date's year and month. - if (inp.value) { - const format = get_format('DATE_INPUT_FORMATS')[0]; - const selected = inp.value.strptime(format); - const year = selected.getUTCFullYear(); - const month = selected.getUTCMonth() + 1; - const re = /\d{4}/; - if (re.test(year.toString()) && month >= 1 && month <= 12) { - DateTimeShortcuts.calendars[num].drawDate(month, year, selected); - } - } - - // Recalculate the clockbox position - // is it left-to-right or right-to-left layout ? - if (window.getComputedStyle(document.body).direction !== 'rtl') { - cal_box.style.left = findPosX(cal_link) + 17 + 'px'; - } - else { - // since style's width is in em, it'd be tough to calculate - // px value of it. let's use an estimated px for now - cal_box.style.left = findPosX(cal_link) - 180 + 'px'; - } - cal_box.style.top = Math.max(0, findPosY(cal_link) - 75) + 'px'; - - cal_box.style.display = 'block'; - document.addEventListener('click', DateTimeShortcuts.dismissCalendarFunc[num]); - }, - dismissCalendar: function(num) { - document.getElementById(DateTimeShortcuts.calendarDivName1 + num).style.display = 'none'; - document.removeEventListener('click', DateTimeShortcuts.dismissCalendarFunc[num]); - }, - drawPrev: function(num) { - DateTimeShortcuts.calendars[num].drawPreviousMonth(); - }, - drawNext: function(num) { - DateTimeShortcuts.calendars[num].drawNextMonth(); - }, - handleCalendarCallback: function(num) { - const format = get_format('DATE_INPUT_FORMATS')[0]; - return function(y, m, d) { - DateTimeShortcuts.calendarInputs[num].value = new Date(y, m - 1, d).strftime(format); - DateTimeShortcuts.calendarInputs[num].focus(); - document.getElementById(DateTimeShortcuts.calendarDivName1 + num).style.display = 'none'; - }; - }, - handleCalendarQuickLink: function(num, offset) { - const d = DateTimeShortcuts.now(); - d.setDate(d.getDate() + offset); - DateTimeShortcuts.calendarInputs[num].value = d.strftime(get_format('DATE_INPUT_FORMATS')[0]); - DateTimeShortcuts.calendarInputs[num].focus(); - DateTimeShortcuts.dismissCalendar(num); - } - }; - - window.addEventListener('load', DateTimeShortcuts.init); - window.DateTimeShortcuts = DateTimeShortcuts; -} diff --git a/static/admin/js/admin/RelatedObjectLookups.js b/static/admin/js/admin/RelatedObjectLookups.js deleted file mode 100644 index bc3acce..0000000 --- a/static/admin/js/admin/RelatedObjectLookups.js +++ /dev/null @@ -1,240 +0,0 @@ -/*global SelectBox, interpolate*/ -// Handles related-objects functionality: lookup link for raw_id_fields -// and Add Another links. -'use strict'; -{ - const $ = django.jQuery; - let popupIndex = 0; - const relatedWindows = []; - - function dismissChildPopups() { - relatedWindows.forEach(function(win) { - if(!win.closed) { - win.dismissChildPopups(); - win.close(); - } - }); - } - - function setPopupIndex() { - if(document.getElementsByName("_popup").length > 0) { - const index = window.name.lastIndexOf("__") + 2; - popupIndex = parseInt(window.name.substring(index)); - } else { - popupIndex = 0; - } - } - - function addPopupIndex(name) { - return name + "__" + (popupIndex + 1); - } - - function removePopupIndex(name) { - return name.replace(new RegExp("__" + (popupIndex + 1) + "$"), ''); - } - - function showAdminPopup(triggeringLink, name_regexp, add_popup) { - const name = addPopupIndex(triggeringLink.id.replace(name_regexp, '')); - const href = new URL(triggeringLink.href); - if (add_popup) { - href.searchParams.set('_popup', 1); - } - const win = window.open(href, name, 'height=500,width=800,resizable=yes,scrollbars=yes'); - relatedWindows.push(win); - win.focus(); - return false; - } - - function showRelatedObjectLookupPopup(triggeringLink) { - return showAdminPopup(triggeringLink, /^lookup_/, true); - } - - function dismissRelatedLookupPopup(win, chosenId) { - const name = removePopupIndex(win.name); - const elem = document.getElementById(name); - if (elem.classList.contains('vManyToManyRawIdAdminField') && elem.value) { - elem.value += ',' + chosenId; - } else { - document.getElementById(name).value = chosenId; - } - const index = relatedWindows.indexOf(win); - if (index > -1) { - relatedWindows.splice(index, 1); - } - win.close(); - } - - function showRelatedObjectPopup(triggeringLink) { - return showAdminPopup(triggeringLink, /^(change|add|delete)_/, false); - } - - function updateRelatedObjectLinks(triggeringLink) { - const $this = $(triggeringLink); - const siblings = $this.nextAll('.view-related, .change-related, .delete-related'); - if (!siblings.length) { - return; - } - const value = $this.val(); - if (value) { - siblings.each(function() { - const elm = $(this); - elm.attr('href', elm.attr('data-href-template').replace('__fk__', value)); - elm.removeAttr('aria-disabled'); - }); - } else { - siblings.removeAttr('href'); - siblings.attr('aria-disabled', true); - } - } - - function updateRelatedSelectsOptions(currentSelect, win, objId, newRepr, newId) { - // After create/edit a model from the options next to the current - // select (+ or :pencil:) update ForeignKey PK of the rest of selects - // in the page. - - const path = win.location.pathname; - // Extract the model from the popup url '...//add/' or - // '...///change/' depending the action (add or change). - const modelName = path.split('/')[path.split('/').length - (objId ? 4 : 3)]; - // Select elements with a specific model reference and context of "available-source". - const selectsRelated = document.querySelectorAll(`[data-model-ref="${modelName}"] [data-context="available-source"]`); - - selectsRelated.forEach(function(select) { - if (currentSelect === select) { - return; - } - - let option = select.querySelector(`option[value="${objId}"]`); - - if (!option) { - option = new Option(newRepr, newId); - select.options.add(option); - return; - } - - option.textContent = newRepr; - option.value = newId; - }); - } - - function dismissAddRelatedObjectPopup(win, newId, newRepr) { - const name = removePopupIndex(win.name); - const elem = document.getElementById(name); - if (elem) { - const elemName = elem.nodeName.toUpperCase(); - if (elemName === 'SELECT') { - elem.options[elem.options.length] = new Option(newRepr, newId, true, true); - updateRelatedSelectsOptions(elem, win, null, newRepr, newId); - } else if (elemName === 'INPUT') { - if (elem.classList.contains('vManyToManyRawIdAdminField') && elem.value) { - elem.value += ',' + newId; - } else { - elem.value = newId; - } - } - // Trigger a change event to update related links if required. - $(elem).trigger('change'); - } else { - const toId = name + "_to"; - const o = new Option(newRepr, newId); - SelectBox.add_to_cache(toId, o); - SelectBox.redisplay(toId); - } - const index = relatedWindows.indexOf(win); - if (index > -1) { - relatedWindows.splice(index, 1); - } - win.close(); - } - - function dismissChangeRelatedObjectPopup(win, objId, newRepr, newId) { - const id = removePopupIndex(win.name.replace(/^edit_/, '')); - const selectsSelector = interpolate('#%s, #%s_from, #%s_to', [id, id, id]); - const selects = $(selectsSelector); - selects.find('option').each(function() { - if (this.value === objId) { - this.textContent = newRepr; - this.value = newId; - } - }).trigger('change'); - updateRelatedSelectsOptions(selects[0], win, objId, newRepr, newId); - selects.next().find('.select2-selection__rendered').each(function() { - // The element can have a clear button as a child. - // Use the lastChild to modify only the displayed value. - this.lastChild.textContent = newRepr; - this.title = newRepr; - }); - const index = relatedWindows.indexOf(win); - if (index > -1) { - relatedWindows.splice(index, 1); - } - win.close(); - } - - function dismissDeleteRelatedObjectPopup(win, objId) { - const id = removePopupIndex(win.name.replace(/^delete_/, '')); - const selectsSelector = interpolate('#%s, #%s_from, #%s_to', [id, id, id]); - const selects = $(selectsSelector); - selects.find('option').each(function() { - if (this.value === objId) { - $(this).remove(); - } - }).trigger('change'); - const index = relatedWindows.indexOf(win); - if (index > -1) { - relatedWindows.splice(index, 1); - } - win.close(); - } - - window.showRelatedObjectLookupPopup = showRelatedObjectLookupPopup; - window.dismissRelatedLookupPopup = dismissRelatedLookupPopup; - window.showRelatedObjectPopup = showRelatedObjectPopup; - window.updateRelatedObjectLinks = updateRelatedObjectLinks; - window.dismissAddRelatedObjectPopup = dismissAddRelatedObjectPopup; - window.dismissChangeRelatedObjectPopup = dismissChangeRelatedObjectPopup; - window.dismissDeleteRelatedObjectPopup = dismissDeleteRelatedObjectPopup; - window.dismissChildPopups = dismissChildPopups; - - // Kept for backward compatibility - window.showAddAnotherPopup = showRelatedObjectPopup; - window.dismissAddAnotherPopup = dismissAddRelatedObjectPopup; - - window.addEventListener('unload', function(evt) { - window.dismissChildPopups(); - }); - - $(document).ready(function() { - setPopupIndex(); - $("a[data-popup-opener]").on('click', function(event) { - event.preventDefault(); - opener.dismissRelatedLookupPopup(window, $(this).data("popup-opener")); - }); - $('body').on('click', '.related-widget-wrapper-link[data-popup="yes"]', function(e) { - e.preventDefault(); - if (this.href) { - const event = $.Event('django:show-related', {href: this.href}); - $(this).trigger(event); - if (!event.isDefaultPrevented()) { - showRelatedObjectPopup(this); - } - } - }); - $('body').on('change', '.related-widget-wrapper select', function(e) { - const event = $.Event('django:update-related'); - $(this).trigger(event); - if (!event.isDefaultPrevented()) { - updateRelatedObjectLinks(this); - } - }); - $('.related-widget-wrapper select').trigger('change'); - $('body').on('click', '.related-lookup', function(e) { - e.preventDefault(); - const event = $.Event('django:lookup-related'); - $(this).trigger(event); - if (!event.isDefaultPrevented()) { - showRelatedObjectLookupPopup(this); - } - }); - }); -} diff --git a/static/admin/js/autocomplete.js b/static/admin/js/autocomplete.js deleted file mode 100644 index d3daeab..0000000 --- a/static/admin/js/autocomplete.js +++ /dev/null @@ -1,33 +0,0 @@ -'use strict'; -{ - const $ = django.jQuery; - - $.fn.djangoAdminSelect2 = function() { - $.each(this, function(i, element) { - $(element).select2({ - ajax: { - data: (params) => { - return { - term: params.term, - page: params.page, - app_label: element.dataset.appLabel, - model_name: element.dataset.modelName, - field_name: element.dataset.fieldName - }; - } - } - }); - }); - return this; - }; - - $(function() { - // Initialize all autocomplete widgets except the one in the template - // form used when a new formset is added. - $('.admin-autocomplete').not('[name*=__prefix__]').djangoAdminSelect2(); - }); - - document.addEventListener('formset:added', (event) => { - $(event.target).find('.admin-autocomplete').djangoAdminSelect2(); - }); -} diff --git a/static/admin/js/calendar.js b/static/admin/js/calendar.js deleted file mode 100644 index 776310f..0000000 --- a/static/admin/js/calendar.js +++ /dev/null @@ -1,239 +0,0 @@ -/*global gettext, pgettext, get_format, quickElement, removeChildren*/ -/* -calendar.js - Calendar functions by Adrian Holovaty -depends on core.js for utility functions like removeChildren or quickElement -*/ -'use strict'; -{ - // CalendarNamespace -- Provides a collection of HTML calendar-related helper functions - const CalendarNamespace = { - monthsOfYear: [ - gettext('January'), - gettext('February'), - gettext('March'), - gettext('April'), - gettext('May'), - gettext('June'), - gettext('July'), - gettext('August'), - gettext('September'), - gettext('October'), - gettext('November'), - gettext('December') - ], - monthsOfYearAbbrev: [ - pgettext('abbrev. month January', 'Jan'), - pgettext('abbrev. month February', 'Feb'), - pgettext('abbrev. month March', 'Mar'), - pgettext('abbrev. month April', 'Apr'), - pgettext('abbrev. month May', 'May'), - pgettext('abbrev. month June', 'Jun'), - pgettext('abbrev. month July', 'Jul'), - pgettext('abbrev. month August', 'Aug'), - pgettext('abbrev. month September', 'Sep'), - pgettext('abbrev. month October', 'Oct'), - pgettext('abbrev. month November', 'Nov'), - pgettext('abbrev. month December', 'Dec') - ], - daysOfWeek: [ - gettext('Sunday'), - gettext('Monday'), - gettext('Tuesday'), - gettext('Wednesday'), - gettext('Thursday'), - gettext('Friday'), - gettext('Saturday') - ], - daysOfWeekAbbrev: [ - pgettext('abbrev. day Sunday', 'Sun'), - pgettext('abbrev. day Monday', 'Mon'), - pgettext('abbrev. day Tuesday', 'Tue'), - pgettext('abbrev. day Wednesday', 'Wed'), - pgettext('abbrev. day Thursday', 'Thur'), - pgettext('abbrev. day Friday', 'Fri'), - pgettext('abbrev. day Saturday', 'Sat') - ], - daysOfWeekInitial: [ - pgettext('one letter Sunday', 'S'), - pgettext('one letter Monday', 'M'), - pgettext('one letter Tuesday', 'T'), - pgettext('one letter Wednesday', 'W'), - pgettext('one letter Thursday', 'T'), - pgettext('one letter Friday', 'F'), - pgettext('one letter Saturday', 'S') - ], - firstDayOfWeek: parseInt(get_format('FIRST_DAY_OF_WEEK')), - isLeapYear: function(year) { - return (((year % 4) === 0) && ((year % 100) !== 0 ) || ((year % 400) === 0)); - }, - getDaysInMonth: function(month, year) { - let days; - if (month === 1 || month === 3 || month === 5 || month === 7 || month === 8 || month === 10 || month === 12) { - days = 31; - } - else if (month === 4 || month === 6 || month === 9 || month === 11) { - days = 30; - } - else if (month === 2 && CalendarNamespace.isLeapYear(year)) { - days = 29; - } - else { - days = 28; - } - return days; - }, - draw: function(month, year, div_id, callback, selected) { // month = 1-12, year = 1-9999 - const today = new Date(); - const todayDay = today.getDate(); - const todayMonth = today.getMonth() + 1; - const todayYear = today.getFullYear(); - let todayClass = ''; - - // Use UTC functions here because the date field does not contain time - // and using the UTC function variants prevent the local time offset - // from altering the date, specifically the day field. For example: - // - // ``` - // var x = new Date('2013-10-02'); - // var day = x.getDate(); - // ``` - // - // The day variable above will be 1 instead of 2 in, say, US Pacific time - // zone. - let isSelectedMonth = false; - if (typeof selected !== 'undefined') { - isSelectedMonth = (selected.getUTCFullYear() === year && (selected.getUTCMonth() + 1) === month); - } - - month = parseInt(month); - year = parseInt(year); - const calDiv = document.getElementById(div_id); - removeChildren(calDiv); - const calTable = document.createElement('table'); - quickElement('caption', calTable, CalendarNamespace.monthsOfYear[month - 1] + ' ' + year); - const tableBody = quickElement('tbody', calTable); - - // Draw days-of-week header - let tableRow = quickElement('tr', tableBody); - for (let i = 0; i < 7; i++) { - quickElement('th', tableRow, CalendarNamespace.daysOfWeekInitial[(i + CalendarNamespace.firstDayOfWeek) % 7]); - } - - const startingPos = new Date(year, month - 1, 1 - CalendarNamespace.firstDayOfWeek).getDay(); - const days = CalendarNamespace.getDaysInMonth(month, year); - - let nonDayCell; - - // Draw blanks before first of month - tableRow = quickElement('tr', tableBody); - for (let i = 0; i < startingPos; i++) { - nonDayCell = quickElement('td', tableRow, ' '); - nonDayCell.className = "nonday"; - } - - function calendarMonth(y, m) { - function onClick(e) { - e.preventDefault(); - callback(y, m, this.textContent); - } - return onClick; - } - - // Draw days of month - let currentDay = 1; - for (let i = startingPos; currentDay <= days; i++) { - if (i % 7 === 0 && currentDay !== 1) { - tableRow = quickElement('tr', tableBody); - } - if ((currentDay === todayDay) && (month === todayMonth) && (year === todayYear)) { - todayClass = 'today'; - } else { - todayClass = ''; - } - - // use UTC function; see above for explanation. - if (isSelectedMonth && currentDay === selected.getUTCDate()) { - if (todayClass !== '') { - todayClass += " "; - } - todayClass += "selected"; - } - - const cell = quickElement('td', tableRow, '', 'class', todayClass); - const link = quickElement('a', cell, currentDay, 'href', '#'); - link.addEventListener('click', calendarMonth(year, month)); - currentDay++; - } - - // Draw blanks after end of month (optional, but makes for valid code) - while (tableRow.childNodes.length < 7) { - nonDayCell = quickElement('td', tableRow, ' '); - nonDayCell.className = "nonday"; - } - - calDiv.appendChild(calTable); - } - }; - - // Calendar -- A calendar instance - function Calendar(div_id, callback, selected) { - // div_id (string) is the ID of the element in which the calendar will - // be displayed - // callback (string) is the name of a JavaScript function that will be - // called with the parameters (year, month, day) when a day in the - // calendar is clicked - this.div_id = div_id; - this.callback = callback; - this.today = new Date(); - this.currentMonth = this.today.getMonth() + 1; - this.currentYear = this.today.getFullYear(); - if (typeof selected !== 'undefined') { - this.selected = selected; - } - } - Calendar.prototype = { - drawCurrent: function() { - CalendarNamespace.draw(this.currentMonth, this.currentYear, this.div_id, this.callback, this.selected); - }, - drawDate: function(month, year, selected) { - this.currentMonth = month; - this.currentYear = year; - - if(selected) { - this.selected = selected; - } - - this.drawCurrent(); - }, - drawPreviousMonth: function() { - if (this.currentMonth === 1) { - this.currentMonth = 12; - this.currentYear--; - } - else { - this.currentMonth--; - } - this.drawCurrent(); - }, - drawNextMonth: function() { - if (this.currentMonth === 12) { - this.currentMonth = 1; - this.currentYear++; - } - else { - this.currentMonth++; - } - this.drawCurrent(); - }, - drawPreviousYear: function() { - this.currentYear--; - this.drawCurrent(); - }, - drawNextYear: function() { - this.currentYear++; - this.drawCurrent(); - } - }; - window.Calendar = Calendar; - window.CalendarNamespace = CalendarNamespace; -} diff --git a/static/admin/js/cancel.js b/static/admin/js/cancel.js deleted file mode 100644 index 3069c6f..0000000 --- a/static/admin/js/cancel.js +++ /dev/null @@ -1,29 +0,0 @@ -'use strict'; -{ - // Call function fn when the DOM is loaded and ready. If it is already - // loaded, call the function now. - // http://youmightnotneedjquery.com/#ready - function ready(fn) { - if (document.readyState !== 'loading') { - fn(); - } else { - document.addEventListener('DOMContentLoaded', fn); - } - } - - ready(function() { - function handleClick(event) { - event.preventDefault(); - const params = new URLSearchParams(window.location.search); - if (params.has('_popup')) { - window.close(); // Close the popup. - } else { - window.history.back(); // Otherwise, go back. - } - } - - document.querySelectorAll('.cancel-link').forEach(function(el) { - el.addEventListener('click', handleClick); - }); - }); -} diff --git a/static/admin/js/change_form.js b/static/admin/js/change_form.js deleted file mode 100644 index 96a4c62..0000000 --- a/static/admin/js/change_form.js +++ /dev/null @@ -1,16 +0,0 @@ -'use strict'; -{ - const inputTags = ['BUTTON', 'INPUT', 'SELECT', 'TEXTAREA']; - const modelName = document.getElementById('django-admin-form-add-constants').dataset.modelName; - if (modelName) { - const form = document.getElementById(modelName + '_form'); - for (const element of form.elements) { - // HTMLElement.offsetParent returns null when the element is not - // rendered. - if (inputTags.includes(element.tagName) && !element.disabled && element.offsetParent) { - element.focus(); - break; - } - } - } -} diff --git a/static/admin/js/collapse.js b/static/admin/js/collapse.js deleted file mode 100644 index c6c7b0f..0000000 --- a/static/admin/js/collapse.js +++ /dev/null @@ -1,43 +0,0 @@ -/*global gettext*/ -'use strict'; -{ - window.addEventListener('load', function() { - // Add anchor tag for Show/Hide link - const fieldsets = document.querySelectorAll('fieldset.collapse'); - for (const [i, elem] of fieldsets.entries()) { - // Don't hide if fields in this fieldset have errors - if (elem.querySelectorAll('div.errors, ul.errorlist').length === 0) { - elem.classList.add('collapsed'); - const h2 = elem.querySelector('h2'); - const link = document.createElement('a'); - link.id = 'fieldsetcollapser' + i; - link.className = 'collapse-toggle'; - link.href = '#'; - link.textContent = gettext('Show'); - h2.appendChild(document.createTextNode(' (')); - h2.appendChild(link); - h2.appendChild(document.createTextNode(')')); - } - } - // Add toggle to hide/show anchor tag - const toggleFunc = function(ev) { - if (ev.target.matches('.collapse-toggle')) { - ev.preventDefault(); - ev.stopPropagation(); - const fieldset = ev.target.closest('fieldset'); - if (fieldset.classList.contains('collapsed')) { - // Show - ev.target.textContent = gettext('Hide'); - fieldset.classList.remove('collapsed'); - } else { - // Hide - ev.target.textContent = gettext('Show'); - fieldset.classList.add('collapsed'); - } - } - }; - document.querySelectorAll('fieldset.module').forEach(function(el) { - el.addEventListener('click', toggleFunc); - }); - }); -} diff --git a/static/admin/js/core.js b/static/admin/js/core.js deleted file mode 100644 index 10504d4..0000000 --- a/static/admin/js/core.js +++ /dev/null @@ -1,184 +0,0 @@ -// Core JavaScript helper functions -'use strict'; - -// quickElement(tagType, parentReference [, textInChildNode, attribute, attributeValue ...]); -function quickElement() { - const obj = document.createElement(arguments[0]); - if (arguments[2]) { - const textNode = document.createTextNode(arguments[2]); - obj.appendChild(textNode); - } - const len = arguments.length; - for (let i = 3; i < len; i += 2) { - obj.setAttribute(arguments[i], arguments[i + 1]); - } - arguments[1].appendChild(obj); - return obj; -} - -// "a" is reference to an object -function removeChildren(a) { - while (a.hasChildNodes()) { - a.removeChild(a.lastChild); - } -} - -// ---------------------------------------------------------------------------- -// Find-position functions by PPK -// See https://www.quirksmode.org/js/findpos.html -// ---------------------------------------------------------------------------- -function findPosX(obj) { - let curleft = 0; - if (obj.offsetParent) { - while (obj.offsetParent) { - curleft += obj.offsetLeft - obj.scrollLeft; - obj = obj.offsetParent; - } - } else if (obj.x) { - curleft += obj.x; - } - return curleft; -} - -function findPosY(obj) { - let curtop = 0; - if (obj.offsetParent) { - while (obj.offsetParent) { - curtop += obj.offsetTop - obj.scrollTop; - obj = obj.offsetParent; - } - } else if (obj.y) { - curtop += obj.y; - } - return curtop; -} - -//----------------------------------------------------------------------------- -// Date object extensions -// ---------------------------------------------------------------------------- -{ - Date.prototype.getTwelveHours = function() { - return this.getHours() % 12 || 12; - }; - - Date.prototype.getTwoDigitMonth = function() { - return (this.getMonth() < 9) ? '0' + (this.getMonth() + 1) : (this.getMonth() + 1); - }; - - Date.prototype.getTwoDigitDate = function() { - return (this.getDate() < 10) ? '0' + this.getDate() : this.getDate(); - }; - - Date.prototype.getTwoDigitTwelveHour = function() { - return (this.getTwelveHours() < 10) ? '0' + this.getTwelveHours() : this.getTwelveHours(); - }; - - Date.prototype.getTwoDigitHour = function() { - return (this.getHours() < 10) ? '0' + this.getHours() : this.getHours(); - }; - - Date.prototype.getTwoDigitMinute = function() { - return (this.getMinutes() < 10) ? '0' + this.getMinutes() : this.getMinutes(); - }; - - Date.prototype.getTwoDigitSecond = function() { - return (this.getSeconds() < 10) ? '0' + this.getSeconds() : this.getSeconds(); - }; - - Date.prototype.getAbbrevDayName = function() { - return typeof window.CalendarNamespace === "undefined" - ? '0' + this.getDay() - : window.CalendarNamespace.daysOfWeekAbbrev[this.getDay()]; - }; - - Date.prototype.getFullDayName = function() { - return typeof window.CalendarNamespace === "undefined" - ? '0' + this.getDay() - : window.CalendarNamespace.daysOfWeek[this.getDay()]; - }; - - Date.prototype.getAbbrevMonthName = function() { - return typeof window.CalendarNamespace === "undefined" - ? this.getTwoDigitMonth() - : window.CalendarNamespace.monthsOfYearAbbrev[this.getMonth()]; - }; - - Date.prototype.getFullMonthName = function() { - return typeof window.CalendarNamespace === "undefined" - ? this.getTwoDigitMonth() - : window.CalendarNamespace.monthsOfYear[this.getMonth()]; - }; - - Date.prototype.strftime = function(format) { - const fields = { - a: this.getAbbrevDayName(), - A: this.getFullDayName(), - b: this.getAbbrevMonthName(), - B: this.getFullMonthName(), - c: this.toString(), - d: this.getTwoDigitDate(), - H: this.getTwoDigitHour(), - I: this.getTwoDigitTwelveHour(), - m: this.getTwoDigitMonth(), - M: this.getTwoDigitMinute(), - p: (this.getHours() >= 12) ? 'PM' : 'AM', - S: this.getTwoDigitSecond(), - w: '0' + this.getDay(), - x: this.toLocaleDateString(), - X: this.toLocaleTimeString(), - y: ('' + this.getFullYear()).substr(2, 4), - Y: '' + this.getFullYear(), - '%': '%' - }; - let result = '', i = 0; - while (i < format.length) { - if (format.charAt(i) === '%') { - result += fields[format.charAt(i + 1)]; - ++i; - } - else { - result += format.charAt(i); - } - ++i; - } - return result; - }; - - // ---------------------------------------------------------------------------- - // String object extensions - // ---------------------------------------------------------------------------- - String.prototype.strptime = function(format) { - const split_format = format.split(/[.\-/]/); - const date = this.split(/[.\-/]/); - let i = 0; - let day, month, year; - while (i < split_format.length) { - switch (split_format[i]) { - case "%d": - day = date[i]; - break; - case "%m": - month = date[i] - 1; - break; - case "%Y": - year = date[i]; - break; - case "%y": - // A %y value in the range of [00, 68] is in the current - // century, while [69, 99] is in the previous century, - // according to the Open Group Specification. - if (parseInt(date[i], 10) >= 69) { - year = date[i]; - } else { - year = (new Date(Date.UTC(date[i], 0))).getUTCFullYear() + 100; - } - break; - } - ++i; - } - // Create Date object from UTC since the parsed value is supposed to be - // in UTC, not local time. Also, the calendar uses UTC functions for - // date extraction. - return new Date(Date.UTC(year, month, day)); - }; -} diff --git a/static/admin/js/filters.js b/static/admin/js/filters.js deleted file mode 100644 index f5536eb..0000000 --- a/static/admin/js/filters.js +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Persist changelist filters state (collapsed/expanded). - */ -'use strict'; -{ - // Init filters. - let filters = JSON.parse(sessionStorage.getItem('django.admin.filtersState')); - - if (!filters) { - filters = {}; - } - - Object.entries(filters).forEach(([key, value]) => { - const detailElement = document.querySelector(`[data-filter-title='${CSS.escape(key)}']`); - - // Check if the filter is present, it could be from other view. - if (detailElement) { - value ? detailElement.setAttribute('open', '') : detailElement.removeAttribute('open'); - } - }); - - // Save filter state when clicks. - const details = document.querySelectorAll('details'); - details.forEach(detail => { - detail.addEventListener('toggle', event => { - filters[`${event.target.dataset.filterTitle}`] = detail.open; - sessionStorage.setItem('django.admin.filtersState', JSON.stringify(filters)); - }); - }); -} diff --git a/static/admin/js/inlines.js b/static/admin/js/inlines.js deleted file mode 100644 index e9a1dfe..0000000 --- a/static/admin/js/inlines.js +++ /dev/null @@ -1,359 +0,0 @@ -/*global DateTimeShortcuts, SelectFilter*/ -/** - * Django admin inlines - * - * Based on jQuery Formset 1.1 - * @author Stanislaus Madueke (stan DOT madueke AT gmail DOT com) - * @requires jQuery 1.2.6 or later - * - * Copyright (c) 2009, Stanislaus Madueke - * All rights reserved. - * - * Spiced up with Code from Zain Memon's GSoC project 2009 - * and modified for Django by Jannis Leidel, Travis Swicegood and Julien Phalip. - * - * Licensed under the New BSD License - * See: https://opensource.org/licenses/bsd-license.php - */ -'use strict'; -{ - const $ = django.jQuery; - $.fn.formset = function(opts) { - const options = $.extend({}, $.fn.formset.defaults, opts); - const $this = $(this); - const $parent = $this.parent(); - const updateElementIndex = function(el, prefix, ndx) { - const id_regex = new RegExp("(" + prefix + "-(\\d+|__prefix__))"); - const replacement = prefix + "-" + ndx; - if ($(el).prop("for")) { - $(el).prop("for", $(el).prop("for").replace(id_regex, replacement)); - } - if (el.id) { - el.id = el.id.replace(id_regex, replacement); - } - if (el.name) { - el.name = el.name.replace(id_regex, replacement); - } - }; - const totalForms = $("#id_" + options.prefix + "-TOTAL_FORMS").prop("autocomplete", "off"); - let nextIndex = parseInt(totalForms.val(), 10); - const maxForms = $("#id_" + options.prefix + "-MAX_NUM_FORMS").prop("autocomplete", "off"); - const minForms = $("#id_" + options.prefix + "-MIN_NUM_FORMS").prop("autocomplete", "off"); - let addButton; - - /** - * The "Add another MyModel" button below the inline forms. - */ - const addInlineAddButton = function() { - if (addButton === null) { - if ($this.prop("tagName") === "TR") { - // If forms are laid out as table rows, insert the - // "add" button in a new table row: - const numCols = $this.eq(-1).children().length; - $parent.append('' + options.addText + ""); - addButton = $parent.find("tr:last a"); - } else { - // Otherwise, insert it immediately after the last form: - $this.filter(":last").after('"); - addButton = $this.filter(":last").next().find("a"); - } - } - addButton.on('click', addInlineClickHandler); - }; - - const addInlineClickHandler = function(e) { - e.preventDefault(); - const template = $("#" + options.prefix + "-empty"); - const row = template.clone(true); - row.removeClass(options.emptyCssClass) - .addClass(options.formCssClass) - .attr("id", options.prefix + "-" + nextIndex); - addInlineDeleteButton(row); - row.find("*").each(function() { - updateElementIndex(this, options.prefix, totalForms.val()); - }); - // Insert the new form when it has been fully edited. - row.insertBefore($(template)); - // Update number of total forms. - $(totalForms).val(parseInt(totalForms.val(), 10) + 1); - nextIndex += 1; - // Hide the add button if there's a limit and it's been reached. - if ((maxForms.val() !== '') && (maxForms.val() - totalForms.val()) <= 0) { - addButton.parent().hide(); - } - // Show the remove buttons if there are more than min_num. - toggleDeleteButtonVisibility(row.closest('.inline-group')); - - // Pass the new form to the post-add callback, if provided. - if (options.added) { - options.added(row); - } - row.get(0).dispatchEvent(new CustomEvent("formset:added", { - bubbles: true, - detail: { - formsetName: options.prefix - } - })); - }; - - /** - * The "X" button that is part of every unsaved inline. - * (When saved, it is replaced with a "Delete" checkbox.) - */ - const addInlineDeleteButton = function(row) { - if (row.is("tr")) { - // If the forms are laid out in table rows, insert - // the remove button into the last table cell: - row.children(":last").append('"); - } else if (row.is("ul") || row.is("ol")) { - // If they're laid out as an ordered/unordered list, - // insert an
    • after the last list item: - row.append('
    • ' + options.deleteText + "
    • "); - } else { - // Otherwise, just insert the remove button as the - // last child element of the form's container: - row.children(":first").append('' + options.deleteText + ""); - } - // Add delete handler for each row. - row.find("a." + options.deleteCssClass).on('click', inlineDeleteHandler.bind(this)); - }; - - const inlineDeleteHandler = function(e1) { - e1.preventDefault(); - const deleteButton = $(e1.target); - const row = deleteButton.closest('.' + options.formCssClass); - const inlineGroup = row.closest('.inline-group'); - // Remove the parent form containing this button, - // and also remove the relevant row with non-field errors: - const prevRow = row.prev(); - if (prevRow.length && prevRow.hasClass('row-form-errors')) { - prevRow.remove(); - } - row.remove(); - nextIndex -= 1; - // Pass the deleted form to the post-delete callback, if provided. - if (options.removed) { - options.removed(row); - } - document.dispatchEvent(new CustomEvent("formset:removed", { - detail: { - formsetName: options.prefix - } - })); - // Update the TOTAL_FORMS form count. - const forms = $("." + options.formCssClass); - $("#id_" + options.prefix + "-TOTAL_FORMS").val(forms.length); - // Show add button again once below maximum number. - if ((maxForms.val() === '') || (maxForms.val() - forms.length) > 0) { - addButton.parent().show(); - } - // Hide the remove buttons if at min_num. - toggleDeleteButtonVisibility(inlineGroup); - // Also, update names and ids for all remaining form controls so - // they remain in sequence: - let i, formCount; - const updateElementCallback = function() { - updateElementIndex(this, options.prefix, i); - }; - for (i = 0, formCount = forms.length; i < formCount; i++) { - updateElementIndex($(forms).get(i), options.prefix, i); - $(forms.get(i)).find("*").each(updateElementCallback); - } - }; - - const toggleDeleteButtonVisibility = function(inlineGroup) { - if ((minForms.val() !== '') && (minForms.val() - totalForms.val()) >= 0) { - inlineGroup.find('.inline-deletelink').hide(); - } else { - inlineGroup.find('.inline-deletelink').show(); - } - }; - - $this.each(function(i) { - $(this).not("." + options.emptyCssClass).addClass(options.formCssClass); - }); - - // Create the delete buttons for all unsaved inlines: - $this.filter('.' + options.formCssClass + ':not(.has_original):not(.' + options.emptyCssClass + ')').each(function() { - addInlineDeleteButton($(this)); - }); - toggleDeleteButtonVisibility($this); - - // Create the add button, initially hidden. - addButton = options.addButton; - addInlineAddButton(); - - // Show the add button if allowed to add more items. - // Note that max_num = None translates to a blank string. - const showAddButton = maxForms.val() === '' || (maxForms.val() - totalForms.val()) > 0; - if ($this.length && showAddButton) { - addButton.parent().show(); - } else { - addButton.parent().hide(); - } - - return this; - }; - - /* Setup plugin defaults */ - $.fn.formset.defaults = { - prefix: "form", // The form prefix for your django formset - addText: "add another", // Text for the add link - deleteText: "remove", // Text for the delete link - addCssClass: "add-row", // CSS class applied to the add link - deleteCssClass: "delete-row", // CSS class applied to the delete link - emptyCssClass: "empty-row", // CSS class applied to the empty row - formCssClass: "dynamic-form", // CSS class applied to each form in a formset - added: null, // Function called each time a new form is added - removed: null, // Function called each time a form is deleted - addButton: null // Existing add button to use - }; - - - // Tabular inlines --------------------------------------------------------- - $.fn.tabularFormset = function(selector, options) { - const $rows = $(this); - - const reinitDateTimeShortCuts = function() { - // Reinitialize the calendar and clock widgets by force - if (typeof DateTimeShortcuts !== "undefined") { - $(".datetimeshortcuts").remove(); - DateTimeShortcuts.init(); - } - }; - - const updateSelectFilter = function() { - // If any SelectFilter widgets are a part of the new form, - // instantiate a new SelectFilter instance for it. - if (typeof SelectFilter !== 'undefined') { - $('.selectfilter').each(function(index, value) { - SelectFilter.init(value.id, this.dataset.fieldName, false); - }); - $('.selectfilterstacked').each(function(index, value) { - SelectFilter.init(value.id, this.dataset.fieldName, true); - }); - } - }; - - const initPrepopulatedFields = function(row) { - row.find('.prepopulated_field').each(function() { - const field = $(this), - input = field.find('input, select, textarea'), - dependency_list = input.data('dependency_list') || [], - dependencies = []; - $.each(dependency_list, function(i, field_name) { - dependencies.push('#' + row.find('.field-' + field_name).find('input, select, textarea').attr('id')); - }); - if (dependencies.length) { - input.prepopulate(dependencies, input.attr('maxlength')); - } - }); - }; - - $rows.formset({ - prefix: options.prefix, - addText: options.addText, - formCssClass: "dynamic-" + options.prefix, - deleteCssClass: "inline-deletelink", - deleteText: options.deleteText, - emptyCssClass: "empty-form", - added: function(row) { - initPrepopulatedFields(row); - reinitDateTimeShortCuts(); - updateSelectFilter(); - }, - addButton: options.addButton - }); - - return $rows; - }; - - // Stacked inlines --------------------------------------------------------- - $.fn.stackedFormset = function(selector, options) { - const $rows = $(this); - const updateInlineLabel = function(row) { - $(selector).find(".inline_label").each(function(i) { - const count = i + 1; - $(this).html($(this).html().replace(/(#\d+)/g, "#" + count)); - }); - }; - - const reinitDateTimeShortCuts = function() { - // Reinitialize the calendar and clock widgets by force, yuck. - if (typeof DateTimeShortcuts !== "undefined") { - $(".datetimeshortcuts").remove(); - DateTimeShortcuts.init(); - } - }; - - const updateSelectFilter = function() { - // If any SelectFilter widgets were added, instantiate a new instance. - if (typeof SelectFilter !== "undefined") { - $(".selectfilter").each(function(index, value) { - SelectFilter.init(value.id, this.dataset.fieldName, false); - }); - $(".selectfilterstacked").each(function(index, value) { - SelectFilter.init(value.id, this.dataset.fieldName, true); - }); - } - }; - - const initPrepopulatedFields = function(row) { - row.find('.prepopulated_field').each(function() { - const field = $(this), - input = field.find('input, select, textarea'), - dependency_list = input.data('dependency_list') || [], - dependencies = []; - $.each(dependency_list, function(i, field_name) { - // Dependency in a fieldset. - let field_element = row.find('.form-row .field-' + field_name); - // Dependency without a fieldset. - if (!field_element.length) { - field_element = row.find('.form-row.field-' + field_name); - } - dependencies.push('#' + field_element.find('input, select, textarea').attr('id')); - }); - if (dependencies.length) { - input.prepopulate(dependencies, input.attr('maxlength')); - } - }); - }; - - $rows.formset({ - prefix: options.prefix, - addText: options.addText, - formCssClass: "dynamic-" + options.prefix, - deleteCssClass: "inline-deletelink", - deleteText: options.deleteText, - emptyCssClass: "empty-form", - removed: updateInlineLabel, - added: function(row) { - initPrepopulatedFields(row); - reinitDateTimeShortCuts(); - updateSelectFilter(); - updateInlineLabel(row); - }, - addButton: options.addButton - }); - - return $rows; - }; - - $(document).ready(function() { - $(".js-inline-admin-formset").each(function() { - const data = $(this).data(), - inlineOptions = data.inlineFormset; - let selector; - switch(data.inlineType) { - case "stacked": - selector = inlineOptions.name + "-group .inline-related"; - $(selector).stackedFormset(selector, inlineOptions.options); - break; - case "tabular": - selector = inlineOptions.name + "-group .tabular.inline-related tbody:first > tr.form-row"; - $(selector).tabularFormset(selector, inlineOptions.options); - break; - } - }); - }); -} diff --git a/static/admin/js/jquery.init.js b/static/admin/js/jquery.init.js deleted file mode 100644 index f40b27f..0000000 --- a/static/admin/js/jquery.init.js +++ /dev/null @@ -1,8 +0,0 @@ -/*global jQuery:false*/ -'use strict'; -/* Puts the included jQuery into our own namespace using noConflict and passing - * it 'true'. This ensures that the included jQuery doesn't pollute the global - * namespace (i.e. this preserves pre-existing values for both window.$ and - * window.jQuery). - */ -window.django = {jQuery: jQuery.noConflict(true)}; diff --git a/static/admin/js/nav_sidebar.js b/static/admin/js/nav_sidebar.js deleted file mode 100644 index 7e735db..0000000 --- a/static/admin/js/nav_sidebar.js +++ /dev/null @@ -1,79 +0,0 @@ -'use strict'; -{ - const toggleNavSidebar = document.getElementById('toggle-nav-sidebar'); - if (toggleNavSidebar !== null) { - const navSidebar = document.getElementById('nav-sidebar'); - const main = document.getElementById('main'); - let navSidebarIsOpen = localStorage.getItem('django.admin.navSidebarIsOpen'); - if (navSidebarIsOpen === null) { - navSidebarIsOpen = 'true'; - } - main.classList.toggle('shifted', navSidebarIsOpen === 'true'); - navSidebar.setAttribute('aria-expanded', navSidebarIsOpen); - - toggleNavSidebar.addEventListener('click', function() { - if (navSidebarIsOpen === 'true') { - navSidebarIsOpen = 'false'; - } else { - navSidebarIsOpen = 'true'; - } - localStorage.setItem('django.admin.navSidebarIsOpen', navSidebarIsOpen); - main.classList.toggle('shifted'); - navSidebar.setAttribute('aria-expanded', navSidebarIsOpen); - }); - } - - function initSidebarQuickFilter() { - const options = []; - const navSidebar = document.getElementById('nav-sidebar'); - if (!navSidebar) { - return; - } - navSidebar.querySelectorAll('th[scope=row] a').forEach((container) => { - options.push({title: container.innerHTML, node: container}); - }); - - function checkValue(event) { - let filterValue = event.target.value; - if (filterValue) { - filterValue = filterValue.toLowerCase(); - } - if (event.key === 'Escape') { - filterValue = ''; - event.target.value = ''; // clear input - } - let matches = false; - for (const o of options) { - let displayValue = ''; - if (filterValue) { - if (o.title.toLowerCase().indexOf(filterValue) === -1) { - displayValue = 'none'; - } else { - matches = true; - } - } - // show/hide parent - o.node.parentNode.parentNode.style.display = displayValue; - } - if (!filterValue || matches) { - event.target.classList.remove('no-results'); - } else { - event.target.classList.add('no-results'); - } - sessionStorage.setItem('django.admin.navSidebarFilterValue', filterValue); - } - - const nav = document.getElementById('nav-filter'); - nav.addEventListener('change', checkValue, false); - nav.addEventListener('input', checkValue, false); - nav.addEventListener('keyup', checkValue, false); - - const storedValue = sessionStorage.getItem('django.admin.navSidebarFilterValue'); - if (storedValue) { - nav.value = storedValue; - checkValue({target: nav, key: ''}); - } - } - window.initSidebarQuickFilter = initSidebarQuickFilter; - initSidebarQuickFilter(); -} diff --git a/static/admin/js/popup_response.js b/static/admin/js/popup_response.js deleted file mode 100644 index fecf0f4..0000000 --- a/static/admin/js/popup_response.js +++ /dev/null @@ -1,15 +0,0 @@ -'use strict'; -{ - const initData = JSON.parse(document.getElementById('django-admin-popup-response-constants').dataset.popupResponse); - switch(initData.action) { - case 'change': - opener.dismissChangeRelatedObjectPopup(window, initData.value, initData.obj, initData.new_value); - break; - case 'delete': - opener.dismissDeleteRelatedObjectPopup(window, initData.value); - break; - default: - opener.dismissAddRelatedObjectPopup(window, initData.value, initData.obj); - break; - } -} diff --git a/static/admin/js/prepopulate.js b/static/admin/js/prepopulate.js deleted file mode 100644 index 89e95ab..0000000 --- a/static/admin/js/prepopulate.js +++ /dev/null @@ -1,43 +0,0 @@ -/*global URLify*/ -'use strict'; -{ - const $ = django.jQuery; - $.fn.prepopulate = function(dependencies, maxLength, allowUnicode) { - /* - Depends on urlify.js - Populates a selected field with the values of the dependent fields, - URLifies and shortens the string. - dependencies - array of dependent fields ids - maxLength - maximum length of the URLify'd string - allowUnicode - Unicode support of the URLify'd string - */ - return this.each(function() { - const prepopulatedField = $(this); - - const populate = function() { - // Bail if the field's value has been changed by the user - if (prepopulatedField.data('_changed')) { - return; - } - - const values = []; - $.each(dependencies, function(i, field) { - field = $(field); - if (field.val().length > 0) { - values.push(field.val()); - } - }); - prepopulatedField.val(URLify(values.join(' '), maxLength, allowUnicode)); - }; - - prepopulatedField.data('_changed', false); - prepopulatedField.on('change', function() { - prepopulatedField.data('_changed', true); - }); - - if (!prepopulatedField.val()) { - $(dependencies.join(',')).on('keyup change focus', populate); - } - }); - }; -} diff --git a/static/admin/js/prepopulate_init.js b/static/admin/js/prepopulate_init.js deleted file mode 100644 index a58841f..0000000 --- a/static/admin/js/prepopulate_init.js +++ /dev/null @@ -1,15 +0,0 @@ -'use strict'; -{ - const $ = django.jQuery; - const fields = $('#django-admin-prepopulated-fields-constants').data('prepopulatedFields'); - $.each(fields, function(index, field) { - $( - '.empty-form .form-row .field-' + field.name + - ', .empty-form.form-row .field-' + field.name + - ', .empty-form .form-row.field-' + field.name - ).addClass('prepopulated_field'); - $(field.id).data('dependency_list', field.dependency_list).prepopulate( - field.dependency_ids, field.maxLength, field.allowUnicode - ); - }); -} diff --git a/static/admin/js/theme.js b/static/admin/js/theme.js deleted file mode 100644 index e79d375..0000000 --- a/static/admin/js/theme.js +++ /dev/null @@ -1,51 +0,0 @@ -'use strict'; -{ - function setTheme(mode) { - if (mode !== "light" && mode !== "dark" && mode !== "auto") { - console.error(`Got invalid theme mode: ${mode}. Resetting to auto.`); - mode = "auto"; - } - document.documentElement.dataset.theme = mode; - localStorage.setItem("theme", mode); - } - - function cycleTheme() { - const currentTheme = localStorage.getItem("theme") || "auto"; - const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches; - - if (prefersDark) { - // Auto (dark) -> Light -> Dark - if (currentTheme === "auto") { - setTheme("light"); - } else if (currentTheme === "light") { - setTheme("dark"); - } else { - setTheme("auto"); - } - } else { - // Auto (light) -> Dark -> Light - if (currentTheme === "auto") { - setTheme("dark"); - } else if (currentTheme === "dark") { - setTheme("light"); - } else { - setTheme("auto"); - } - } - } - - function initTheme() { - // set theme defined in localStorage if there is one, or fallback to auto mode - const currentTheme = localStorage.getItem("theme"); - currentTheme ? setTheme(currentTheme) : setTheme("auto"); - } - - window.addEventListener('load', function(_) { - const buttons = document.getElementsByClassName("theme-toggle"); - Array.from(buttons).forEach((btn) => { - btn.addEventListener("click", cycleTheme); - }); - }); - - initTheme(); -} diff --git a/static/admin/js/unusable_password_field.js b/static/admin/js/unusable_password_field.js deleted file mode 100644 index ec26238..0000000 --- a/static/admin/js/unusable_password_field.js +++ /dev/null @@ -1,29 +0,0 @@ -"use strict"; -// Fallback JS for browsers which do not support :has selector used in -// admin/css/unusable_password_fields.css -// Remove file once all supported browsers support :has selector -try { - // If browser does not support :has selector this will raise an error - document.querySelector("form:has(input)"); -} catch (error) { - console.log("Defaulting to javascript for usable password form management: " + error); - // JS replacement for unsupported :has selector - document.querySelectorAll('input[name="usable_password"]').forEach(option => { - option.addEventListener('change', function() { - const usablePassword = (this.value === "true" ? this.checked : !this.checked); - const submit1 = document.querySelector('input[type="submit"].set-password'); - const submit2 = document.querySelector('input[type="submit"].unset-password'); - const messages = document.querySelector('#id_unusable_warning'); - document.getElementById('id_password1').closest('.form-row').hidden = !usablePassword; - document.getElementById('id_password2').closest('.form-row').hidden = !usablePassword; - if (messages) { - messages.hidden = usablePassword; - } - if (submit1 && submit2) { - submit1.hidden = !usablePassword; - submit2.hidden = usablePassword; - } - }); - option.dispatchEvent(new Event('change')); - }); -} diff --git a/static/admin/js/urlify.js b/static/admin/js/urlify.js deleted file mode 100644 index 9fc0409..0000000 --- a/static/admin/js/urlify.js +++ /dev/null @@ -1,169 +0,0 @@ -/*global XRegExp*/ -'use strict'; -{ - const LATIN_MAP = { - 'À': 'A', 'Á': 'A', 'Â': 'A', 'Ã': 'A', 'Ä': 'A', 'Å': 'A', 'Æ': 'AE', - 'Ç': 'C', 'È': 'E', 'É': 'E', 'Ê': 'E', 'Ë': 'E', 'Ì': 'I', 'Í': 'I', - 'Î': 'I', 'Ï': 'I', 'Ð': 'D', 'Ñ': 'N', 'Ò': 'O', 'Ó': 'O', 'Ô': 'O', - 'Õ': 'O', 'Ö': 'O', 'Ő': 'O', 'Ø': 'O', 'Ù': 'U', 'Ú': 'U', 'Û': 'U', - 'Ü': 'U', 'Ű': 'U', 'Ý': 'Y', 'Þ': 'TH', 'Ÿ': 'Y', 'ß': 'ss', 'à': 'a', - 'á': 'a', 'â': 'a', 'ã': 'a', 'ä': 'a', 'å': 'a', 'æ': 'ae', 'ç': 'c', - 'è': 'e', 'é': 'e', 'ê': 'e', 'ë': 'e', 'ì': 'i', 'í': 'i', 'î': 'i', - 'ï': 'i', 'ð': 'd', 'ñ': 'n', 'ò': 'o', 'ó': 'o', 'ô': 'o', 'õ': 'o', - 'ö': 'o', 'ő': 'o', 'ø': 'o', 'ù': 'u', 'ú': 'u', 'û': 'u', 'ü': 'u', - 'ű': 'u', 'ý': 'y', 'þ': 'th', 'ÿ': 'y' - }; - const LATIN_SYMBOLS_MAP = { - '©': '(c)' - }; - const GREEK_MAP = { - 'α': 'a', 'β': 'b', 'γ': 'g', 'δ': 'd', 'ε': 'e', 'ζ': 'z', 'η': 'h', - 'θ': '8', 'ι': 'i', 'κ': 'k', 'λ': 'l', 'μ': 'm', 'ν': 'n', 'ξ': '3', - 'ο': 'o', 'π': 'p', 'ρ': 'r', 'σ': 's', 'τ': 't', 'υ': 'y', 'φ': 'f', - 'χ': 'x', 'ψ': 'ps', 'ω': 'w', 'ά': 'a', 'έ': 'e', 'ί': 'i', 'ό': 'o', - 'ύ': 'y', 'ή': 'h', 'ώ': 'w', 'ς': 's', 'ϊ': 'i', 'ΰ': 'y', 'ϋ': 'y', - 'ΐ': 'i', 'Α': 'A', 'Β': 'B', 'Γ': 'G', 'Δ': 'D', 'Ε': 'E', 'Ζ': 'Z', - 'Η': 'H', 'Θ': '8', 'Ι': 'I', 'Κ': 'K', 'Λ': 'L', 'Μ': 'M', 'Ν': 'N', - 'Ξ': '3', 'Ο': 'O', 'Π': 'P', 'Ρ': 'R', 'Σ': 'S', 'Τ': 'T', 'Υ': 'Y', - 'Φ': 'F', 'Χ': 'X', 'Ψ': 'PS', 'Ω': 'W', 'Ά': 'A', 'Έ': 'E', 'Ί': 'I', - 'Ό': 'O', 'Ύ': 'Y', 'Ή': 'H', 'Ώ': 'W', 'Ϊ': 'I', 'Ϋ': 'Y' - }; - const TURKISH_MAP = { - 'ş': 's', 'Ş': 'S', 'ı': 'i', 'İ': 'I', 'ç': 'c', 'Ç': 'C', 'ü': 'u', - 'Ü': 'U', 'ö': 'o', 'Ö': 'O', 'ğ': 'g', 'Ğ': 'G' - }; - const ROMANIAN_MAP = { - 'ă': 'a', 'î': 'i', 'ș': 's', 'ț': 't', 'â': 'a', - 'Ă': 'A', 'Î': 'I', 'Ș': 'S', 'Ț': 'T', 'Â': 'A' - }; - const RUSSIAN_MAP = { - 'а': 'a', 'б': 'b', 'в': 'v', 'г': 'g', 'д': 'd', 'е': 'e', 'ё': 'yo', - 'ж': 'zh', 'з': 'z', 'и': 'i', 'й': 'j', 'к': 'k', 'л': 'l', 'м': 'm', - 'н': 'n', 'о': 'o', 'п': 'p', 'р': 'r', 'с': 's', 'т': 't', 'у': 'u', - 'ф': 'f', 'х': 'h', 'ц': 'c', 'ч': 'ch', 'ш': 'sh', 'щ': 'sh', 'ъ': '', - 'ы': 'y', 'ь': '', 'э': 'e', 'ю': 'yu', 'я': 'ya', - 'А': 'A', 'Б': 'B', 'В': 'V', 'Г': 'G', 'Д': 'D', 'Е': 'E', 'Ё': 'Yo', - 'Ж': 'Zh', 'З': 'Z', 'И': 'I', 'Й': 'J', 'К': 'K', 'Л': 'L', 'М': 'M', - 'Н': 'N', 'О': 'O', 'П': 'P', 'Р': 'R', 'С': 'S', 'Т': 'T', 'У': 'U', - 'Ф': 'F', 'Х': 'H', 'Ц': 'C', 'Ч': 'Ch', 'Ш': 'Sh', 'Щ': 'Sh', 'Ъ': '', - 'Ы': 'Y', 'Ь': '', 'Э': 'E', 'Ю': 'Yu', 'Я': 'Ya' - }; - const UKRAINIAN_MAP = { - 'Є': 'Ye', 'І': 'I', 'Ї': 'Yi', 'Ґ': 'G', 'є': 'ye', 'і': 'i', - 'ї': 'yi', 'ґ': 'g' - }; - const CZECH_MAP = { - 'č': 'c', 'ď': 'd', 'ě': 'e', 'ň': 'n', 'ř': 'r', 'š': 's', 'ť': 't', - 'ů': 'u', 'ž': 'z', 'Č': 'C', 'Ď': 'D', 'Ě': 'E', 'Ň': 'N', 'Ř': 'R', - 'Š': 'S', 'Ť': 'T', 'Ů': 'U', 'Ž': 'Z' - }; - const SLOVAK_MAP = { - 'á': 'a', 'ä': 'a', 'č': 'c', 'ď': 'd', 'é': 'e', 'í': 'i', 'ľ': 'l', - 'ĺ': 'l', 'ň': 'n', 'ó': 'o', 'ô': 'o', 'ŕ': 'r', 'š': 's', 'ť': 't', - 'ú': 'u', 'ý': 'y', 'ž': 'z', - 'Á': 'a', 'Ä': 'A', 'Č': 'C', 'Ď': 'D', 'É': 'E', 'Í': 'I', 'Ľ': 'L', - 'Ĺ': 'L', 'Ň': 'N', 'Ó': 'O', 'Ô': 'O', 'Ŕ': 'R', 'Š': 'S', 'Ť': 'T', - 'Ú': 'U', 'Ý': 'Y', 'Ž': 'Z' - }; - const POLISH_MAP = { - 'ą': 'a', 'ć': 'c', 'ę': 'e', 'ł': 'l', 'ń': 'n', 'ó': 'o', 'ś': 's', - 'ź': 'z', 'ż': 'z', - 'Ą': 'A', 'Ć': 'C', 'Ę': 'E', 'Ł': 'L', 'Ń': 'N', 'Ó': 'O', 'Ś': 'S', - 'Ź': 'Z', 'Ż': 'Z' - }; - const LATVIAN_MAP = { - 'ā': 'a', 'č': 'c', 'ē': 'e', 'ģ': 'g', 'ī': 'i', 'ķ': 'k', 'ļ': 'l', - 'ņ': 'n', 'š': 's', 'ū': 'u', 'ž': 'z', - 'Ā': 'A', 'Č': 'C', 'Ē': 'E', 'Ģ': 'G', 'Ī': 'I', 'Ķ': 'K', 'Ļ': 'L', - 'Ņ': 'N', 'Š': 'S', 'Ū': 'U', 'Ž': 'Z' - }; - const ARABIC_MAP = { - 'أ': 'a', 'ب': 'b', 'ت': 't', 'ث': 'th', 'ج': 'g', 'ح': 'h', 'خ': 'kh', 'د': 'd', - 'ذ': 'th', 'ر': 'r', 'ز': 'z', 'س': 's', 'ش': 'sh', 'ص': 's', 'ض': 'd', 'ط': 't', - 'ظ': 'th', 'ع': 'aa', 'غ': 'gh', 'ف': 'f', 'ق': 'k', 'ك': 'k', 'ل': 'l', 'م': 'm', - 'ن': 'n', 'ه': 'h', 'و': 'o', 'ي': 'y' - }; - const LITHUANIAN_MAP = { - 'ą': 'a', 'č': 'c', 'ę': 'e', 'ė': 'e', 'į': 'i', 'š': 's', 'ų': 'u', - 'ū': 'u', 'ž': 'z', - 'Ą': 'A', 'Č': 'C', 'Ę': 'E', 'Ė': 'E', 'Į': 'I', 'Š': 'S', 'Ų': 'U', - 'Ū': 'U', 'Ž': 'Z' - }; - const SERBIAN_MAP = { - 'ђ': 'dj', 'ј': 'j', 'љ': 'lj', 'њ': 'nj', 'ћ': 'c', 'џ': 'dz', - 'đ': 'dj', 'Ђ': 'Dj', 'Ј': 'j', 'Љ': 'Lj', 'Њ': 'Nj', 'Ћ': 'C', - 'Џ': 'Dz', 'Đ': 'Dj' - }; - const AZERBAIJANI_MAP = { - 'ç': 'c', 'ə': 'e', 'ğ': 'g', 'ı': 'i', 'ö': 'o', 'ş': 's', 'ü': 'u', - 'Ç': 'C', 'Ə': 'E', 'Ğ': 'G', 'İ': 'I', 'Ö': 'O', 'Ş': 'S', 'Ü': 'U' - }; - const GEORGIAN_MAP = { - 'ა': 'a', 'ბ': 'b', 'გ': 'g', 'დ': 'd', 'ე': 'e', 'ვ': 'v', 'ზ': 'z', - 'თ': 't', 'ი': 'i', 'კ': 'k', 'ლ': 'l', 'მ': 'm', 'ნ': 'n', 'ო': 'o', - 'პ': 'p', 'ჟ': 'j', 'რ': 'r', 'ს': 's', 'ტ': 't', 'უ': 'u', 'ფ': 'f', - 'ქ': 'q', 'ღ': 'g', 'ყ': 'y', 'შ': 'sh', 'ჩ': 'ch', 'ც': 'c', 'ძ': 'dz', - 'წ': 'w', 'ჭ': 'ch', 'ხ': 'x', 'ჯ': 'j', 'ჰ': 'h' - }; - - const ALL_DOWNCODE_MAPS = [ - LATIN_MAP, - LATIN_SYMBOLS_MAP, - GREEK_MAP, - TURKISH_MAP, - ROMANIAN_MAP, - RUSSIAN_MAP, - UKRAINIAN_MAP, - CZECH_MAP, - SLOVAK_MAP, - POLISH_MAP, - LATVIAN_MAP, - ARABIC_MAP, - LITHUANIAN_MAP, - SERBIAN_MAP, - AZERBAIJANI_MAP, - GEORGIAN_MAP - ]; - - const Downcoder = { - 'Initialize': function() { - if (Downcoder.map) { // already made - return; - } - Downcoder.map = {}; - for (const lookup of ALL_DOWNCODE_MAPS) { - Object.assign(Downcoder.map, lookup); - } - Downcoder.regex = new RegExp(Object.keys(Downcoder.map).join('|'), 'g'); - } - }; - - function downcode(slug) { - Downcoder.Initialize(); - return slug.replace(Downcoder.regex, function(m) { - return Downcoder.map[m]; - }); - } - - - function URLify(s, num_chars, allowUnicode) { - // changes, e.g., "Petty theft" to "petty-theft" - if (!allowUnicode) { - s = downcode(s); - } - s = s.toLowerCase(); // convert to lowercase - // if downcode doesn't hit, the char will be stripped here - if (allowUnicode) { - // Keep Unicode letters including both lowercase and uppercase - // characters, whitespace, and dash; remove other characters. - s = XRegExp.replace(s, XRegExp('[^-_\\p{L}\\p{N}\\s]', 'g'), ''); - } else { - s = s.replace(/[^-\w\s]/g, ''); // remove unneeded chars - } - s = s.replace(/^\s+|\s+$/g, ''); // trim leading/trailing spaces - s = s.replace(/[-\s]+/g, '-'); // convert spaces to hyphens - s = s.substring(0, num_chars); // trim to first num_chars chars - return s.replace(/-+$/g, ''); // trim any trailing hyphens - } - window.URLify = URLify; -} diff --git a/static/admin/js/vendor/jquery/LICENSE.txt b/static/admin/js/vendor/jquery/LICENSE.txt deleted file mode 100644 index f642c3f..0000000 --- a/static/admin/js/vendor/jquery/LICENSE.txt +++ /dev/null @@ -1,20 +0,0 @@ -Copyright OpenJS Foundation and other contributors, https://openjsf.org/ - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/static/admin/js/vendor/jquery/jquery.js b/static/admin/js/vendor/jquery/jquery.js deleted file mode 100644 index 1a86433..0000000 --- a/static/admin/js/vendor/jquery/jquery.js +++ /dev/null @@ -1,10716 +0,0 @@ -/*! - * jQuery JavaScript Library v3.7.1 - * https://jquery.com/ - * - * Copyright OpenJS Foundation and other contributors - * Released under the MIT license - * https://jquery.org/license - * - * Date: 2023-08-28T13:37Z - */ -( function( global, factory ) { - - "use strict"; - - if ( typeof module === "object" && typeof module.exports === "object" ) { - - // For CommonJS and CommonJS-like environments where a proper `window` - // is present, execute the factory and get jQuery. - // For environments that do not have a `window` with a `document` - // (such as Node.js), expose a factory as module.exports. - // This accentuates the need for the creation of a real `window`. - // e.g. var jQuery = require("jquery")(window); - // See ticket trac-14549 for more info. - module.exports = global.document ? - factory( global, true ) : - function( w ) { - if ( !w.document ) { - throw new Error( "jQuery requires a window with a document" ); - } - return factory( w ); - }; - } else { - factory( global ); - } - -// Pass this if window is not defined yet -} )( typeof window !== "undefined" ? window : this, function( window, noGlobal ) { - -// Edge <= 12 - 13+, Firefox <=18 - 45+, IE 10 - 11, Safari 5.1 - 9+, iOS 6 - 9.1 -// throw exceptions when non-strict code (e.g., ASP.NET 4.5) accesses strict mode -// arguments.callee.caller (trac-13335). But as of jQuery 3.0 (2016), strict mode should be common -// enough that all such attempts are guarded in a try block. -"use strict"; - -var arr = []; - -var getProto = Object.getPrototypeOf; - -var slice = arr.slice; - -var flat = arr.flat ? function( array ) { - return arr.flat.call( array ); -} : function( array ) { - return arr.concat.apply( [], array ); -}; - - -var push = arr.push; - -var indexOf = arr.indexOf; - -var class2type = {}; - -var toString = class2type.toString; - -var hasOwn = class2type.hasOwnProperty; - -var fnToString = hasOwn.toString; - -var ObjectFunctionString = fnToString.call( Object ); - -var support = {}; - -var isFunction = function isFunction( obj ) { - - // Support: Chrome <=57, Firefox <=52 - // In some browsers, typeof returns "function" for HTML elements - // (i.e., `typeof document.createElement( "object" ) === "function"`). - // We don't want to classify *any* DOM node as a function. - // Support: QtWeb <=3.8.5, WebKit <=534.34, wkhtmltopdf tool <=0.12.5 - // Plus for old WebKit, typeof returns "function" for HTML collections - // (e.g., `typeof document.getElementsByTagName("div") === "function"`). (gh-4756) - return typeof obj === "function" && typeof obj.nodeType !== "number" && - typeof obj.item !== "function"; - }; - - -var isWindow = function isWindow( obj ) { - return obj != null && obj === obj.window; - }; - - -var document = window.document; - - - - var preservedScriptAttributes = { - type: true, - src: true, - nonce: true, - noModule: true - }; - - function DOMEval( code, node, doc ) { - doc = doc || document; - - var i, val, - script = doc.createElement( "script" ); - - script.text = code; - if ( node ) { - for ( i in preservedScriptAttributes ) { - - // Support: Firefox 64+, Edge 18+ - // Some browsers don't support the "nonce" property on scripts. - // On the other hand, just using `getAttribute` is not enough as - // the `nonce` attribute is reset to an empty string whenever it - // becomes browsing-context connected. - // See https://github.com/whatwg/html/issues/2369 - // See https://html.spec.whatwg.org/#nonce-attributes - // The `node.getAttribute` check was added for the sake of - // `jQuery.globalEval` so that it can fake a nonce-containing node - // via an object. - val = node[ i ] || node.getAttribute && node.getAttribute( i ); - if ( val ) { - script.setAttribute( i, val ); - } - } - } - doc.head.appendChild( script ).parentNode.removeChild( script ); - } - - -function toType( obj ) { - if ( obj == null ) { - return obj + ""; - } - - // Support: Android <=2.3 only (functionish RegExp) - return typeof obj === "object" || typeof obj === "function" ? - class2type[ toString.call( obj ) ] || "object" : - typeof obj; -} -/* global Symbol */ -// Defining this global in .eslintrc.json would create a danger of using the global -// unguarded in another place, it seems safer to define global only for this module - - - -var version = "3.7.1", - - rhtmlSuffix = /HTML$/i, - - // Define a local copy of jQuery - jQuery = function( selector, context ) { - - // The jQuery object is actually just the init constructor 'enhanced' - // Need init if jQuery is called (just allow error to be thrown if not included) - return new jQuery.fn.init( selector, context ); - }; - -jQuery.fn = jQuery.prototype = { - - // The current version of jQuery being used - jquery: version, - - constructor: jQuery, - - // The default length of a jQuery object is 0 - length: 0, - - toArray: function() { - return slice.call( this ); - }, - - // Get the Nth element in the matched element set OR - // Get the whole matched element set as a clean array - get: function( num ) { - - // Return all the elements in a clean array - if ( num == null ) { - return slice.call( this ); - } - - // Return just the one element from the set - return num < 0 ? this[ num + this.length ] : this[ num ]; - }, - - // Take an array of elements and push it onto the stack - // (returning the new matched element set) - pushStack: function( elems ) { - - // Build a new jQuery matched element set - var ret = jQuery.merge( this.constructor(), elems ); - - // Add the old object onto the stack (as a reference) - ret.prevObject = this; - - // Return the newly-formed element set - return ret; - }, - - // Execute a callback for every element in the matched set. - each: function( callback ) { - return jQuery.each( this, callback ); - }, - - map: function( callback ) { - return this.pushStack( jQuery.map( this, function( elem, i ) { - return callback.call( elem, i, elem ); - } ) ); - }, - - slice: function() { - return this.pushStack( slice.apply( this, arguments ) ); - }, - - first: function() { - return this.eq( 0 ); - }, - - last: function() { - return this.eq( -1 ); - }, - - even: function() { - return this.pushStack( jQuery.grep( this, function( _elem, i ) { - return ( i + 1 ) % 2; - } ) ); - }, - - odd: function() { - return this.pushStack( jQuery.grep( this, function( _elem, i ) { - return i % 2; - } ) ); - }, - - eq: function( i ) { - var len = this.length, - j = +i + ( i < 0 ? len : 0 ); - return this.pushStack( j >= 0 && j < len ? [ this[ j ] ] : [] ); - }, - - end: function() { - return this.prevObject || this.constructor(); - }, - - // For internal use only. - // Behaves like an Array's method, not like a jQuery method. - push: push, - sort: arr.sort, - splice: arr.splice -}; - -jQuery.extend = jQuery.fn.extend = function() { - var options, name, src, copy, copyIsArray, clone, - target = arguments[ 0 ] || {}, - i = 1, - length = arguments.length, - deep = false; - - // Handle a deep copy situation - if ( typeof target === "boolean" ) { - deep = target; - - // Skip the boolean and the target - target = arguments[ i ] || {}; - i++; - } - - // Handle case when target is a string or something (possible in deep copy) - if ( typeof target !== "object" && !isFunction( target ) ) { - target = {}; - } - - // Extend jQuery itself if only one argument is passed - if ( i === length ) { - target = this; - i--; - } - - for ( ; i < length; i++ ) { - - // Only deal with non-null/undefined values - if ( ( options = arguments[ i ] ) != null ) { - - // Extend the base object - for ( name in options ) { - copy = options[ name ]; - - // Prevent Object.prototype pollution - // Prevent never-ending loop - if ( name === "__proto__" || target === copy ) { - continue; - } - - // Recurse if we're merging plain objects or arrays - if ( deep && copy && ( jQuery.isPlainObject( copy ) || - ( copyIsArray = Array.isArray( copy ) ) ) ) { - src = target[ name ]; - - // Ensure proper type for the source value - if ( copyIsArray && !Array.isArray( src ) ) { - clone = []; - } else if ( !copyIsArray && !jQuery.isPlainObject( src ) ) { - clone = {}; - } else { - clone = src; - } - copyIsArray = false; - - // Never move original objects, clone them - target[ name ] = jQuery.extend( deep, clone, copy ); - - // Don't bring in undefined values - } else if ( copy !== undefined ) { - target[ name ] = copy; - } - } - } - } - - // Return the modified object - return target; -}; - -jQuery.extend( { - - // Unique for each copy of jQuery on the page - expando: "jQuery" + ( version + Math.random() ).replace( /\D/g, "" ), - - // Assume jQuery is ready without the ready module - isReady: true, - - error: function( msg ) { - throw new Error( msg ); - }, - - noop: function() {}, - - isPlainObject: function( obj ) { - var proto, Ctor; - - // Detect obvious negatives - // Use toString instead of jQuery.type to catch host objects - if ( !obj || toString.call( obj ) !== "[object Object]" ) { - return false; - } - - proto = getProto( obj ); - - // Objects with no prototype (e.g., `Object.create( null )`) are plain - if ( !proto ) { - return true; - } - - // Objects with prototype are plain iff they were constructed by a global Object function - Ctor = hasOwn.call( proto, "constructor" ) && proto.constructor; - return typeof Ctor === "function" && fnToString.call( Ctor ) === ObjectFunctionString; - }, - - isEmptyObject: function( obj ) { - var name; - - for ( name in obj ) { - return false; - } - return true; - }, - - // Evaluates a script in a provided context; falls back to the global one - // if not specified. - globalEval: function( code, options, doc ) { - DOMEval( code, { nonce: options && options.nonce }, doc ); - }, - - each: function( obj, callback ) { - var length, i = 0; - - if ( isArrayLike( obj ) ) { - length = obj.length; - for ( ; i < length; i++ ) { - if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) { - break; - } - } - } else { - for ( i in obj ) { - if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) { - break; - } - } - } - - return obj; - }, - - - // Retrieve the text value of an array of DOM nodes - text: function( elem ) { - var node, - ret = "", - i = 0, - nodeType = elem.nodeType; - - if ( !nodeType ) { - - // If no nodeType, this is expected to be an array - while ( ( node = elem[ i++ ] ) ) { - - // Do not traverse comment nodes - ret += jQuery.text( node ); - } - } - if ( nodeType === 1 || nodeType === 11 ) { - return elem.textContent; - } - if ( nodeType === 9 ) { - return elem.documentElement.textContent; - } - if ( nodeType === 3 || nodeType === 4 ) { - return elem.nodeValue; - } - - // Do not include comment or processing instruction nodes - - return ret; - }, - - // results is for internal usage only - makeArray: function( arr, results ) { - var ret = results || []; - - if ( arr != null ) { - if ( isArrayLike( Object( arr ) ) ) { - jQuery.merge( ret, - typeof arr === "string" ? - [ arr ] : arr - ); - } else { - push.call( ret, arr ); - } - } - - return ret; - }, - - inArray: function( elem, arr, i ) { - return arr == null ? -1 : indexOf.call( arr, elem, i ); - }, - - isXMLDoc: function( elem ) { - var namespace = elem && elem.namespaceURI, - docElem = elem && ( elem.ownerDocument || elem ).documentElement; - - // Assume HTML when documentElement doesn't yet exist, such as inside - // document fragments. - return !rhtmlSuffix.test( namespace || docElem && docElem.nodeName || "HTML" ); - }, - - // Support: Android <=4.0 only, PhantomJS 1 only - // push.apply(_, arraylike) throws on ancient WebKit - merge: function( first, second ) { - var len = +second.length, - j = 0, - i = first.length; - - for ( ; j < len; j++ ) { - first[ i++ ] = second[ j ]; - } - - first.length = i; - - return first; - }, - - grep: function( elems, callback, invert ) { - var callbackInverse, - matches = [], - i = 0, - length = elems.length, - callbackExpect = !invert; - - // Go through the array, only saving the items - // that pass the validator function - for ( ; i < length; i++ ) { - callbackInverse = !callback( elems[ i ], i ); - if ( callbackInverse !== callbackExpect ) { - matches.push( elems[ i ] ); - } - } - - return matches; - }, - - // arg is for internal usage only - map: function( elems, callback, arg ) { - var length, value, - i = 0, - ret = []; - - // Go through the array, translating each of the items to their new values - if ( isArrayLike( elems ) ) { - length = elems.length; - for ( ; i < length; i++ ) { - value = callback( elems[ i ], i, arg ); - - if ( value != null ) { - ret.push( value ); - } - } - - // Go through every key on the object, - } else { - for ( i in elems ) { - value = callback( elems[ i ], i, arg ); - - if ( value != null ) { - ret.push( value ); - } - } - } - - // Flatten any nested arrays - return flat( ret ); - }, - - // A global GUID counter for objects - guid: 1, - - // jQuery.support is not used in Core but other projects attach their - // properties to it so it needs to exist. - support: support -} ); - -if ( typeof Symbol === "function" ) { - jQuery.fn[ Symbol.iterator ] = arr[ Symbol.iterator ]; -} - -// Populate the class2type map -jQuery.each( "Boolean Number String Function Array Date RegExp Object Error Symbol".split( " " ), - function( _i, name ) { - class2type[ "[object " + name + "]" ] = name.toLowerCase(); - } ); - -function isArrayLike( obj ) { - - // Support: real iOS 8.2 only (not reproducible in simulator) - // `in` check used to prevent JIT error (gh-2145) - // hasOwn isn't used here due to false negatives - // regarding Nodelist length in IE - var length = !!obj && "length" in obj && obj.length, - type = toType( obj ); - - if ( isFunction( obj ) || isWindow( obj ) ) { - return false; - } - - return type === "array" || length === 0 || - typeof length === "number" && length > 0 && ( length - 1 ) in obj; -} - - -function nodeName( elem, name ) { - - return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase(); - -} -var pop = arr.pop; - - -var sort = arr.sort; - - -var splice = arr.splice; - - -var whitespace = "[\\x20\\t\\r\\n\\f]"; - - -var rtrimCSS = new RegExp( - "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", - "g" -); - - - - -// Note: an element does not contain itself -jQuery.contains = function( a, b ) { - var bup = b && b.parentNode; - - return a === bup || !!( bup && bup.nodeType === 1 && ( - - // Support: IE 9 - 11+ - // IE doesn't have `contains` on SVG. - a.contains ? - a.contains( bup ) : - a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16 - ) ); -}; - - - - -// CSS string/identifier serialization -// https://drafts.csswg.org/cssom/#common-serializing-idioms -var rcssescape = /([\0-\x1f\x7f]|^-?\d)|^-$|[^\x80-\uFFFF\w-]/g; - -function fcssescape( ch, asCodePoint ) { - if ( asCodePoint ) { - - // U+0000 NULL becomes U+FFFD REPLACEMENT CHARACTER - if ( ch === "\0" ) { - return "\uFFFD"; - } - - // Control characters and (dependent upon position) numbers get escaped as code points - return ch.slice( 0, -1 ) + "\\" + ch.charCodeAt( ch.length - 1 ).toString( 16 ) + " "; - } - - // Other potentially-special ASCII characters get backslash-escaped - return "\\" + ch; -} - -jQuery.escapeSelector = function( sel ) { - return ( sel + "" ).replace( rcssescape, fcssescape ); -}; - - - - -var preferredDoc = document, - pushNative = push; - -( function() { - -var i, - Expr, - outermostContext, - sortInput, - hasDuplicate, - push = pushNative, - - // Local document vars - document, - documentElement, - documentIsHTML, - rbuggyQSA, - matches, - - // Instance-specific data - expando = jQuery.expando, - dirruns = 0, - done = 0, - classCache = createCache(), - tokenCache = createCache(), - compilerCache = createCache(), - nonnativeSelectorCache = createCache(), - sortOrder = function( a, b ) { - if ( a === b ) { - hasDuplicate = true; - } - return 0; - }, - - booleans = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|" + - "loop|multiple|open|readonly|required|scoped", - - // Regular expressions - - // https://www.w3.org/TR/css-syntax-3/#ident-token-diagram - identifier = "(?:\\\\[\\da-fA-F]{1,6}" + whitespace + - "?|\\\\[^\\r\\n\\f]|[\\w-]|[^\0-\\x7f])+", - - // Attribute selectors: https://www.w3.org/TR/selectors/#attribute-selectors - attributes = "\\[" + whitespace + "*(" + identifier + ")(?:" + whitespace + - - // Operator (capture 2) - "*([*^$|!~]?=)" + whitespace + - - // "Attribute values must be CSS identifiers [capture 5] or strings [capture 3 or capture 4]" - "*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|(" + identifier + "))|)" + - whitespace + "*\\]", - - pseudos = ":(" + identifier + ")(?:\\((" + - - // To reduce the number of selectors needing tokenize in the preFilter, prefer arguments: - // 1. quoted (capture 3; capture 4 or capture 5) - "('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|" + - - // 2. simple (capture 6) - "((?:\\\\.|[^\\\\()[\\]]|" + attributes + ")*)|" + - - // 3. anything else (capture 2) - ".*" + - ")\\)|)", - - // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter - rwhitespace = new RegExp( whitespace + "+", "g" ), - - rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ), - rleadingCombinator = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + - whitespace + "*" ), - rdescend = new RegExp( whitespace + "|>" ), - - rpseudo = new RegExp( pseudos ), - ridentifier = new RegExp( "^" + identifier + "$" ), - - matchExpr = { - ID: new RegExp( "^#(" + identifier + ")" ), - CLASS: new RegExp( "^\\.(" + identifier + ")" ), - TAG: new RegExp( "^(" + identifier + "|[*])" ), - ATTR: new RegExp( "^" + attributes ), - PSEUDO: new RegExp( "^" + pseudos ), - CHILD: new RegExp( - "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + - whitespace + "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + - whitespace + "*(\\d+)|))" + whitespace + "*\\)|)", "i" ), - bool: new RegExp( "^(?:" + booleans + ")$", "i" ), - - // For use in libraries implementing .is() - // We use this for POS matching in `select` - needsContext: new RegExp( "^" + whitespace + - "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + whitespace + - "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" ) - }, - - rinputs = /^(?:input|select|textarea|button)$/i, - rheader = /^h\d$/i, - - // Easily-parseable/retrievable ID or TAG or CLASS selectors - rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/, - - rsibling = /[+~]/, - - // CSS escapes - // https://www.w3.org/TR/CSS21/syndata.html#escaped-characters - runescape = new RegExp( "\\\\[\\da-fA-F]{1,6}" + whitespace + - "?|\\\\([^\\r\\n\\f])", "g" ), - funescape = function( escape, nonHex ) { - var high = "0x" + escape.slice( 1 ) - 0x10000; - - if ( nonHex ) { - - // Strip the backslash prefix from a non-hex escape sequence - return nonHex; - } - - // Replace a hexadecimal escape sequence with the encoded Unicode code point - // Support: IE <=11+ - // For values outside the Basic Multilingual Plane (BMP), manually construct a - // surrogate pair - return high < 0 ? - String.fromCharCode( high + 0x10000 ) : - String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 ); - }, - - // Used for iframes; see `setDocument`. - // Support: IE 9 - 11+, Edge 12 - 18+ - // Removing the function wrapper causes a "Permission Denied" - // error in IE/Edge. - unloadHandler = function() { - setDocument(); - }, - - inDisabledFieldset = addCombinator( - function( elem ) { - return elem.disabled === true && nodeName( elem, "fieldset" ); - }, - { dir: "parentNode", next: "legend" } - ); - -// Support: IE <=9 only -// Accessing document.activeElement can throw unexpectedly -// https://bugs.jquery.com/ticket/13393 -function safeActiveElement() { - try { - return document.activeElement; - } catch ( err ) { } -} - -// Optimize for push.apply( _, NodeList ) -try { - push.apply( - ( arr = slice.call( preferredDoc.childNodes ) ), - preferredDoc.childNodes - ); - - // Support: Android <=4.0 - // Detect silently failing push.apply - // eslint-disable-next-line no-unused-expressions - arr[ preferredDoc.childNodes.length ].nodeType; -} catch ( e ) { - push = { - apply: function( target, els ) { - pushNative.apply( target, slice.call( els ) ); - }, - call: function( target ) { - pushNative.apply( target, slice.call( arguments, 1 ) ); - } - }; -} - -function find( selector, context, results, seed ) { - var m, i, elem, nid, match, groups, newSelector, - newContext = context && context.ownerDocument, - - // nodeType defaults to 9, since context defaults to document - nodeType = context ? context.nodeType : 9; - - results = results || []; - - // Return early from calls with invalid selector or context - if ( typeof selector !== "string" || !selector || - nodeType !== 1 && nodeType !== 9 && nodeType !== 11 ) { - - return results; - } - - // Try to shortcut find operations (as opposed to filters) in HTML documents - if ( !seed ) { - setDocument( context ); - context = context || document; - - if ( documentIsHTML ) { - - // If the selector is sufficiently simple, try using a "get*By*" DOM method - // (excepting DocumentFragment context, where the methods don't exist) - if ( nodeType !== 11 && ( match = rquickExpr.exec( selector ) ) ) { - - // ID selector - if ( ( m = match[ 1 ] ) ) { - - // Document context - if ( nodeType === 9 ) { - if ( ( elem = context.getElementById( m ) ) ) { - - // Support: IE 9 only - // getElementById can match elements by name instead of ID - if ( elem.id === m ) { - push.call( results, elem ); - return results; - } - } else { - return results; - } - - // Element context - } else { - - // Support: IE 9 only - // getElementById can match elements by name instead of ID - if ( newContext && ( elem = newContext.getElementById( m ) ) && - find.contains( context, elem ) && - elem.id === m ) { - - push.call( results, elem ); - return results; - } - } - - // Type selector - } else if ( match[ 2 ] ) { - push.apply( results, context.getElementsByTagName( selector ) ); - return results; - - // Class selector - } else if ( ( m = match[ 3 ] ) && context.getElementsByClassName ) { - push.apply( results, context.getElementsByClassName( m ) ); - return results; - } - } - - // Take advantage of querySelectorAll - if ( !nonnativeSelectorCache[ selector + " " ] && - ( !rbuggyQSA || !rbuggyQSA.test( selector ) ) ) { - - newSelector = selector; - newContext = context; - - // qSA considers elements outside a scoping root when evaluating child or - // descendant combinators, which is not what we want. - // In such cases, we work around the behavior by prefixing every selector in the - // list with an ID selector referencing the scope context. - // The technique has to be used as well when a leading combinator is used - // as such selectors are not recognized by querySelectorAll. - // Thanks to Andrew Dupont for this technique. - if ( nodeType === 1 && - ( rdescend.test( selector ) || rleadingCombinator.test( selector ) ) ) { - - // Expand context for sibling selectors - newContext = rsibling.test( selector ) && testContext( context.parentNode ) || - context; - - // We can use :scope instead of the ID hack if the browser - // supports it & if we're not changing the context. - // Support: IE 11+, Edge 17 - 18+ - // IE/Edge sometimes throw a "Permission denied" error when - // strict-comparing two documents; shallow comparisons work. - // eslint-disable-next-line eqeqeq - if ( newContext != context || !support.scope ) { - - // Capture the context ID, setting it first if necessary - if ( ( nid = context.getAttribute( "id" ) ) ) { - nid = jQuery.escapeSelector( nid ); - } else { - context.setAttribute( "id", ( nid = expando ) ); - } - } - - // Prefix every selector in the list - groups = tokenize( selector ); - i = groups.length; - while ( i-- ) { - groups[ i ] = ( nid ? "#" + nid : ":scope" ) + " " + - toSelector( groups[ i ] ); - } - newSelector = groups.join( "," ); - } - - try { - push.apply( results, - newContext.querySelectorAll( newSelector ) - ); - return results; - } catch ( qsaError ) { - nonnativeSelectorCache( selector, true ); - } finally { - if ( nid === expando ) { - context.removeAttribute( "id" ); - } - } - } - } - } - - // All others - return select( selector.replace( rtrimCSS, "$1" ), context, results, seed ); -} - -/** - * Create key-value caches of limited size - * @returns {function(string, object)} Returns the Object data after storing it on itself with - * property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength) - * deleting the oldest entry - */ -function createCache() { - var keys = []; - - function cache( key, value ) { - - // Use (key + " ") to avoid collision with native prototype properties - // (see https://github.com/jquery/sizzle/issues/157) - if ( keys.push( key + " " ) > Expr.cacheLength ) { - - // Only keep the most recent entries - delete cache[ keys.shift() ]; - } - return ( cache[ key + " " ] = value ); - } - return cache; -} - -/** - * Mark a function for special use by jQuery selector module - * @param {Function} fn The function to mark - */ -function markFunction( fn ) { - fn[ expando ] = true; - return fn; -} - -/** - * Support testing using an element - * @param {Function} fn Passed the created element and returns a boolean result - */ -function assert( fn ) { - var el = document.createElement( "fieldset" ); - - try { - return !!fn( el ); - } catch ( e ) { - return false; - } finally { - - // Remove from its parent by default - if ( el.parentNode ) { - el.parentNode.removeChild( el ); - } - - // release memory in IE - el = null; - } -} - -/** - * Returns a function to use in pseudos for input types - * @param {String} type - */ -function createInputPseudo( type ) { - return function( elem ) { - return nodeName( elem, "input" ) && elem.type === type; - }; -} - -/** - * Returns a function to use in pseudos for buttons - * @param {String} type - */ -function createButtonPseudo( type ) { - return function( elem ) { - return ( nodeName( elem, "input" ) || nodeName( elem, "button" ) ) && - elem.type === type; - }; -} - -/** - * Returns a function to use in pseudos for :enabled/:disabled - * @param {Boolean} disabled true for :disabled; false for :enabled - */ -function createDisabledPseudo( disabled ) { - - // Known :disabled false positives: fieldset[disabled] > legend:nth-of-type(n+2) :can-disable - return function( elem ) { - - // Only certain elements can match :enabled or :disabled - // https://html.spec.whatwg.org/multipage/scripting.html#selector-enabled - // https://html.spec.whatwg.org/multipage/scripting.html#selector-disabled - if ( "form" in elem ) { - - // Check for inherited disabledness on relevant non-disabled elements: - // * listed form-associated elements in a disabled fieldset - // https://html.spec.whatwg.org/multipage/forms.html#category-listed - // https://html.spec.whatwg.org/multipage/forms.html#concept-fe-disabled - // * option elements in a disabled optgroup - // https://html.spec.whatwg.org/multipage/forms.html#concept-option-disabled - // All such elements have a "form" property. - if ( elem.parentNode && elem.disabled === false ) { - - // Option elements defer to a parent optgroup if present - if ( "label" in elem ) { - if ( "label" in elem.parentNode ) { - return elem.parentNode.disabled === disabled; - } else { - return elem.disabled === disabled; - } - } - - // Support: IE 6 - 11+ - // Use the isDisabled shortcut property to check for disabled fieldset ancestors - return elem.isDisabled === disabled || - - // Where there is no isDisabled, check manually - elem.isDisabled !== !disabled && - inDisabledFieldset( elem ) === disabled; - } - - return elem.disabled === disabled; - - // Try to winnow out elements that can't be disabled before trusting the disabled property. - // Some victims get caught in our net (label, legend, menu, track), but it shouldn't - // even exist on them, let alone have a boolean value. - } else if ( "label" in elem ) { - return elem.disabled === disabled; - } - - // Remaining elements are neither :enabled nor :disabled - return false; - }; -} - -/** - * Returns a function to use in pseudos for positionals - * @param {Function} fn - */ -function createPositionalPseudo( fn ) { - return markFunction( function( argument ) { - argument = +argument; - return markFunction( function( seed, matches ) { - var j, - matchIndexes = fn( [], seed.length, argument ), - i = matchIndexes.length; - - // Match elements found at the specified indexes - while ( i-- ) { - if ( seed[ ( j = matchIndexes[ i ] ) ] ) { - seed[ j ] = !( matches[ j ] = seed[ j ] ); - } - } - } ); - } ); -} - -/** - * Checks a node for validity as a jQuery selector context - * @param {Element|Object=} context - * @returns {Element|Object|Boolean} The input node if acceptable, otherwise a falsy value - */ -function testContext( context ) { - return context && typeof context.getElementsByTagName !== "undefined" && context; -} - -/** - * Sets document-related variables once based on the current document - * @param {Element|Object} [node] An element or document object to use to set the document - * @returns {Object} Returns the current document - */ -function setDocument( node ) { - var subWindow, - doc = node ? node.ownerDocument || node : preferredDoc; - - // Return early if doc is invalid or already selected - // Support: IE 11+, Edge 17 - 18+ - // IE/Edge sometimes throw a "Permission denied" error when strict-comparing - // two documents; shallow comparisons work. - // eslint-disable-next-line eqeqeq - if ( doc == document || doc.nodeType !== 9 || !doc.documentElement ) { - return document; - } - - // Update global variables - document = doc; - documentElement = document.documentElement; - documentIsHTML = !jQuery.isXMLDoc( document ); - - // Support: iOS 7 only, IE 9 - 11+ - // Older browsers didn't support unprefixed `matches`. - matches = documentElement.matches || - documentElement.webkitMatchesSelector || - documentElement.msMatchesSelector; - - // Support: IE 9 - 11+, Edge 12 - 18+ - // Accessing iframe documents after unload throws "permission denied" errors - // (see trac-13936). - // Limit the fix to IE & Edge Legacy; despite Edge 15+ implementing `matches`, - // all IE 9+ and Edge Legacy versions implement `msMatchesSelector` as well. - if ( documentElement.msMatchesSelector && - - // Support: IE 11+, Edge 17 - 18+ - // IE/Edge sometimes throw a "Permission denied" error when strict-comparing - // two documents; shallow comparisons work. - // eslint-disable-next-line eqeqeq - preferredDoc != document && - ( subWindow = document.defaultView ) && subWindow.top !== subWindow ) { - - // Support: IE 9 - 11+, Edge 12 - 18+ - subWindow.addEventListener( "unload", unloadHandler ); - } - - // Support: IE <10 - // Check if getElementById returns elements by name - // The broken getElementById methods don't pick up programmatically-set names, - // so use a roundabout getElementsByName test - support.getById = assert( function( el ) { - documentElement.appendChild( el ).id = jQuery.expando; - return !document.getElementsByName || - !document.getElementsByName( jQuery.expando ).length; - } ); - - // Support: IE 9 only - // Check to see if it's possible to do matchesSelector - // on a disconnected node. - support.disconnectedMatch = assert( function( el ) { - return matches.call( el, "*" ); - } ); - - // Support: IE 9 - 11+, Edge 12 - 18+ - // IE/Edge don't support the :scope pseudo-class. - support.scope = assert( function() { - return document.querySelectorAll( ":scope" ); - } ); - - // Support: Chrome 105 - 111 only, Safari 15.4 - 16.3 only - // Make sure the `:has()` argument is parsed unforgivingly. - // We include `*` in the test to detect buggy implementations that are - // _selectively_ forgiving (specifically when the list includes at least - // one valid selector). - // Note that we treat complete lack of support for `:has()` as if it were - // spec-compliant support, which is fine because use of `:has()` in such - // environments will fail in the qSA path and fall back to jQuery traversal - // anyway. - support.cssHas = assert( function() { - try { - document.querySelector( ":has(*,:jqfake)" ); - return false; - } catch ( e ) { - return true; - } - } ); - - // ID filter and find - if ( support.getById ) { - Expr.filter.ID = function( id ) { - var attrId = id.replace( runescape, funescape ); - return function( elem ) { - return elem.getAttribute( "id" ) === attrId; - }; - }; - Expr.find.ID = function( id, context ) { - if ( typeof context.getElementById !== "undefined" && documentIsHTML ) { - var elem = context.getElementById( id ); - return elem ? [ elem ] : []; - } - }; - } else { - Expr.filter.ID = function( id ) { - var attrId = id.replace( runescape, funescape ); - return function( elem ) { - var node = typeof elem.getAttributeNode !== "undefined" && - elem.getAttributeNode( "id" ); - return node && node.value === attrId; - }; - }; - - // Support: IE 6 - 7 only - // getElementById is not reliable as a find shortcut - Expr.find.ID = function( id, context ) { - if ( typeof context.getElementById !== "undefined" && documentIsHTML ) { - var node, i, elems, - elem = context.getElementById( id ); - - if ( elem ) { - - // Verify the id attribute - node = elem.getAttributeNode( "id" ); - if ( node && node.value === id ) { - return [ elem ]; - } - - // Fall back on getElementsByName - elems = context.getElementsByName( id ); - i = 0; - while ( ( elem = elems[ i++ ] ) ) { - node = elem.getAttributeNode( "id" ); - if ( node && node.value === id ) { - return [ elem ]; - } - } - } - - return []; - } - }; - } - - // Tag - Expr.find.TAG = function( tag, context ) { - if ( typeof context.getElementsByTagName !== "undefined" ) { - return context.getElementsByTagName( tag ); - - // DocumentFragment nodes don't have gEBTN - } else { - return context.querySelectorAll( tag ); - } - }; - - // Class - Expr.find.CLASS = function( className, context ) { - if ( typeof context.getElementsByClassName !== "undefined" && documentIsHTML ) { - return context.getElementsByClassName( className ); - } - }; - - /* QSA/matchesSelector - ---------------------------------------------------------------------- */ - - // QSA and matchesSelector support - - rbuggyQSA = []; - - // Build QSA regex - // Regex strategy adopted from Diego Perini - assert( function( el ) { - - var input; - - documentElement.appendChild( el ).innerHTML = - "" + - ""; - - // Support: iOS <=7 - 8 only - // Boolean attributes and "value" are not treated correctly in some XML documents - if ( !el.querySelectorAll( "[selected]" ).length ) { - rbuggyQSA.push( "\\[" + whitespace + "*(?:value|" + booleans + ")" ); - } - - // Support: iOS <=7 - 8 only - if ( !el.querySelectorAll( "[id~=" + expando + "-]" ).length ) { - rbuggyQSA.push( "~=" ); - } - - // Support: iOS 8 only - // https://bugs.webkit.org/show_bug.cgi?id=136851 - // In-page `selector#id sibling-combinator selector` fails - if ( !el.querySelectorAll( "a#" + expando + "+*" ).length ) { - rbuggyQSA.push( ".#.+[+~]" ); - } - - // Support: Chrome <=105+, Firefox <=104+, Safari <=15.4+ - // In some of the document kinds, these selectors wouldn't work natively. - // This is probably OK but for backwards compatibility we want to maintain - // handling them through jQuery traversal in jQuery 3.x. - if ( !el.querySelectorAll( ":checked" ).length ) { - rbuggyQSA.push( ":checked" ); - } - - // Support: Windows 8 Native Apps - // The type and name attributes are restricted during .innerHTML assignment - input = document.createElement( "input" ); - input.setAttribute( "type", "hidden" ); - el.appendChild( input ).setAttribute( "name", "D" ); - - // Support: IE 9 - 11+ - // IE's :disabled selector does not pick up the children of disabled fieldsets - // Support: Chrome <=105+, Firefox <=104+, Safari <=15.4+ - // In some of the document kinds, these selectors wouldn't work natively. - // This is probably OK but for backwards compatibility we want to maintain - // handling them through jQuery traversal in jQuery 3.x. - documentElement.appendChild( el ).disabled = true; - if ( el.querySelectorAll( ":disabled" ).length !== 2 ) { - rbuggyQSA.push( ":enabled", ":disabled" ); - } - - // Support: IE 11+, Edge 15 - 18+ - // IE 11/Edge don't find elements on a `[name='']` query in some cases. - // Adding a temporary attribute to the document before the selection works - // around the issue. - // Interestingly, IE 10 & older don't seem to have the issue. - input = document.createElement( "input" ); - input.setAttribute( "name", "" ); - el.appendChild( input ); - if ( !el.querySelectorAll( "[name='']" ).length ) { - rbuggyQSA.push( "\\[" + whitespace + "*name" + whitespace + "*=" + - whitespace + "*(?:''|\"\")" ); - } - } ); - - if ( !support.cssHas ) { - - // Support: Chrome 105 - 110+, Safari 15.4 - 16.3+ - // Our regular `try-catch` mechanism fails to detect natively-unsupported - // pseudo-classes inside `:has()` (such as `:has(:contains("Foo"))`) - // in browsers that parse the `:has()` argument as a forgiving selector list. - // https://drafts.csswg.org/selectors/#relational now requires the argument - // to be parsed unforgivingly, but browsers have not yet fully adjusted. - rbuggyQSA.push( ":has" ); - } - - rbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join( "|" ) ); - - /* Sorting - ---------------------------------------------------------------------- */ - - // Document order sorting - sortOrder = function( a, b ) { - - // Flag for duplicate removal - if ( a === b ) { - hasDuplicate = true; - return 0; - } - - // Sort on method existence if only one input has compareDocumentPosition - var compare = !a.compareDocumentPosition - !b.compareDocumentPosition; - if ( compare ) { - return compare; - } - - // Calculate position if both inputs belong to the same document - // Support: IE 11+, Edge 17 - 18+ - // IE/Edge sometimes throw a "Permission denied" error when strict-comparing - // two documents; shallow comparisons work. - // eslint-disable-next-line eqeqeq - compare = ( a.ownerDocument || a ) == ( b.ownerDocument || b ) ? - a.compareDocumentPosition( b ) : - - // Otherwise we know they are disconnected - 1; - - // Disconnected nodes - if ( compare & 1 || - ( !support.sortDetached && b.compareDocumentPosition( a ) === compare ) ) { - - // Choose the first element that is related to our preferred document - // Support: IE 11+, Edge 17 - 18+ - // IE/Edge sometimes throw a "Permission denied" error when strict-comparing - // two documents; shallow comparisons work. - // eslint-disable-next-line eqeqeq - if ( a === document || a.ownerDocument == preferredDoc && - find.contains( preferredDoc, a ) ) { - return -1; - } - - // Support: IE 11+, Edge 17 - 18+ - // IE/Edge sometimes throw a "Permission denied" error when strict-comparing - // two documents; shallow comparisons work. - // eslint-disable-next-line eqeqeq - if ( b === document || b.ownerDocument == preferredDoc && - find.contains( preferredDoc, b ) ) { - return 1; - } - - // Maintain original order - return sortInput ? - ( indexOf.call( sortInput, a ) - indexOf.call( sortInput, b ) ) : - 0; - } - - return compare & 4 ? -1 : 1; - }; - - return document; -} - -find.matches = function( expr, elements ) { - return find( expr, null, null, elements ); -}; - -find.matchesSelector = function( elem, expr ) { - setDocument( elem ); - - if ( documentIsHTML && - !nonnativeSelectorCache[ expr + " " ] && - ( !rbuggyQSA || !rbuggyQSA.test( expr ) ) ) { - - try { - var ret = matches.call( elem, expr ); - - // IE 9's matchesSelector returns false on disconnected nodes - if ( ret || support.disconnectedMatch || - - // As well, disconnected nodes are said to be in a document - // fragment in IE 9 - elem.document && elem.document.nodeType !== 11 ) { - return ret; - } - } catch ( e ) { - nonnativeSelectorCache( expr, true ); - } - } - - return find( expr, document, null, [ elem ] ).length > 0; -}; - -find.contains = function( context, elem ) { - - // Set document vars if needed - // Support: IE 11+, Edge 17 - 18+ - // IE/Edge sometimes throw a "Permission denied" error when strict-comparing - // two documents; shallow comparisons work. - // eslint-disable-next-line eqeqeq - if ( ( context.ownerDocument || context ) != document ) { - setDocument( context ); - } - return jQuery.contains( context, elem ); -}; - - -find.attr = function( elem, name ) { - - // Set document vars if needed - // Support: IE 11+, Edge 17 - 18+ - // IE/Edge sometimes throw a "Permission denied" error when strict-comparing - // two documents; shallow comparisons work. - // eslint-disable-next-line eqeqeq - if ( ( elem.ownerDocument || elem ) != document ) { - setDocument( elem ); - } - - var fn = Expr.attrHandle[ name.toLowerCase() ], - - // Don't get fooled by Object.prototype properties (see trac-13807) - val = fn && hasOwn.call( Expr.attrHandle, name.toLowerCase() ) ? - fn( elem, name, !documentIsHTML ) : - undefined; - - if ( val !== undefined ) { - return val; - } - - return elem.getAttribute( name ); -}; - -find.error = function( msg ) { - throw new Error( "Syntax error, unrecognized expression: " + msg ); -}; - -/** - * Document sorting and removing duplicates - * @param {ArrayLike} results - */ -jQuery.uniqueSort = function( results ) { - var elem, - duplicates = [], - j = 0, - i = 0; - - // Unless we *know* we can detect duplicates, assume their presence - // - // Support: Android <=4.0+ - // Testing for detecting duplicates is unpredictable so instead assume we can't - // depend on duplicate detection in all browsers without a stable sort. - hasDuplicate = !support.sortStable; - sortInput = !support.sortStable && slice.call( results, 0 ); - sort.call( results, sortOrder ); - - if ( hasDuplicate ) { - while ( ( elem = results[ i++ ] ) ) { - if ( elem === results[ i ] ) { - j = duplicates.push( i ); - } - } - while ( j-- ) { - splice.call( results, duplicates[ j ], 1 ); - } - } - - // Clear input after sorting to release objects - // See https://github.com/jquery/sizzle/pull/225 - sortInput = null; - - return results; -}; - -jQuery.fn.uniqueSort = function() { - return this.pushStack( jQuery.uniqueSort( slice.apply( this ) ) ); -}; - -Expr = jQuery.expr = { - - // Can be adjusted by the user - cacheLength: 50, - - createPseudo: markFunction, - - match: matchExpr, - - attrHandle: {}, - - find: {}, - - relative: { - ">": { dir: "parentNode", first: true }, - " ": { dir: "parentNode" }, - "+": { dir: "previousSibling", first: true }, - "~": { dir: "previousSibling" } - }, - - preFilter: { - ATTR: function( match ) { - match[ 1 ] = match[ 1 ].replace( runescape, funescape ); - - // Move the given value to match[3] whether quoted or unquoted - match[ 3 ] = ( match[ 3 ] || match[ 4 ] || match[ 5 ] || "" ) - .replace( runescape, funescape ); - - if ( match[ 2 ] === "~=" ) { - match[ 3 ] = " " + match[ 3 ] + " "; - } - - return match.slice( 0, 4 ); - }, - - CHILD: function( match ) { - - /* matches from matchExpr["CHILD"] - 1 type (only|nth|...) - 2 what (child|of-type) - 3 argument (even|odd|\d*|\d*n([+-]\d+)?|...) - 4 xn-component of xn+y argument ([+-]?\d*n|) - 5 sign of xn-component - 6 x of xn-component - 7 sign of y-component - 8 y of y-component - */ - match[ 1 ] = match[ 1 ].toLowerCase(); - - if ( match[ 1 ].slice( 0, 3 ) === "nth" ) { - - // nth-* requires argument - if ( !match[ 3 ] ) { - find.error( match[ 0 ] ); - } - - // numeric x and y parameters for Expr.filter.CHILD - // remember that false/true cast respectively to 0/1 - match[ 4 ] = +( match[ 4 ] ? - match[ 5 ] + ( match[ 6 ] || 1 ) : - 2 * ( match[ 3 ] === "even" || match[ 3 ] === "odd" ) - ); - match[ 5 ] = +( ( match[ 7 ] + match[ 8 ] ) || match[ 3 ] === "odd" ); - - // other types prohibit arguments - } else if ( match[ 3 ] ) { - find.error( match[ 0 ] ); - } - - return match; - }, - - PSEUDO: function( match ) { - var excess, - unquoted = !match[ 6 ] && match[ 2 ]; - - if ( matchExpr.CHILD.test( match[ 0 ] ) ) { - return null; - } - - // Accept quoted arguments as-is - if ( match[ 3 ] ) { - match[ 2 ] = match[ 4 ] || match[ 5 ] || ""; - - // Strip excess characters from unquoted arguments - } else if ( unquoted && rpseudo.test( unquoted ) && - - // Get excess from tokenize (recursively) - ( excess = tokenize( unquoted, true ) ) && - - // advance to the next closing parenthesis - ( excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length ) ) { - - // excess is a negative index - match[ 0 ] = match[ 0 ].slice( 0, excess ); - match[ 2 ] = unquoted.slice( 0, excess ); - } - - // Return only captures needed by the pseudo filter method (type and argument) - return match.slice( 0, 3 ); - } - }, - - filter: { - - TAG: function( nodeNameSelector ) { - var expectedNodeName = nodeNameSelector.replace( runescape, funescape ).toLowerCase(); - return nodeNameSelector === "*" ? - function() { - return true; - } : - function( elem ) { - return nodeName( elem, expectedNodeName ); - }; - }, - - CLASS: function( className ) { - var pattern = classCache[ className + " " ]; - - return pattern || - ( pattern = new RegExp( "(^|" + whitespace + ")" + className + - "(" + whitespace + "|$)" ) ) && - classCache( className, function( elem ) { - return pattern.test( - typeof elem.className === "string" && elem.className || - typeof elem.getAttribute !== "undefined" && - elem.getAttribute( "class" ) || - "" - ); - } ); - }, - - ATTR: function( name, operator, check ) { - return function( elem ) { - var result = find.attr( elem, name ); - - if ( result == null ) { - return operator === "!="; - } - if ( !operator ) { - return true; - } - - result += ""; - - if ( operator === "=" ) { - return result === check; - } - if ( operator === "!=" ) { - return result !== check; - } - if ( operator === "^=" ) { - return check && result.indexOf( check ) === 0; - } - if ( operator === "*=" ) { - return check && result.indexOf( check ) > -1; - } - if ( operator === "$=" ) { - return check && result.slice( -check.length ) === check; - } - if ( operator === "~=" ) { - return ( " " + result.replace( rwhitespace, " " ) + " " ) - .indexOf( check ) > -1; - } - if ( operator === "|=" ) { - return result === check || result.slice( 0, check.length + 1 ) === check + "-"; - } - - return false; - }; - }, - - CHILD: function( type, what, _argument, first, last ) { - var simple = type.slice( 0, 3 ) !== "nth", - forward = type.slice( -4 ) !== "last", - ofType = what === "of-type"; - - return first === 1 && last === 0 ? - - // Shortcut for :nth-*(n) - function( elem ) { - return !!elem.parentNode; - } : - - function( elem, _context, xml ) { - var cache, outerCache, node, nodeIndex, start, - dir = simple !== forward ? "nextSibling" : "previousSibling", - parent = elem.parentNode, - name = ofType && elem.nodeName.toLowerCase(), - useCache = !xml && !ofType, - diff = false; - - if ( parent ) { - - // :(first|last|only)-(child|of-type) - if ( simple ) { - while ( dir ) { - node = elem; - while ( ( node = node[ dir ] ) ) { - if ( ofType ? - nodeName( node, name ) : - node.nodeType === 1 ) { - - return false; - } - } - - // Reverse direction for :only-* (if we haven't yet done so) - start = dir = type === "only" && !start && "nextSibling"; - } - return true; - } - - start = [ forward ? parent.firstChild : parent.lastChild ]; - - // non-xml :nth-child(...) stores cache data on `parent` - if ( forward && useCache ) { - - // Seek `elem` from a previously-cached index - outerCache = parent[ expando ] || ( parent[ expando ] = {} ); - cache = outerCache[ type ] || []; - nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ]; - diff = nodeIndex && cache[ 2 ]; - node = nodeIndex && parent.childNodes[ nodeIndex ]; - - while ( ( node = ++nodeIndex && node && node[ dir ] || - - // Fallback to seeking `elem` from the start - ( diff = nodeIndex = 0 ) || start.pop() ) ) { - - // When found, cache indexes on `parent` and break - if ( node.nodeType === 1 && ++diff && node === elem ) { - outerCache[ type ] = [ dirruns, nodeIndex, diff ]; - break; - } - } - - } else { - - // Use previously-cached element index if available - if ( useCache ) { - outerCache = elem[ expando ] || ( elem[ expando ] = {} ); - cache = outerCache[ type ] || []; - nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ]; - diff = nodeIndex; - } - - // xml :nth-child(...) - // or :nth-last-child(...) or :nth(-last)?-of-type(...) - if ( diff === false ) { - - // Use the same loop as above to seek `elem` from the start - while ( ( node = ++nodeIndex && node && node[ dir ] || - ( diff = nodeIndex = 0 ) || start.pop() ) ) { - - if ( ( ofType ? - nodeName( node, name ) : - node.nodeType === 1 ) && - ++diff ) { - - // Cache the index of each encountered element - if ( useCache ) { - outerCache = node[ expando ] || - ( node[ expando ] = {} ); - outerCache[ type ] = [ dirruns, diff ]; - } - - if ( node === elem ) { - break; - } - } - } - } - } - - // Incorporate the offset, then check against cycle size - diff -= last; - return diff === first || ( diff % first === 0 && diff / first >= 0 ); - } - }; - }, - - PSEUDO: function( pseudo, argument ) { - - // pseudo-class names are case-insensitive - // https://www.w3.org/TR/selectors/#pseudo-classes - // Prioritize by case sensitivity in case custom pseudos are added with uppercase letters - // Remember that setFilters inherits from pseudos - var args, - fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] || - find.error( "unsupported pseudo: " + pseudo ); - - // The user may use createPseudo to indicate that - // arguments are needed to create the filter function - // just as jQuery does - if ( fn[ expando ] ) { - return fn( argument ); - } - - // But maintain support for old signatures - if ( fn.length > 1 ) { - args = [ pseudo, pseudo, "", argument ]; - return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ? - markFunction( function( seed, matches ) { - var idx, - matched = fn( seed, argument ), - i = matched.length; - while ( i-- ) { - idx = indexOf.call( seed, matched[ i ] ); - seed[ idx ] = !( matches[ idx ] = matched[ i ] ); - } - } ) : - function( elem ) { - return fn( elem, 0, args ); - }; - } - - return fn; - } - }, - - pseudos: { - - // Potentially complex pseudos - not: markFunction( function( selector ) { - - // Trim the selector passed to compile - // to avoid treating leading and trailing - // spaces as combinators - var input = [], - results = [], - matcher = compile( selector.replace( rtrimCSS, "$1" ) ); - - return matcher[ expando ] ? - markFunction( function( seed, matches, _context, xml ) { - var elem, - unmatched = matcher( seed, null, xml, [] ), - i = seed.length; - - // Match elements unmatched by `matcher` - while ( i-- ) { - if ( ( elem = unmatched[ i ] ) ) { - seed[ i ] = !( matches[ i ] = elem ); - } - } - } ) : - function( elem, _context, xml ) { - input[ 0 ] = elem; - matcher( input, null, xml, results ); - - // Don't keep the element - // (see https://github.com/jquery/sizzle/issues/299) - input[ 0 ] = null; - return !results.pop(); - }; - } ), - - has: markFunction( function( selector ) { - return function( elem ) { - return find( selector, elem ).length > 0; - }; - } ), - - contains: markFunction( function( text ) { - text = text.replace( runescape, funescape ); - return function( elem ) { - return ( elem.textContent || jQuery.text( elem ) ).indexOf( text ) > -1; - }; - } ), - - // "Whether an element is represented by a :lang() selector - // is based solely on the element's language value - // being equal to the identifier C, - // or beginning with the identifier C immediately followed by "-". - // The matching of C against the element's language value is performed case-insensitively. - // The identifier C does not have to be a valid language name." - // https://www.w3.org/TR/selectors/#lang-pseudo - lang: markFunction( function( lang ) { - - // lang value must be a valid identifier - if ( !ridentifier.test( lang || "" ) ) { - find.error( "unsupported lang: " + lang ); - } - lang = lang.replace( runescape, funescape ).toLowerCase(); - return function( elem ) { - var elemLang; - do { - if ( ( elemLang = documentIsHTML ? - elem.lang : - elem.getAttribute( "xml:lang" ) || elem.getAttribute( "lang" ) ) ) { - - elemLang = elemLang.toLowerCase(); - return elemLang === lang || elemLang.indexOf( lang + "-" ) === 0; - } - } while ( ( elem = elem.parentNode ) && elem.nodeType === 1 ); - return false; - }; - } ), - - // Miscellaneous - target: function( elem ) { - var hash = window.location && window.location.hash; - return hash && hash.slice( 1 ) === elem.id; - }, - - root: function( elem ) { - return elem === documentElement; - }, - - focus: function( elem ) { - return elem === safeActiveElement() && - document.hasFocus() && - !!( elem.type || elem.href || ~elem.tabIndex ); - }, - - // Boolean properties - enabled: createDisabledPseudo( false ), - disabled: createDisabledPseudo( true ), - - checked: function( elem ) { - - // In CSS3, :checked should return both checked and selected elements - // https://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked - return ( nodeName( elem, "input" ) && !!elem.checked ) || - ( nodeName( elem, "option" ) && !!elem.selected ); - }, - - selected: function( elem ) { - - // Support: IE <=11+ - // Accessing the selectedIndex property - // forces the browser to treat the default option as - // selected when in an optgroup. - if ( elem.parentNode ) { - // eslint-disable-next-line no-unused-expressions - elem.parentNode.selectedIndex; - } - - return elem.selected === true; - }, - - // Contents - empty: function( elem ) { - - // https://www.w3.org/TR/selectors/#empty-pseudo - // :empty is negated by element (1) or content nodes (text: 3; cdata: 4; entity ref: 5), - // but not by others (comment: 8; processing instruction: 7; etc.) - // nodeType < 6 works because attributes (2) do not appear as children - for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { - if ( elem.nodeType < 6 ) { - return false; - } - } - return true; - }, - - parent: function( elem ) { - return !Expr.pseudos.empty( elem ); - }, - - // Element/input types - header: function( elem ) { - return rheader.test( elem.nodeName ); - }, - - input: function( elem ) { - return rinputs.test( elem.nodeName ); - }, - - button: function( elem ) { - return nodeName( elem, "input" ) && elem.type === "button" || - nodeName( elem, "button" ); - }, - - text: function( elem ) { - var attr; - return nodeName( elem, "input" ) && elem.type === "text" && - - // Support: IE <10 only - // New HTML5 attribute values (e.g., "search") appear - // with elem.type === "text" - ( ( attr = elem.getAttribute( "type" ) ) == null || - attr.toLowerCase() === "text" ); - }, - - // Position-in-collection - first: createPositionalPseudo( function() { - return [ 0 ]; - } ), - - last: createPositionalPseudo( function( _matchIndexes, length ) { - return [ length - 1 ]; - } ), - - eq: createPositionalPseudo( function( _matchIndexes, length, argument ) { - return [ argument < 0 ? argument + length : argument ]; - } ), - - even: createPositionalPseudo( function( matchIndexes, length ) { - var i = 0; - for ( ; i < length; i += 2 ) { - matchIndexes.push( i ); - } - return matchIndexes; - } ), - - odd: createPositionalPseudo( function( matchIndexes, length ) { - var i = 1; - for ( ; i < length; i += 2 ) { - matchIndexes.push( i ); - } - return matchIndexes; - } ), - - lt: createPositionalPseudo( function( matchIndexes, length, argument ) { - var i; - - if ( argument < 0 ) { - i = argument + length; - } else if ( argument > length ) { - i = length; - } else { - i = argument; - } - - for ( ; --i >= 0; ) { - matchIndexes.push( i ); - } - return matchIndexes; - } ), - - gt: createPositionalPseudo( function( matchIndexes, length, argument ) { - var i = argument < 0 ? argument + length : argument; - for ( ; ++i < length; ) { - matchIndexes.push( i ); - } - return matchIndexes; - } ) - } -}; - -Expr.pseudos.nth = Expr.pseudos.eq; - -// Add button/input type pseudos -for ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) { - Expr.pseudos[ i ] = createInputPseudo( i ); -} -for ( i in { submit: true, reset: true } ) { - Expr.pseudos[ i ] = createButtonPseudo( i ); -} - -// Easy API for creating new setFilters -function setFilters() {} -setFilters.prototype = Expr.filters = Expr.pseudos; -Expr.setFilters = new setFilters(); - -function tokenize( selector, parseOnly ) { - var matched, match, tokens, type, - soFar, groups, preFilters, - cached = tokenCache[ selector + " " ]; - - if ( cached ) { - return parseOnly ? 0 : cached.slice( 0 ); - } - - soFar = selector; - groups = []; - preFilters = Expr.preFilter; - - while ( soFar ) { - - // Comma and first run - if ( !matched || ( match = rcomma.exec( soFar ) ) ) { - if ( match ) { - - // Don't consume trailing commas as valid - soFar = soFar.slice( match[ 0 ].length ) || soFar; - } - groups.push( ( tokens = [] ) ); - } - - matched = false; - - // Combinators - if ( ( match = rleadingCombinator.exec( soFar ) ) ) { - matched = match.shift(); - tokens.push( { - value: matched, - - // Cast descendant combinators to space - type: match[ 0 ].replace( rtrimCSS, " " ) - } ); - soFar = soFar.slice( matched.length ); - } - - // Filters - for ( type in Expr.filter ) { - if ( ( match = matchExpr[ type ].exec( soFar ) ) && ( !preFilters[ type ] || - ( match = preFilters[ type ]( match ) ) ) ) { - matched = match.shift(); - tokens.push( { - value: matched, - type: type, - matches: match - } ); - soFar = soFar.slice( matched.length ); - } - } - - if ( !matched ) { - break; - } - } - - // Return the length of the invalid excess - // if we're just parsing - // Otherwise, throw an error or return tokens - if ( parseOnly ) { - return soFar.length; - } - - return soFar ? - find.error( selector ) : - - // Cache the tokens - tokenCache( selector, groups ).slice( 0 ); -} - -function toSelector( tokens ) { - var i = 0, - len = tokens.length, - selector = ""; - for ( ; i < len; i++ ) { - selector += tokens[ i ].value; - } - return selector; -} - -function addCombinator( matcher, combinator, base ) { - var dir = combinator.dir, - skip = combinator.next, - key = skip || dir, - checkNonElements = base && key === "parentNode", - doneName = done++; - - return combinator.first ? - - // Check against closest ancestor/preceding element - function( elem, context, xml ) { - while ( ( elem = elem[ dir ] ) ) { - if ( elem.nodeType === 1 || checkNonElements ) { - return matcher( elem, context, xml ); - } - } - return false; - } : - - // Check against all ancestor/preceding elements - function( elem, context, xml ) { - var oldCache, outerCache, - newCache = [ dirruns, doneName ]; - - // We can't set arbitrary data on XML nodes, so they don't benefit from combinator caching - if ( xml ) { - while ( ( elem = elem[ dir ] ) ) { - if ( elem.nodeType === 1 || checkNonElements ) { - if ( matcher( elem, context, xml ) ) { - return true; - } - } - } - } else { - while ( ( elem = elem[ dir ] ) ) { - if ( elem.nodeType === 1 || checkNonElements ) { - outerCache = elem[ expando ] || ( elem[ expando ] = {} ); - - if ( skip && nodeName( elem, skip ) ) { - elem = elem[ dir ] || elem; - } else if ( ( oldCache = outerCache[ key ] ) && - oldCache[ 0 ] === dirruns && oldCache[ 1 ] === doneName ) { - - // Assign to newCache so results back-propagate to previous elements - return ( newCache[ 2 ] = oldCache[ 2 ] ); - } else { - - // Reuse newcache so results back-propagate to previous elements - outerCache[ key ] = newCache; - - // A match means we're done; a fail means we have to keep checking - if ( ( newCache[ 2 ] = matcher( elem, context, xml ) ) ) { - return true; - } - } - } - } - } - return false; - }; -} - -function elementMatcher( matchers ) { - return matchers.length > 1 ? - function( elem, context, xml ) { - var i = matchers.length; - while ( i-- ) { - if ( !matchers[ i ]( elem, context, xml ) ) { - return false; - } - } - return true; - } : - matchers[ 0 ]; -} - -function multipleContexts( selector, contexts, results ) { - var i = 0, - len = contexts.length; - for ( ; i < len; i++ ) { - find( selector, contexts[ i ], results ); - } - return results; -} - -function condense( unmatched, map, filter, context, xml ) { - var elem, - newUnmatched = [], - i = 0, - len = unmatched.length, - mapped = map != null; - - for ( ; i < len; i++ ) { - if ( ( elem = unmatched[ i ] ) ) { - if ( !filter || filter( elem, context, xml ) ) { - newUnmatched.push( elem ); - if ( mapped ) { - map.push( i ); - } - } - } - } - - return newUnmatched; -} - -function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) { - if ( postFilter && !postFilter[ expando ] ) { - postFilter = setMatcher( postFilter ); - } - if ( postFinder && !postFinder[ expando ] ) { - postFinder = setMatcher( postFinder, postSelector ); - } - return markFunction( function( seed, results, context, xml ) { - var temp, i, elem, matcherOut, - preMap = [], - postMap = [], - preexisting = results.length, - - // Get initial elements from seed or context - elems = seed || - multipleContexts( selector || "*", - context.nodeType ? [ context ] : context, [] ), - - // Prefilter to get matcher input, preserving a map for seed-results synchronization - matcherIn = preFilter && ( seed || !selector ) ? - condense( elems, preMap, preFilter, context, xml ) : - elems; - - if ( matcher ) { - - // If we have a postFinder, or filtered seed, or non-seed postFilter - // or preexisting results, - matcherOut = postFinder || ( seed ? preFilter : preexisting || postFilter ) ? - - // ...intermediate processing is necessary - [] : - - // ...otherwise use results directly - results; - - // Find primary matches - matcher( matcherIn, matcherOut, context, xml ); - } else { - matcherOut = matcherIn; - } - - // Apply postFilter - if ( postFilter ) { - temp = condense( matcherOut, postMap ); - postFilter( temp, [], context, xml ); - - // Un-match failing elements by moving them back to matcherIn - i = temp.length; - while ( i-- ) { - if ( ( elem = temp[ i ] ) ) { - matcherOut[ postMap[ i ] ] = !( matcherIn[ postMap[ i ] ] = elem ); - } - } - } - - if ( seed ) { - if ( postFinder || preFilter ) { - if ( postFinder ) { - - // Get the final matcherOut by condensing this intermediate into postFinder contexts - temp = []; - i = matcherOut.length; - while ( i-- ) { - if ( ( elem = matcherOut[ i ] ) ) { - - // Restore matcherIn since elem is not yet a final match - temp.push( ( matcherIn[ i ] = elem ) ); - } - } - postFinder( null, ( matcherOut = [] ), temp, xml ); - } - - // Move matched elements from seed to results to keep them synchronized - i = matcherOut.length; - while ( i-- ) { - if ( ( elem = matcherOut[ i ] ) && - ( temp = postFinder ? indexOf.call( seed, elem ) : preMap[ i ] ) > -1 ) { - - seed[ temp ] = !( results[ temp ] = elem ); - } - } - } - - // Add elements to results, through postFinder if defined - } else { - matcherOut = condense( - matcherOut === results ? - matcherOut.splice( preexisting, matcherOut.length ) : - matcherOut - ); - if ( postFinder ) { - postFinder( null, results, matcherOut, xml ); - } else { - push.apply( results, matcherOut ); - } - } - } ); -} - -function matcherFromTokens( tokens ) { - var checkContext, matcher, j, - len = tokens.length, - leadingRelative = Expr.relative[ tokens[ 0 ].type ], - implicitRelative = leadingRelative || Expr.relative[ " " ], - i = leadingRelative ? 1 : 0, - - // The foundational matcher ensures that elements are reachable from top-level context(s) - matchContext = addCombinator( function( elem ) { - return elem === checkContext; - }, implicitRelative, true ), - matchAnyContext = addCombinator( function( elem ) { - return indexOf.call( checkContext, elem ) > -1; - }, implicitRelative, true ), - matchers = [ function( elem, context, xml ) { - - // Support: IE 11+, Edge 17 - 18+ - // IE/Edge sometimes throw a "Permission denied" error when strict-comparing - // two documents; shallow comparisons work. - // eslint-disable-next-line eqeqeq - var ret = ( !leadingRelative && ( xml || context != outermostContext ) ) || ( - ( checkContext = context ).nodeType ? - matchContext( elem, context, xml ) : - matchAnyContext( elem, context, xml ) ); - - // Avoid hanging onto element - // (see https://github.com/jquery/sizzle/issues/299) - checkContext = null; - return ret; - } ]; - - for ( ; i < len; i++ ) { - if ( ( matcher = Expr.relative[ tokens[ i ].type ] ) ) { - matchers = [ addCombinator( elementMatcher( matchers ), matcher ) ]; - } else { - matcher = Expr.filter[ tokens[ i ].type ].apply( null, tokens[ i ].matches ); - - // Return special upon seeing a positional matcher - if ( matcher[ expando ] ) { - - // Find the next relative operator (if any) for proper handling - j = ++i; - for ( ; j < len; j++ ) { - if ( Expr.relative[ tokens[ j ].type ] ) { - break; - } - } - return setMatcher( - i > 1 && elementMatcher( matchers ), - i > 1 && toSelector( - - // If the preceding token was a descendant combinator, insert an implicit any-element `*` - tokens.slice( 0, i - 1 ) - .concat( { value: tokens[ i - 2 ].type === " " ? "*" : "" } ) - ).replace( rtrimCSS, "$1" ), - matcher, - i < j && matcherFromTokens( tokens.slice( i, j ) ), - j < len && matcherFromTokens( ( tokens = tokens.slice( j ) ) ), - j < len && toSelector( tokens ) - ); - } - matchers.push( matcher ); - } - } - - return elementMatcher( matchers ); -} - -function matcherFromGroupMatchers( elementMatchers, setMatchers ) { - var bySet = setMatchers.length > 0, - byElement = elementMatchers.length > 0, - superMatcher = function( seed, context, xml, results, outermost ) { - var elem, j, matcher, - matchedCount = 0, - i = "0", - unmatched = seed && [], - setMatched = [], - contextBackup = outermostContext, - - // We must always have either seed elements or outermost context - elems = seed || byElement && Expr.find.TAG( "*", outermost ), - - // Use integer dirruns iff this is the outermost matcher - dirrunsUnique = ( dirruns += contextBackup == null ? 1 : Math.random() || 0.1 ), - len = elems.length; - - if ( outermost ) { - - // Support: IE 11+, Edge 17 - 18+ - // IE/Edge sometimes throw a "Permission denied" error when strict-comparing - // two documents; shallow comparisons work. - // eslint-disable-next-line eqeqeq - outermostContext = context == document || context || outermost; - } - - // Add elements passing elementMatchers directly to results - // Support: iOS <=7 - 9 only - // Tolerate NodeList properties (IE: "length"; Safari: ) matching - // elements by id. (see trac-14142) - for ( ; i !== len && ( elem = elems[ i ] ) != null; i++ ) { - if ( byElement && elem ) { - j = 0; - - // Support: IE 11+, Edge 17 - 18+ - // IE/Edge sometimes throw a "Permission denied" error when strict-comparing - // two documents; shallow comparisons work. - // eslint-disable-next-line eqeqeq - if ( !context && elem.ownerDocument != document ) { - setDocument( elem ); - xml = !documentIsHTML; - } - while ( ( matcher = elementMatchers[ j++ ] ) ) { - if ( matcher( elem, context || document, xml ) ) { - push.call( results, elem ); - break; - } - } - if ( outermost ) { - dirruns = dirrunsUnique; - } - } - - // Track unmatched elements for set filters - if ( bySet ) { - - // They will have gone through all possible matchers - if ( ( elem = !matcher && elem ) ) { - matchedCount--; - } - - // Lengthen the array for every element, matched or not - if ( seed ) { - unmatched.push( elem ); - } - } - } - - // `i` is now the count of elements visited above, and adding it to `matchedCount` - // makes the latter nonnegative. - matchedCount += i; - - // Apply set filters to unmatched elements - // NOTE: This can be skipped if there are no unmatched elements (i.e., `matchedCount` - // equals `i`), unless we didn't visit _any_ elements in the above loop because we have - // no element matchers and no seed. - // Incrementing an initially-string "0" `i` allows `i` to remain a string only in that - // case, which will result in a "00" `matchedCount` that differs from `i` but is also - // numerically zero. - if ( bySet && i !== matchedCount ) { - j = 0; - while ( ( matcher = setMatchers[ j++ ] ) ) { - matcher( unmatched, setMatched, context, xml ); - } - - if ( seed ) { - - // Reintegrate element matches to eliminate the need for sorting - if ( matchedCount > 0 ) { - while ( i-- ) { - if ( !( unmatched[ i ] || setMatched[ i ] ) ) { - setMatched[ i ] = pop.call( results ); - } - } - } - - // Discard index placeholder values to get only actual matches - setMatched = condense( setMatched ); - } - - // Add matches to results - push.apply( results, setMatched ); - - // Seedless set matches succeeding multiple successful matchers stipulate sorting - if ( outermost && !seed && setMatched.length > 0 && - ( matchedCount + setMatchers.length ) > 1 ) { - - jQuery.uniqueSort( results ); - } - } - - // Override manipulation of globals by nested matchers - if ( outermost ) { - dirruns = dirrunsUnique; - outermostContext = contextBackup; - } - - return unmatched; - }; - - return bySet ? - markFunction( superMatcher ) : - superMatcher; -} - -function compile( selector, match /* Internal Use Only */ ) { - var i, - setMatchers = [], - elementMatchers = [], - cached = compilerCache[ selector + " " ]; - - if ( !cached ) { - - // Generate a function of recursive functions that can be used to check each element - if ( !match ) { - match = tokenize( selector ); - } - i = match.length; - while ( i-- ) { - cached = matcherFromTokens( match[ i ] ); - if ( cached[ expando ] ) { - setMatchers.push( cached ); - } else { - elementMatchers.push( cached ); - } - } - - // Cache the compiled function - cached = compilerCache( selector, - matcherFromGroupMatchers( elementMatchers, setMatchers ) ); - - // Save selector and tokenization - cached.selector = selector; - } - return cached; -} - -/** - * A low-level selection function that works with jQuery's compiled - * selector functions - * @param {String|Function} selector A selector or a pre-compiled - * selector function built with jQuery selector compile - * @param {Element} context - * @param {Array} [results] - * @param {Array} [seed] A set of elements to match against - */ -function select( selector, context, results, seed ) { - var i, tokens, token, type, find, - compiled = typeof selector === "function" && selector, - match = !seed && tokenize( ( selector = compiled.selector || selector ) ); - - results = results || []; - - // Try to minimize operations if there is only one selector in the list and no seed - // (the latter of which guarantees us context) - if ( match.length === 1 ) { - - // Reduce context if the leading compound selector is an ID - tokens = match[ 0 ] = match[ 0 ].slice( 0 ); - if ( tokens.length > 2 && ( token = tokens[ 0 ] ).type === "ID" && - context.nodeType === 9 && documentIsHTML && Expr.relative[ tokens[ 1 ].type ] ) { - - context = ( Expr.find.ID( - token.matches[ 0 ].replace( runescape, funescape ), - context - ) || [] )[ 0 ]; - if ( !context ) { - return results; - - // Precompiled matchers will still verify ancestry, so step up a level - } else if ( compiled ) { - context = context.parentNode; - } - - selector = selector.slice( tokens.shift().value.length ); - } - - // Fetch a seed set for right-to-left matching - i = matchExpr.needsContext.test( selector ) ? 0 : tokens.length; - while ( i-- ) { - token = tokens[ i ]; - - // Abort if we hit a combinator - if ( Expr.relative[ ( type = token.type ) ] ) { - break; - } - if ( ( find = Expr.find[ type ] ) ) { - - // Search, expanding context for leading sibling combinators - if ( ( seed = find( - token.matches[ 0 ].replace( runescape, funescape ), - rsibling.test( tokens[ 0 ].type ) && - testContext( context.parentNode ) || context - ) ) ) { - - // If seed is empty or no tokens remain, we can return early - tokens.splice( i, 1 ); - selector = seed.length && toSelector( tokens ); - if ( !selector ) { - push.apply( results, seed ); - return results; - } - - break; - } - } - } - } - - // Compile and execute a filtering function if one is not provided - // Provide `match` to avoid retokenization if we modified the selector above - ( compiled || compile( selector, match ) )( - seed, - context, - !documentIsHTML, - results, - !context || rsibling.test( selector ) && testContext( context.parentNode ) || context - ); - return results; -} - -// One-time assignments - -// Support: Android <=4.0 - 4.1+ -// Sort stability -support.sortStable = expando.split( "" ).sort( sortOrder ).join( "" ) === expando; - -// Initialize against the default document -setDocument(); - -// Support: Android <=4.0 - 4.1+ -// Detached nodes confoundingly follow *each other* -support.sortDetached = assert( function( el ) { - - // Should return 1, but returns 4 (following) - return el.compareDocumentPosition( document.createElement( "fieldset" ) ) & 1; -} ); - -jQuery.find = find; - -// Deprecated -jQuery.expr[ ":" ] = jQuery.expr.pseudos; -jQuery.unique = jQuery.uniqueSort; - -// These have always been private, but they used to be documented as part of -// Sizzle so let's maintain them for now for backwards compatibility purposes. -find.compile = compile; -find.select = select; -find.setDocument = setDocument; -find.tokenize = tokenize; - -find.escape = jQuery.escapeSelector; -find.getText = jQuery.text; -find.isXML = jQuery.isXMLDoc; -find.selectors = jQuery.expr; -find.support = jQuery.support; -find.uniqueSort = jQuery.uniqueSort; - - /* eslint-enable */ - -} )(); - - -var dir = function( elem, dir, until ) { - var matched = [], - truncate = until !== undefined; - - while ( ( elem = elem[ dir ] ) && elem.nodeType !== 9 ) { - if ( elem.nodeType === 1 ) { - if ( truncate && jQuery( elem ).is( until ) ) { - break; - } - matched.push( elem ); - } - } - return matched; -}; - - -var siblings = function( n, elem ) { - var matched = []; - - for ( ; n; n = n.nextSibling ) { - if ( n.nodeType === 1 && n !== elem ) { - matched.push( n ); - } - } - - return matched; -}; - - -var rneedsContext = jQuery.expr.match.needsContext; - -var rsingleTag = ( /^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i ); - - - -// Implement the identical functionality for filter and not -function winnow( elements, qualifier, not ) { - if ( isFunction( qualifier ) ) { - return jQuery.grep( elements, function( elem, i ) { - return !!qualifier.call( elem, i, elem ) !== not; - } ); - } - - // Single element - if ( qualifier.nodeType ) { - return jQuery.grep( elements, function( elem ) { - return ( elem === qualifier ) !== not; - } ); - } - - // Arraylike of elements (jQuery, arguments, Array) - if ( typeof qualifier !== "string" ) { - return jQuery.grep( elements, function( elem ) { - return ( indexOf.call( qualifier, elem ) > -1 ) !== not; - } ); - } - - // Filtered directly for both simple and complex selectors - return jQuery.filter( qualifier, elements, not ); -} - -jQuery.filter = function( expr, elems, not ) { - var elem = elems[ 0 ]; - - if ( not ) { - expr = ":not(" + expr + ")"; - } - - if ( elems.length === 1 && elem.nodeType === 1 ) { - return jQuery.find.matchesSelector( elem, expr ) ? [ elem ] : []; - } - - return jQuery.find.matches( expr, jQuery.grep( elems, function( elem ) { - return elem.nodeType === 1; - } ) ); -}; - -jQuery.fn.extend( { - find: function( selector ) { - var i, ret, - len = this.length, - self = this; - - if ( typeof selector !== "string" ) { - return this.pushStack( jQuery( selector ).filter( function() { - for ( i = 0; i < len; i++ ) { - if ( jQuery.contains( self[ i ], this ) ) { - return true; - } - } - } ) ); - } - - ret = this.pushStack( [] ); - - for ( i = 0; i < len; i++ ) { - jQuery.find( selector, self[ i ], ret ); - } - - return len > 1 ? jQuery.uniqueSort( ret ) : ret; - }, - filter: function( selector ) { - return this.pushStack( winnow( this, selector || [], false ) ); - }, - not: function( selector ) { - return this.pushStack( winnow( this, selector || [], true ) ); - }, - is: function( selector ) { - return !!winnow( - this, - - // If this is a positional/relative selector, check membership in the returned set - // so $("p:first").is("p:last") won't return true for a doc with two "p". - typeof selector === "string" && rneedsContext.test( selector ) ? - jQuery( selector ) : - selector || [], - false - ).length; - } -} ); - - -// Initialize a jQuery object - - -// A central reference to the root jQuery(document) -var rootjQuery, - - // A simple way to check for HTML strings - // Prioritize #id over to avoid XSS via location.hash (trac-9521) - // Strict HTML recognition (trac-11290: must start with <) - // Shortcut simple #id case for speed - rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/, - - init = jQuery.fn.init = function( selector, context, root ) { - var match, elem; - - // HANDLE: $(""), $(null), $(undefined), $(false) - if ( !selector ) { - return this; - } - - // Method init() accepts an alternate rootjQuery - // so migrate can support jQuery.sub (gh-2101) - root = root || rootjQuery; - - // Handle HTML strings - if ( typeof selector === "string" ) { - if ( selector[ 0 ] === "<" && - selector[ selector.length - 1 ] === ">" && - selector.length >= 3 ) { - - // Assume that strings that start and end with <> are HTML and skip the regex check - match = [ null, selector, null ]; - - } else { - match = rquickExpr.exec( selector ); - } - - // Match html or make sure no context is specified for #id - if ( match && ( match[ 1 ] || !context ) ) { - - // HANDLE: $(html) -> $(array) - if ( match[ 1 ] ) { - context = context instanceof jQuery ? context[ 0 ] : context; - - // Option to run scripts is true for back-compat - // Intentionally let the error be thrown if parseHTML is not present - jQuery.merge( this, jQuery.parseHTML( - match[ 1 ], - context && context.nodeType ? context.ownerDocument || context : document, - true - ) ); - - // HANDLE: $(html, props) - if ( rsingleTag.test( match[ 1 ] ) && jQuery.isPlainObject( context ) ) { - for ( match in context ) { - - // Properties of context are called as methods if possible - if ( isFunction( this[ match ] ) ) { - this[ match ]( context[ match ] ); - - // ...and otherwise set as attributes - } else { - this.attr( match, context[ match ] ); - } - } - } - - return this; - - // HANDLE: $(#id) - } else { - elem = document.getElementById( match[ 2 ] ); - - if ( elem ) { - - // Inject the element directly into the jQuery object - this[ 0 ] = elem; - this.length = 1; - } - return this; - } - - // HANDLE: $(expr, $(...)) - } else if ( !context || context.jquery ) { - return ( context || root ).find( selector ); - - // HANDLE: $(expr, context) - // (which is just equivalent to: $(context).find(expr) - } else { - return this.constructor( context ).find( selector ); - } - - // HANDLE: $(DOMElement) - } else if ( selector.nodeType ) { - this[ 0 ] = selector; - this.length = 1; - return this; - - // HANDLE: $(function) - // Shortcut for document ready - } else if ( isFunction( selector ) ) { - return root.ready !== undefined ? - root.ready( selector ) : - - // Execute immediately if ready is not present - selector( jQuery ); - } - - return jQuery.makeArray( selector, this ); - }; - -// Give the init function the jQuery prototype for later instantiation -init.prototype = jQuery.fn; - -// Initialize central reference -rootjQuery = jQuery( document ); - - -var rparentsprev = /^(?:parents|prev(?:Until|All))/, - - // Methods guaranteed to produce a unique set when starting from a unique set - guaranteedUnique = { - children: true, - contents: true, - next: true, - prev: true - }; - -jQuery.fn.extend( { - has: function( target ) { - var targets = jQuery( target, this ), - l = targets.length; - - return this.filter( function() { - var i = 0; - for ( ; i < l; i++ ) { - if ( jQuery.contains( this, targets[ i ] ) ) { - return true; - } - } - } ); - }, - - closest: function( selectors, context ) { - var cur, - i = 0, - l = this.length, - matched = [], - targets = typeof selectors !== "string" && jQuery( selectors ); - - // Positional selectors never match, since there's no _selection_ context - if ( !rneedsContext.test( selectors ) ) { - for ( ; i < l; i++ ) { - for ( cur = this[ i ]; cur && cur !== context; cur = cur.parentNode ) { - - // Always skip document fragments - if ( cur.nodeType < 11 && ( targets ? - targets.index( cur ) > -1 : - - // Don't pass non-elements to jQuery#find - cur.nodeType === 1 && - jQuery.find.matchesSelector( cur, selectors ) ) ) { - - matched.push( cur ); - break; - } - } - } - } - - return this.pushStack( matched.length > 1 ? jQuery.uniqueSort( matched ) : matched ); - }, - - // Determine the position of an element within the set - index: function( elem ) { - - // No argument, return index in parent - if ( !elem ) { - return ( this[ 0 ] && this[ 0 ].parentNode ) ? this.first().prevAll().length : -1; - } - - // Index in selector - if ( typeof elem === "string" ) { - return indexOf.call( jQuery( elem ), this[ 0 ] ); - } - - // Locate the position of the desired element - return indexOf.call( this, - - // If it receives a jQuery object, the first element is used - elem.jquery ? elem[ 0 ] : elem - ); - }, - - add: function( selector, context ) { - return this.pushStack( - jQuery.uniqueSort( - jQuery.merge( this.get(), jQuery( selector, context ) ) - ) - ); - }, - - addBack: function( selector ) { - return this.add( selector == null ? - this.prevObject : this.prevObject.filter( selector ) - ); - } -} ); - -function sibling( cur, dir ) { - while ( ( cur = cur[ dir ] ) && cur.nodeType !== 1 ) {} - return cur; -} - -jQuery.each( { - parent: function( elem ) { - var parent = elem.parentNode; - return parent && parent.nodeType !== 11 ? parent : null; - }, - parents: function( elem ) { - return dir( elem, "parentNode" ); - }, - parentsUntil: function( elem, _i, until ) { - return dir( elem, "parentNode", until ); - }, - next: function( elem ) { - return sibling( elem, "nextSibling" ); - }, - prev: function( elem ) { - return sibling( elem, "previousSibling" ); - }, - nextAll: function( elem ) { - return dir( elem, "nextSibling" ); - }, - prevAll: function( elem ) { - return dir( elem, "previousSibling" ); - }, - nextUntil: function( elem, _i, until ) { - return dir( elem, "nextSibling", until ); - }, - prevUntil: function( elem, _i, until ) { - return dir( elem, "previousSibling", until ); - }, - siblings: function( elem ) { - return siblings( ( elem.parentNode || {} ).firstChild, elem ); - }, - children: function( elem ) { - return siblings( elem.firstChild ); - }, - contents: function( elem ) { - if ( elem.contentDocument != null && - - // Support: IE 11+ - // elements with no `data` attribute has an object - // `contentDocument` with a `null` prototype. - getProto( elem.contentDocument ) ) { - - return elem.contentDocument; - } - - // Support: IE 9 - 11 only, iOS 7 only, Android Browser <=4.3 only - // Treat the template element as a regular one in browsers that - // don't support it. - if ( nodeName( elem, "template" ) ) { - elem = elem.content || elem; - } - - return jQuery.merge( [], elem.childNodes ); - } -}, function( name, fn ) { - jQuery.fn[ name ] = function( until, selector ) { - var matched = jQuery.map( this, fn, until ); - - if ( name.slice( -5 ) !== "Until" ) { - selector = until; - } - - if ( selector && typeof selector === "string" ) { - matched = jQuery.filter( selector, matched ); - } - - if ( this.length > 1 ) { - - // Remove duplicates - if ( !guaranteedUnique[ name ] ) { - jQuery.uniqueSort( matched ); - } - - // Reverse order for parents* and prev-derivatives - if ( rparentsprev.test( name ) ) { - matched.reverse(); - } - } - - return this.pushStack( matched ); - }; -} ); -var rnothtmlwhite = ( /[^\x20\t\r\n\f]+/g ); - - - -// Convert String-formatted options into Object-formatted ones -function createOptions( options ) { - var object = {}; - jQuery.each( options.match( rnothtmlwhite ) || [], function( _, flag ) { - object[ flag ] = true; - } ); - return object; -} - -/* - * Create a callback list using the following parameters: - * - * options: an optional list of space-separated options that will change how - * the callback list behaves or a more traditional option object - * - * By default a callback list will act like an event callback list and can be - * "fired" multiple times. - * - * Possible options: - * - * once: will ensure the callback list can only be fired once (like a Deferred) - * - * memory: will keep track of previous values and will call any callback added - * after the list has been fired right away with the latest "memorized" - * values (like a Deferred) - * - * unique: will ensure a callback can only be added once (no duplicate in the list) - * - * stopOnFalse: interrupt callings when a callback returns false - * - */ -jQuery.Callbacks = function( options ) { - - // Convert options from String-formatted to Object-formatted if needed - // (we check in cache first) - options = typeof options === "string" ? - createOptions( options ) : - jQuery.extend( {}, options ); - - var // Flag to know if list is currently firing - firing, - - // Last fire value for non-forgettable lists - memory, - - // Flag to know if list was already fired - fired, - - // Flag to prevent firing - locked, - - // Actual callback list - list = [], - - // Queue of execution data for repeatable lists - queue = [], - - // Index of currently firing callback (modified by add/remove as needed) - firingIndex = -1, - - // Fire callbacks - fire = function() { - - // Enforce single-firing - locked = locked || options.once; - - // Execute callbacks for all pending executions, - // respecting firingIndex overrides and runtime changes - fired = firing = true; - for ( ; queue.length; firingIndex = -1 ) { - memory = queue.shift(); - while ( ++firingIndex < list.length ) { - - // Run callback and check for early termination - if ( list[ firingIndex ].apply( memory[ 0 ], memory[ 1 ] ) === false && - options.stopOnFalse ) { - - // Jump to end and forget the data so .add doesn't re-fire - firingIndex = list.length; - memory = false; - } - } - } - - // Forget the data if we're done with it - if ( !options.memory ) { - memory = false; - } - - firing = false; - - // Clean up if we're done firing for good - if ( locked ) { - - // Keep an empty list if we have data for future add calls - if ( memory ) { - list = []; - - // Otherwise, this object is spent - } else { - list = ""; - } - } - }, - - // Actual Callbacks object - self = { - - // Add a callback or a collection of callbacks to the list - add: function() { - if ( list ) { - - // If we have memory from a past run, we should fire after adding - if ( memory && !firing ) { - firingIndex = list.length - 1; - queue.push( memory ); - } - - ( function add( args ) { - jQuery.each( args, function( _, arg ) { - if ( isFunction( arg ) ) { - if ( !options.unique || !self.has( arg ) ) { - list.push( arg ); - } - } else if ( arg && arg.length && toType( arg ) !== "string" ) { - - // Inspect recursively - add( arg ); - } - } ); - } )( arguments ); - - if ( memory && !firing ) { - fire(); - } - } - return this; - }, - - // Remove a callback from the list - remove: function() { - jQuery.each( arguments, function( _, arg ) { - var index; - while ( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) { - list.splice( index, 1 ); - - // Handle firing indexes - if ( index <= firingIndex ) { - firingIndex--; - } - } - } ); - return this; - }, - - // Check if a given callback is in the list. - // If no argument is given, return whether or not list has callbacks attached. - has: function( fn ) { - return fn ? - jQuery.inArray( fn, list ) > -1 : - list.length > 0; - }, - - // Remove all callbacks from the list - empty: function() { - if ( list ) { - list = []; - } - return this; - }, - - // Disable .fire and .add - // Abort any current/pending executions - // Clear all callbacks and values - disable: function() { - locked = queue = []; - list = memory = ""; - return this; - }, - disabled: function() { - return !list; - }, - - // Disable .fire - // Also disable .add unless we have memory (since it would have no effect) - // Abort any pending executions - lock: function() { - locked = queue = []; - if ( !memory && !firing ) { - list = memory = ""; - } - return this; - }, - locked: function() { - return !!locked; - }, - - // Call all callbacks with the given context and arguments - fireWith: function( context, args ) { - if ( !locked ) { - args = args || []; - args = [ context, args.slice ? args.slice() : args ]; - queue.push( args ); - if ( !firing ) { - fire(); - } - } - return this; - }, - - // Call all the callbacks with the given arguments - fire: function() { - self.fireWith( this, arguments ); - return this; - }, - - // To know if the callbacks have already been called at least once - fired: function() { - return !!fired; - } - }; - - return self; -}; - - -function Identity( v ) { - return v; -} -function Thrower( ex ) { - throw ex; -} - -function adoptValue( value, resolve, reject, noValue ) { - var method; - - try { - - // Check for promise aspect first to privilege synchronous behavior - if ( value && isFunction( ( method = value.promise ) ) ) { - method.call( value ).done( resolve ).fail( reject ); - - // Other thenables - } else if ( value && isFunction( ( method = value.then ) ) ) { - method.call( value, resolve, reject ); - - // Other non-thenables - } else { - - // Control `resolve` arguments by letting Array#slice cast boolean `noValue` to integer: - // * false: [ value ].slice( 0 ) => resolve( value ) - // * true: [ value ].slice( 1 ) => resolve() - resolve.apply( undefined, [ value ].slice( noValue ) ); - } - - // For Promises/A+, convert exceptions into rejections - // Since jQuery.when doesn't unwrap thenables, we can skip the extra checks appearing in - // Deferred#then to conditionally suppress rejection. - } catch ( value ) { - - // Support: Android 4.0 only - // Strict mode functions invoked without .call/.apply get global-object context - reject.apply( undefined, [ value ] ); - } -} - -jQuery.extend( { - - Deferred: function( func ) { - var tuples = [ - - // action, add listener, callbacks, - // ... .then handlers, argument index, [final state] - [ "notify", "progress", jQuery.Callbacks( "memory" ), - jQuery.Callbacks( "memory" ), 2 ], - [ "resolve", "done", jQuery.Callbacks( "once memory" ), - jQuery.Callbacks( "once memory" ), 0, "resolved" ], - [ "reject", "fail", jQuery.Callbacks( "once memory" ), - jQuery.Callbacks( "once memory" ), 1, "rejected" ] - ], - state = "pending", - promise = { - state: function() { - return state; - }, - always: function() { - deferred.done( arguments ).fail( arguments ); - return this; - }, - "catch": function( fn ) { - return promise.then( null, fn ); - }, - - // Keep pipe for back-compat - pipe: function( /* fnDone, fnFail, fnProgress */ ) { - var fns = arguments; - - return jQuery.Deferred( function( newDefer ) { - jQuery.each( tuples, function( _i, tuple ) { - - // Map tuples (progress, done, fail) to arguments (done, fail, progress) - var fn = isFunction( fns[ tuple[ 4 ] ] ) && fns[ tuple[ 4 ] ]; - - // deferred.progress(function() { bind to newDefer or newDefer.notify }) - // deferred.done(function() { bind to newDefer or newDefer.resolve }) - // deferred.fail(function() { bind to newDefer or newDefer.reject }) - deferred[ tuple[ 1 ] ]( function() { - var returned = fn && fn.apply( this, arguments ); - if ( returned && isFunction( returned.promise ) ) { - returned.promise() - .progress( newDefer.notify ) - .done( newDefer.resolve ) - .fail( newDefer.reject ); - } else { - newDefer[ tuple[ 0 ] + "With" ]( - this, - fn ? [ returned ] : arguments - ); - } - } ); - } ); - fns = null; - } ).promise(); - }, - then: function( onFulfilled, onRejected, onProgress ) { - var maxDepth = 0; - function resolve( depth, deferred, handler, special ) { - return function() { - var that = this, - args = arguments, - mightThrow = function() { - var returned, then; - - // Support: Promises/A+ section 2.3.3.3.3 - // https://promisesaplus.com/#point-59 - // Ignore double-resolution attempts - if ( depth < maxDepth ) { - return; - } - - returned = handler.apply( that, args ); - - // Support: Promises/A+ section 2.3.1 - // https://promisesaplus.com/#point-48 - if ( returned === deferred.promise() ) { - throw new TypeError( "Thenable self-resolution" ); - } - - // Support: Promises/A+ sections 2.3.3.1, 3.5 - // https://promisesaplus.com/#point-54 - // https://promisesaplus.com/#point-75 - // Retrieve `then` only once - then = returned && - - // Support: Promises/A+ section 2.3.4 - // https://promisesaplus.com/#point-64 - // Only check objects and functions for thenability - ( typeof returned === "object" || - typeof returned === "function" ) && - returned.then; - - // Handle a returned thenable - if ( isFunction( then ) ) { - - // Special processors (notify) just wait for resolution - if ( special ) { - then.call( - returned, - resolve( maxDepth, deferred, Identity, special ), - resolve( maxDepth, deferred, Thrower, special ) - ); - - // Normal processors (resolve) also hook into progress - } else { - - // ...and disregard older resolution values - maxDepth++; - - then.call( - returned, - resolve( maxDepth, deferred, Identity, special ), - resolve( maxDepth, deferred, Thrower, special ), - resolve( maxDepth, deferred, Identity, - deferred.notifyWith ) - ); - } - - // Handle all other returned values - } else { - - // Only substitute handlers pass on context - // and multiple values (non-spec behavior) - if ( handler !== Identity ) { - that = undefined; - args = [ returned ]; - } - - // Process the value(s) - // Default process is resolve - ( special || deferred.resolveWith )( that, args ); - } - }, - - // Only normal processors (resolve) catch and reject exceptions - process = special ? - mightThrow : - function() { - try { - mightThrow(); - } catch ( e ) { - - if ( jQuery.Deferred.exceptionHook ) { - jQuery.Deferred.exceptionHook( e, - process.error ); - } - - // Support: Promises/A+ section 2.3.3.3.4.1 - // https://promisesaplus.com/#point-61 - // Ignore post-resolution exceptions - if ( depth + 1 >= maxDepth ) { - - // Only substitute handlers pass on context - // and multiple values (non-spec behavior) - if ( handler !== Thrower ) { - that = undefined; - args = [ e ]; - } - - deferred.rejectWith( that, args ); - } - } - }; - - // Support: Promises/A+ section 2.3.3.3.1 - // https://promisesaplus.com/#point-57 - // Re-resolve promises immediately to dodge false rejection from - // subsequent errors - if ( depth ) { - process(); - } else { - - // Call an optional hook to record the error, in case of exception - // since it's otherwise lost when execution goes async - if ( jQuery.Deferred.getErrorHook ) { - process.error = jQuery.Deferred.getErrorHook(); - - // The deprecated alias of the above. While the name suggests - // returning the stack, not an error instance, jQuery just passes - // it directly to `console.warn` so both will work; an instance - // just better cooperates with source maps. - } else if ( jQuery.Deferred.getStackHook ) { - process.error = jQuery.Deferred.getStackHook(); - } - window.setTimeout( process ); - } - }; - } - - return jQuery.Deferred( function( newDefer ) { - - // progress_handlers.add( ... ) - tuples[ 0 ][ 3 ].add( - resolve( - 0, - newDefer, - isFunction( onProgress ) ? - onProgress : - Identity, - newDefer.notifyWith - ) - ); - - // fulfilled_handlers.add( ... ) - tuples[ 1 ][ 3 ].add( - resolve( - 0, - newDefer, - isFunction( onFulfilled ) ? - onFulfilled : - Identity - ) - ); - - // rejected_handlers.add( ... ) - tuples[ 2 ][ 3 ].add( - resolve( - 0, - newDefer, - isFunction( onRejected ) ? - onRejected : - Thrower - ) - ); - } ).promise(); - }, - - // Get a promise for this deferred - // If obj is provided, the promise aspect is added to the object - promise: function( obj ) { - return obj != null ? jQuery.extend( obj, promise ) : promise; - } - }, - deferred = {}; - - // Add list-specific methods - jQuery.each( tuples, function( i, tuple ) { - var list = tuple[ 2 ], - stateString = tuple[ 5 ]; - - // promise.progress = list.add - // promise.done = list.add - // promise.fail = list.add - promise[ tuple[ 1 ] ] = list.add; - - // Handle state - if ( stateString ) { - list.add( - function() { - - // state = "resolved" (i.e., fulfilled) - // state = "rejected" - state = stateString; - }, - - // rejected_callbacks.disable - // fulfilled_callbacks.disable - tuples[ 3 - i ][ 2 ].disable, - - // rejected_handlers.disable - // fulfilled_handlers.disable - tuples[ 3 - i ][ 3 ].disable, - - // progress_callbacks.lock - tuples[ 0 ][ 2 ].lock, - - // progress_handlers.lock - tuples[ 0 ][ 3 ].lock - ); - } - - // progress_handlers.fire - // fulfilled_handlers.fire - // rejected_handlers.fire - list.add( tuple[ 3 ].fire ); - - // deferred.notify = function() { deferred.notifyWith(...) } - // deferred.resolve = function() { deferred.resolveWith(...) } - // deferred.reject = function() { deferred.rejectWith(...) } - deferred[ tuple[ 0 ] ] = function() { - deferred[ tuple[ 0 ] + "With" ]( this === deferred ? undefined : this, arguments ); - return this; - }; - - // deferred.notifyWith = list.fireWith - // deferred.resolveWith = list.fireWith - // deferred.rejectWith = list.fireWith - deferred[ tuple[ 0 ] + "With" ] = list.fireWith; - } ); - - // Make the deferred a promise - promise.promise( deferred ); - - // Call given func if any - if ( func ) { - func.call( deferred, deferred ); - } - - // All done! - return deferred; - }, - - // Deferred helper - when: function( singleValue ) { - var - - // count of uncompleted subordinates - remaining = arguments.length, - - // count of unprocessed arguments - i = remaining, - - // subordinate fulfillment data - resolveContexts = Array( i ), - resolveValues = slice.call( arguments ), - - // the primary Deferred - primary = jQuery.Deferred(), - - // subordinate callback factory - updateFunc = function( i ) { - return function( value ) { - resolveContexts[ i ] = this; - resolveValues[ i ] = arguments.length > 1 ? slice.call( arguments ) : value; - if ( !( --remaining ) ) { - primary.resolveWith( resolveContexts, resolveValues ); - } - }; - }; - - // Single- and empty arguments are adopted like Promise.resolve - if ( remaining <= 1 ) { - adoptValue( singleValue, primary.done( updateFunc( i ) ).resolve, primary.reject, - !remaining ); - - // Use .then() to unwrap secondary thenables (cf. gh-3000) - if ( primary.state() === "pending" || - isFunction( resolveValues[ i ] && resolveValues[ i ].then ) ) { - - return primary.then(); - } - } - - // Multiple arguments are aggregated like Promise.all array elements - while ( i-- ) { - adoptValue( resolveValues[ i ], updateFunc( i ), primary.reject ); - } - - return primary.promise(); - } -} ); - - -// These usually indicate a programmer mistake during development, -// warn about them ASAP rather than swallowing them by default. -var rerrorNames = /^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/; - -// If `jQuery.Deferred.getErrorHook` is defined, `asyncError` is an error -// captured before the async barrier to get the original error cause -// which may otherwise be hidden. -jQuery.Deferred.exceptionHook = function( error, asyncError ) { - - // Support: IE 8 - 9 only - // Console exists when dev tools are open, which can happen at any time - if ( window.console && window.console.warn && error && rerrorNames.test( error.name ) ) { - window.console.warn( "jQuery.Deferred exception: " + error.message, - error.stack, asyncError ); - } -}; - - - - -jQuery.readyException = function( error ) { - window.setTimeout( function() { - throw error; - } ); -}; - - - - -// The deferred used on DOM ready -var readyList = jQuery.Deferred(); - -jQuery.fn.ready = function( fn ) { - - readyList - .then( fn ) - - // Wrap jQuery.readyException in a function so that the lookup - // happens at the time of error handling instead of callback - // registration. - .catch( function( error ) { - jQuery.readyException( error ); - } ); - - return this; -}; - -jQuery.extend( { - - // Is the DOM ready to be used? Set to true once it occurs. - isReady: false, - - // A counter to track how many items to wait for before - // the ready event fires. See trac-6781 - readyWait: 1, - - // Handle when the DOM is ready - ready: function( wait ) { - - // Abort if there are pending holds or we're already ready - if ( wait === true ? --jQuery.readyWait : jQuery.isReady ) { - return; - } - - // Remember that the DOM is ready - jQuery.isReady = true; - - // If a normal DOM Ready event fired, decrement, and wait if need be - if ( wait !== true && --jQuery.readyWait > 0 ) { - return; - } - - // If there are functions bound, to execute - readyList.resolveWith( document, [ jQuery ] ); - } -} ); - -jQuery.ready.then = readyList.then; - -// The ready event handler and self cleanup method -function completed() { - document.removeEventListener( "DOMContentLoaded", completed ); - window.removeEventListener( "load", completed ); - jQuery.ready(); -} - -// Catch cases where $(document).ready() is called -// after the browser event has already occurred. -// Support: IE <=9 - 10 only -// Older IE sometimes signals "interactive" too soon -if ( document.readyState === "complete" || - ( document.readyState !== "loading" && !document.documentElement.doScroll ) ) { - - // Handle it asynchronously to allow scripts the opportunity to delay ready - window.setTimeout( jQuery.ready ); - -} else { - - // Use the handy event callback - document.addEventListener( "DOMContentLoaded", completed ); - - // A fallback to window.onload, that will always work - window.addEventListener( "load", completed ); -} - - - - -// Multifunctional method to get and set values of a collection -// The value/s can optionally be executed if it's a function -var access = function( elems, fn, key, value, chainable, emptyGet, raw ) { - var i = 0, - len = elems.length, - bulk = key == null; - - // Sets many values - if ( toType( key ) === "object" ) { - chainable = true; - for ( i in key ) { - access( elems, fn, i, key[ i ], true, emptyGet, raw ); - } - - // Sets one value - } else if ( value !== undefined ) { - chainable = true; - - if ( !isFunction( value ) ) { - raw = true; - } - - if ( bulk ) { - - // Bulk operations run against the entire set - if ( raw ) { - fn.call( elems, value ); - fn = null; - - // ...except when executing function values - } else { - bulk = fn; - fn = function( elem, _key, value ) { - return bulk.call( jQuery( elem ), value ); - }; - } - } - - if ( fn ) { - for ( ; i < len; i++ ) { - fn( - elems[ i ], key, raw ? - value : - value.call( elems[ i ], i, fn( elems[ i ], key ) ) - ); - } - } - } - - if ( chainable ) { - return elems; - } - - // Gets - if ( bulk ) { - return fn.call( elems ); - } - - return len ? fn( elems[ 0 ], key ) : emptyGet; -}; - - -// Matches dashed string for camelizing -var rmsPrefix = /^-ms-/, - rdashAlpha = /-([a-z])/g; - -// Used by camelCase as callback to replace() -function fcamelCase( _all, letter ) { - return letter.toUpperCase(); -} - -// Convert dashed to camelCase; used by the css and data modules -// Support: IE <=9 - 11, Edge 12 - 15 -// Microsoft forgot to hump their vendor prefix (trac-9572) -function camelCase( string ) { - return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase ); -} -var acceptData = function( owner ) { - - // Accepts only: - // - Node - // - Node.ELEMENT_NODE - // - Node.DOCUMENT_NODE - // - Object - // - Any - return owner.nodeType === 1 || owner.nodeType === 9 || !( +owner.nodeType ); -}; - - - - -function Data() { - this.expando = jQuery.expando + Data.uid++; -} - -Data.uid = 1; - -Data.prototype = { - - cache: function( owner ) { - - // Check if the owner object already has a cache - var value = owner[ this.expando ]; - - // If not, create one - if ( !value ) { - value = {}; - - // We can accept data for non-element nodes in modern browsers, - // but we should not, see trac-8335. - // Always return an empty object. - if ( acceptData( owner ) ) { - - // If it is a node unlikely to be stringify-ed or looped over - // use plain assignment - if ( owner.nodeType ) { - owner[ this.expando ] = value; - - // Otherwise secure it in a non-enumerable property - // configurable must be true to allow the property to be - // deleted when data is removed - } else { - Object.defineProperty( owner, this.expando, { - value: value, - configurable: true - } ); - } - } - } - - return value; - }, - set: function( owner, data, value ) { - var prop, - cache = this.cache( owner ); - - // Handle: [ owner, key, value ] args - // Always use camelCase key (gh-2257) - if ( typeof data === "string" ) { - cache[ camelCase( data ) ] = value; - - // Handle: [ owner, { properties } ] args - } else { - - // Copy the properties one-by-one to the cache object - for ( prop in data ) { - cache[ camelCase( prop ) ] = data[ prop ]; - } - } - return cache; - }, - get: function( owner, key ) { - return key === undefined ? - this.cache( owner ) : - - // Always use camelCase key (gh-2257) - owner[ this.expando ] && owner[ this.expando ][ camelCase( key ) ]; - }, - access: function( owner, key, value ) { - - // In cases where either: - // - // 1. No key was specified - // 2. A string key was specified, but no value provided - // - // Take the "read" path and allow the get method to determine - // which value to return, respectively either: - // - // 1. The entire cache object - // 2. The data stored at the key - // - if ( key === undefined || - ( ( key && typeof key === "string" ) && value === undefined ) ) { - - return this.get( owner, key ); - } - - // When the key is not a string, or both a key and value - // are specified, set or extend (existing objects) with either: - // - // 1. An object of properties - // 2. A key and value - // - this.set( owner, key, value ); - - // Since the "set" path can have two possible entry points - // return the expected data based on which path was taken[*] - return value !== undefined ? value : key; - }, - remove: function( owner, key ) { - var i, - cache = owner[ this.expando ]; - - if ( cache === undefined ) { - return; - } - - if ( key !== undefined ) { - - // Support array or space separated string of keys - if ( Array.isArray( key ) ) { - - // If key is an array of keys... - // We always set camelCase keys, so remove that. - key = key.map( camelCase ); - } else { - key = camelCase( key ); - - // If a key with the spaces exists, use it. - // Otherwise, create an array by matching non-whitespace - key = key in cache ? - [ key ] : - ( key.match( rnothtmlwhite ) || [] ); - } - - i = key.length; - - while ( i-- ) { - delete cache[ key[ i ] ]; - } - } - - // Remove the expando if there's no more data - if ( key === undefined || jQuery.isEmptyObject( cache ) ) { - - // Support: Chrome <=35 - 45 - // Webkit & Blink performance suffers when deleting properties - // from DOM nodes, so set to undefined instead - // https://bugs.chromium.org/p/chromium/issues/detail?id=378607 (bug restricted) - if ( owner.nodeType ) { - owner[ this.expando ] = undefined; - } else { - delete owner[ this.expando ]; - } - } - }, - hasData: function( owner ) { - var cache = owner[ this.expando ]; - return cache !== undefined && !jQuery.isEmptyObject( cache ); - } -}; -var dataPriv = new Data(); - -var dataUser = new Data(); - - - -// Implementation Summary -// -// 1. Enforce API surface and semantic compatibility with 1.9.x branch -// 2. Improve the module's maintainability by reducing the storage -// paths to a single mechanism. -// 3. Use the same single mechanism to support "private" and "user" data. -// 4. _Never_ expose "private" data to user code (TODO: Drop _data, _removeData) -// 5. Avoid exposing implementation details on user objects (eg. expando properties) -// 6. Provide a clear path for implementation upgrade to WeakMap in 2014 - -var rbrace = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/, - rmultiDash = /[A-Z]/g; - -function getData( data ) { - if ( data === "true" ) { - return true; - } - - if ( data === "false" ) { - return false; - } - - if ( data === "null" ) { - return null; - } - - // Only convert to a number if it doesn't change the string - if ( data === +data + "" ) { - return +data; - } - - if ( rbrace.test( data ) ) { - return JSON.parse( data ); - } - - return data; -} - -function dataAttr( elem, key, data ) { - var name; - - // If nothing was found internally, try to fetch any - // data from the HTML5 data-* attribute - if ( data === undefined && elem.nodeType === 1 ) { - name = "data-" + key.replace( rmultiDash, "-$&" ).toLowerCase(); - data = elem.getAttribute( name ); - - if ( typeof data === "string" ) { - try { - data = getData( data ); - } catch ( e ) {} - - // Make sure we set the data so it isn't changed later - dataUser.set( elem, key, data ); - } else { - data = undefined; - } - } - return data; -} - -jQuery.extend( { - hasData: function( elem ) { - return dataUser.hasData( elem ) || dataPriv.hasData( elem ); - }, - - data: function( elem, name, data ) { - return dataUser.access( elem, name, data ); - }, - - removeData: function( elem, name ) { - dataUser.remove( elem, name ); - }, - - // TODO: Now that all calls to _data and _removeData have been replaced - // with direct calls to dataPriv methods, these can be deprecated. - _data: function( elem, name, data ) { - return dataPriv.access( elem, name, data ); - }, - - _removeData: function( elem, name ) { - dataPriv.remove( elem, name ); - } -} ); - -jQuery.fn.extend( { - data: function( key, value ) { - var i, name, data, - elem = this[ 0 ], - attrs = elem && elem.attributes; - - // Gets all values - if ( key === undefined ) { - if ( this.length ) { - data = dataUser.get( elem ); - - if ( elem.nodeType === 1 && !dataPriv.get( elem, "hasDataAttrs" ) ) { - i = attrs.length; - while ( i-- ) { - - // Support: IE 11 only - // The attrs elements can be null (trac-14894) - if ( attrs[ i ] ) { - name = attrs[ i ].name; - if ( name.indexOf( "data-" ) === 0 ) { - name = camelCase( name.slice( 5 ) ); - dataAttr( elem, name, data[ name ] ); - } - } - } - dataPriv.set( elem, "hasDataAttrs", true ); - } - } - - return data; - } - - // Sets multiple values - if ( typeof key === "object" ) { - return this.each( function() { - dataUser.set( this, key ); - } ); - } - - return access( this, function( value ) { - var data; - - // The calling jQuery object (element matches) is not empty - // (and therefore has an element appears at this[ 0 ]) and the - // `value` parameter was not undefined. An empty jQuery object - // will result in `undefined` for elem = this[ 0 ] which will - // throw an exception if an attempt to read a data cache is made. - if ( elem && value === undefined ) { - - // Attempt to get data from the cache - // The key will always be camelCased in Data - data = dataUser.get( elem, key ); - if ( data !== undefined ) { - return data; - } - - // Attempt to "discover" the data in - // HTML5 custom data-* attrs - data = dataAttr( elem, key ); - if ( data !== undefined ) { - return data; - } - - // We tried really hard, but the data doesn't exist. - return; - } - - // Set the data... - this.each( function() { - - // We always store the camelCased key - dataUser.set( this, key, value ); - } ); - }, null, value, arguments.length > 1, null, true ); - }, - - removeData: function( key ) { - return this.each( function() { - dataUser.remove( this, key ); - } ); - } -} ); - - -jQuery.extend( { - queue: function( elem, type, data ) { - var queue; - - if ( elem ) { - type = ( type || "fx" ) + "queue"; - queue = dataPriv.get( elem, type ); - - // Speed up dequeue by getting out quickly if this is just a lookup - if ( data ) { - if ( !queue || Array.isArray( data ) ) { - queue = dataPriv.access( elem, type, jQuery.makeArray( data ) ); - } else { - queue.push( data ); - } - } - return queue || []; - } - }, - - dequeue: function( elem, type ) { - type = type || "fx"; - - var queue = jQuery.queue( elem, type ), - startLength = queue.length, - fn = queue.shift(), - hooks = jQuery._queueHooks( elem, type ), - next = function() { - jQuery.dequeue( elem, type ); - }; - - // If the fx queue is dequeued, always remove the progress sentinel - if ( fn === "inprogress" ) { - fn = queue.shift(); - startLength--; - } - - if ( fn ) { - - // Add a progress sentinel to prevent the fx queue from being - // automatically dequeued - if ( type === "fx" ) { - queue.unshift( "inprogress" ); - } - - // Clear up the last queue stop function - delete hooks.stop; - fn.call( elem, next, hooks ); - } - - if ( !startLength && hooks ) { - hooks.empty.fire(); - } - }, - - // Not public - generate a queueHooks object, or return the current one - _queueHooks: function( elem, type ) { - var key = type + "queueHooks"; - return dataPriv.get( elem, key ) || dataPriv.access( elem, key, { - empty: jQuery.Callbacks( "once memory" ).add( function() { - dataPriv.remove( elem, [ type + "queue", key ] ); - } ) - } ); - } -} ); - -jQuery.fn.extend( { - queue: function( type, data ) { - var setter = 2; - - if ( typeof type !== "string" ) { - data = type; - type = "fx"; - setter--; - } - - if ( arguments.length < setter ) { - return jQuery.queue( this[ 0 ], type ); - } - - return data === undefined ? - this : - this.each( function() { - var queue = jQuery.queue( this, type, data ); - - // Ensure a hooks for this queue - jQuery._queueHooks( this, type ); - - if ( type === "fx" && queue[ 0 ] !== "inprogress" ) { - jQuery.dequeue( this, type ); - } - } ); - }, - dequeue: function( type ) { - return this.each( function() { - jQuery.dequeue( this, type ); - } ); - }, - clearQueue: function( type ) { - return this.queue( type || "fx", [] ); - }, - - // Get a promise resolved when queues of a certain type - // are emptied (fx is the type by default) - promise: function( type, obj ) { - var tmp, - count = 1, - defer = jQuery.Deferred(), - elements = this, - i = this.length, - resolve = function() { - if ( !( --count ) ) { - defer.resolveWith( elements, [ elements ] ); - } - }; - - if ( typeof type !== "string" ) { - obj = type; - type = undefined; - } - type = type || "fx"; - - while ( i-- ) { - tmp = dataPriv.get( elements[ i ], type + "queueHooks" ); - if ( tmp && tmp.empty ) { - count++; - tmp.empty.add( resolve ); - } - } - resolve(); - return defer.promise( obj ); - } -} ); -var pnum = ( /[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/ ).source; - -var rcssNum = new RegExp( "^(?:([+-])=|)(" + pnum + ")([a-z%]*)$", "i" ); - - -var cssExpand = [ "Top", "Right", "Bottom", "Left" ]; - -var documentElement = document.documentElement; - - - - var isAttached = function( elem ) { - return jQuery.contains( elem.ownerDocument, elem ); - }, - composed = { composed: true }; - - // Support: IE 9 - 11+, Edge 12 - 18+, iOS 10.0 - 10.2 only - // Check attachment across shadow DOM boundaries when possible (gh-3504) - // Support: iOS 10.0-10.2 only - // Early iOS 10 versions support `attachShadow` but not `getRootNode`, - // leading to errors. We need to check for `getRootNode`. - if ( documentElement.getRootNode ) { - isAttached = function( elem ) { - return jQuery.contains( elem.ownerDocument, elem ) || - elem.getRootNode( composed ) === elem.ownerDocument; - }; - } -var isHiddenWithinTree = function( elem, el ) { - - // isHiddenWithinTree might be called from jQuery#filter function; - // in that case, element will be second argument - elem = el || elem; - - // Inline style trumps all - return elem.style.display === "none" || - elem.style.display === "" && - - // Otherwise, check computed style - // Support: Firefox <=43 - 45 - // Disconnected elements can have computed display: none, so first confirm that elem is - // in the document. - isAttached( elem ) && - - jQuery.css( elem, "display" ) === "none"; - }; - - - -function adjustCSS( elem, prop, valueParts, tween ) { - var adjusted, scale, - maxIterations = 20, - currentValue = tween ? - function() { - return tween.cur(); - } : - function() { - return jQuery.css( elem, prop, "" ); - }, - initial = currentValue(), - unit = valueParts && valueParts[ 3 ] || ( jQuery.cssNumber[ prop ] ? "" : "px" ), - - // Starting value computation is required for potential unit mismatches - initialInUnit = elem.nodeType && - ( jQuery.cssNumber[ prop ] || unit !== "px" && +initial ) && - rcssNum.exec( jQuery.css( elem, prop ) ); - - if ( initialInUnit && initialInUnit[ 3 ] !== unit ) { - - // Support: Firefox <=54 - // Halve the iteration target value to prevent interference from CSS upper bounds (gh-2144) - initial = initial / 2; - - // Trust units reported by jQuery.css - unit = unit || initialInUnit[ 3 ]; - - // Iteratively approximate from a nonzero starting point - initialInUnit = +initial || 1; - - while ( maxIterations-- ) { - - // Evaluate and update our best guess (doubling guesses that zero out). - // Finish if the scale equals or crosses 1 (making the old*new product non-positive). - jQuery.style( elem, prop, initialInUnit + unit ); - if ( ( 1 - scale ) * ( 1 - ( scale = currentValue() / initial || 0.5 ) ) <= 0 ) { - maxIterations = 0; - } - initialInUnit = initialInUnit / scale; - - } - - initialInUnit = initialInUnit * 2; - jQuery.style( elem, prop, initialInUnit + unit ); - - // Make sure we update the tween properties later on - valueParts = valueParts || []; - } - - if ( valueParts ) { - initialInUnit = +initialInUnit || +initial || 0; - - // Apply relative offset (+=/-=) if specified - adjusted = valueParts[ 1 ] ? - initialInUnit + ( valueParts[ 1 ] + 1 ) * valueParts[ 2 ] : - +valueParts[ 2 ]; - if ( tween ) { - tween.unit = unit; - tween.start = initialInUnit; - tween.end = adjusted; - } - } - return adjusted; -} - - -var defaultDisplayMap = {}; - -function getDefaultDisplay( elem ) { - var temp, - doc = elem.ownerDocument, - nodeName = elem.nodeName, - display = defaultDisplayMap[ nodeName ]; - - if ( display ) { - return display; - } - - temp = doc.body.appendChild( doc.createElement( nodeName ) ); - display = jQuery.css( temp, "display" ); - - temp.parentNode.removeChild( temp ); - - if ( display === "none" ) { - display = "block"; - } - defaultDisplayMap[ nodeName ] = display; - - return display; -} - -function showHide( elements, show ) { - var display, elem, - values = [], - index = 0, - length = elements.length; - - // Determine new display value for elements that need to change - for ( ; index < length; index++ ) { - elem = elements[ index ]; - if ( !elem.style ) { - continue; - } - - display = elem.style.display; - if ( show ) { - - // Since we force visibility upon cascade-hidden elements, an immediate (and slow) - // check is required in this first loop unless we have a nonempty display value (either - // inline or about-to-be-restored) - if ( display === "none" ) { - values[ index ] = dataPriv.get( elem, "display" ) || null; - if ( !values[ index ] ) { - elem.style.display = ""; - } - } - if ( elem.style.display === "" && isHiddenWithinTree( elem ) ) { - values[ index ] = getDefaultDisplay( elem ); - } - } else { - if ( display !== "none" ) { - values[ index ] = "none"; - - // Remember what we're overwriting - dataPriv.set( elem, "display", display ); - } - } - } - - // Set the display of the elements in a second loop to avoid constant reflow - for ( index = 0; index < length; index++ ) { - if ( values[ index ] != null ) { - elements[ index ].style.display = values[ index ]; - } - } - - return elements; -} - -jQuery.fn.extend( { - show: function() { - return showHide( this, true ); - }, - hide: function() { - return showHide( this ); - }, - toggle: function( state ) { - if ( typeof state === "boolean" ) { - return state ? this.show() : this.hide(); - } - - return this.each( function() { - if ( isHiddenWithinTree( this ) ) { - jQuery( this ).show(); - } else { - jQuery( this ).hide(); - } - } ); - } -} ); -var rcheckableType = ( /^(?:checkbox|radio)$/i ); - -var rtagName = ( /<([a-z][^\/\0>\x20\t\r\n\f]*)/i ); - -var rscriptType = ( /^$|^module$|\/(?:java|ecma)script/i ); - - - -( function() { - var fragment = document.createDocumentFragment(), - div = fragment.appendChild( document.createElement( "div" ) ), - input = document.createElement( "input" ); - - // Support: Android 4.0 - 4.3 only - // Check state lost if the name is set (trac-11217) - // Support: Windows Web Apps (WWA) - // `name` and `type` must use .setAttribute for WWA (trac-14901) - input.setAttribute( "type", "radio" ); - input.setAttribute( "checked", "checked" ); - input.setAttribute( "name", "t" ); - - div.appendChild( input ); - - // Support: Android <=4.1 only - // Older WebKit doesn't clone checked state correctly in fragments - support.checkClone = div.cloneNode( true ).cloneNode( true ).lastChild.checked; - - // Support: IE <=11 only - // Make sure textarea (and checkbox) defaultValue is properly cloned - div.innerHTML = ""; - support.noCloneChecked = !!div.cloneNode( true ).lastChild.defaultValue; - - // Support: IE <=9 only - // IE <=9 replaces "; - support.option = !!div.lastChild; -} )(); - - -// We have to close these tags to support XHTML (trac-13200) -var wrapMap = { - - // XHTML parsers do not magically insert elements in the - // same way that tag soup parsers do. So we cannot shorten - // this by omitting or other required elements. - thead: [ 1, "", "
      " ], - col: [ 2, "", "
      " ], - tr: [ 2, "", "
      " ], - td: [ 3, "", "
      " ], - - _default: [ 0, "", "" ] -}; - -wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; -wrapMap.th = wrapMap.td; - -// Support: IE <=9 only -if ( !support.option ) { - wrapMap.optgroup = wrapMap.option = [ 1, "" ]; -} - - -function getAll( context, tag ) { - - // Support: IE <=9 - 11 only - // Use typeof to avoid zero-argument method invocation on host objects (trac-15151) - var ret; - - if ( typeof context.getElementsByTagName !== "undefined" ) { - ret = context.getElementsByTagName( tag || "*" ); - - } else if ( typeof context.querySelectorAll !== "undefined" ) { - ret = context.querySelectorAll( tag || "*" ); - - } else { - ret = []; - } - - if ( tag === undefined || tag && nodeName( context, tag ) ) { - return jQuery.merge( [ context ], ret ); - } - - return ret; -} - - -// Mark scripts as having already been evaluated -function setGlobalEval( elems, refElements ) { - var i = 0, - l = elems.length; - - for ( ; i < l; i++ ) { - dataPriv.set( - elems[ i ], - "globalEval", - !refElements || dataPriv.get( refElements[ i ], "globalEval" ) - ); - } -} - - -var rhtml = /<|&#?\w+;/; - -function buildFragment( elems, context, scripts, selection, ignored ) { - var elem, tmp, tag, wrap, attached, j, - fragment = context.createDocumentFragment(), - nodes = [], - i = 0, - l = elems.length; - - for ( ; i < l; i++ ) { - elem = elems[ i ]; - - if ( elem || elem === 0 ) { - - // Add nodes directly - if ( toType( elem ) === "object" ) { - - // Support: Android <=4.0 only, PhantomJS 1 only - // push.apply(_, arraylike) throws on ancient WebKit - jQuery.merge( nodes, elem.nodeType ? [ elem ] : elem ); - - // Convert non-html into a text node - } else if ( !rhtml.test( elem ) ) { - nodes.push( context.createTextNode( elem ) ); - - // Convert html into DOM nodes - } else { - tmp = tmp || fragment.appendChild( context.createElement( "div" ) ); - - // Deserialize a standard representation - tag = ( rtagName.exec( elem ) || [ "", "" ] )[ 1 ].toLowerCase(); - wrap = wrapMap[ tag ] || wrapMap._default; - tmp.innerHTML = wrap[ 1 ] + jQuery.htmlPrefilter( elem ) + wrap[ 2 ]; - - // Descend through wrappers to the right content - j = wrap[ 0 ]; - while ( j-- ) { - tmp = tmp.lastChild; - } - - // Support: Android <=4.0 only, PhantomJS 1 only - // push.apply(_, arraylike) throws on ancient WebKit - jQuery.merge( nodes, tmp.childNodes ); - - // Remember the top-level container - tmp = fragment.firstChild; - - // Ensure the created nodes are orphaned (trac-12392) - tmp.textContent = ""; - } - } - } - - // Remove wrapper from fragment - fragment.textContent = ""; - - i = 0; - while ( ( elem = nodes[ i++ ] ) ) { - - // Skip elements already in the context collection (trac-4087) - if ( selection && jQuery.inArray( elem, selection ) > -1 ) { - if ( ignored ) { - ignored.push( elem ); - } - continue; - } - - attached = isAttached( elem ); - - // Append to fragment - tmp = getAll( fragment.appendChild( elem ), "script" ); - - // Preserve script evaluation history - if ( attached ) { - setGlobalEval( tmp ); - } - - // Capture executables - if ( scripts ) { - j = 0; - while ( ( elem = tmp[ j++ ] ) ) { - if ( rscriptType.test( elem.type || "" ) ) { - scripts.push( elem ); - } - } - } - } - - return fragment; -} - - -var rtypenamespace = /^([^.]*)(?:\.(.+)|)/; - -function returnTrue() { - return true; -} - -function returnFalse() { - return false; -} - -function on( elem, types, selector, data, fn, one ) { - var origFn, type; - - // Types can be a map of types/handlers - if ( typeof types === "object" ) { - - // ( types-Object, selector, data ) - if ( typeof selector !== "string" ) { - - // ( types-Object, data ) - data = data || selector; - selector = undefined; - } - for ( type in types ) { - on( elem, type, selector, data, types[ type ], one ); - } - return elem; - } - - if ( data == null && fn == null ) { - - // ( types, fn ) - fn = selector; - data = selector = undefined; - } else if ( fn == null ) { - if ( typeof selector === "string" ) { - - // ( types, selector, fn ) - fn = data; - data = undefined; - } else { - - // ( types, data, fn ) - fn = data; - data = selector; - selector = undefined; - } - } - if ( fn === false ) { - fn = returnFalse; - } else if ( !fn ) { - return elem; - } - - if ( one === 1 ) { - origFn = fn; - fn = function( event ) { - - // Can use an empty set, since event contains the info - jQuery().off( event ); - return origFn.apply( this, arguments ); - }; - - // Use same guid so caller can remove using origFn - fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); - } - return elem.each( function() { - jQuery.event.add( this, types, fn, data, selector ); - } ); -} - -/* - * Helper functions for managing events -- not part of the public interface. - * Props to Dean Edwards' addEvent library for many of the ideas. - */ -jQuery.event = { - - global: {}, - - add: function( elem, types, handler, data, selector ) { - - var handleObjIn, eventHandle, tmp, - events, t, handleObj, - special, handlers, type, namespaces, origType, - elemData = dataPriv.get( elem ); - - // Only attach events to objects that accept data - if ( !acceptData( elem ) ) { - return; - } - - // Caller can pass in an object of custom data in lieu of the handler - if ( handler.handler ) { - handleObjIn = handler; - handler = handleObjIn.handler; - selector = handleObjIn.selector; - } - - // Ensure that invalid selectors throw exceptions at attach time - // Evaluate against documentElement in case elem is a non-element node (e.g., document) - if ( selector ) { - jQuery.find.matchesSelector( documentElement, selector ); - } - - // Make sure that the handler has a unique ID, used to find/remove it later - if ( !handler.guid ) { - handler.guid = jQuery.guid++; - } - - // Init the element's event structure and main handler, if this is the first - if ( !( events = elemData.events ) ) { - events = elemData.events = Object.create( null ); - } - if ( !( eventHandle = elemData.handle ) ) { - eventHandle = elemData.handle = function( e ) { - - // Discard the second event of a jQuery.event.trigger() and - // when an event is called after a page has unloaded - return typeof jQuery !== "undefined" && jQuery.event.triggered !== e.type ? - jQuery.event.dispatch.apply( elem, arguments ) : undefined; - }; - } - - // Handle multiple events separated by a space - types = ( types || "" ).match( rnothtmlwhite ) || [ "" ]; - t = types.length; - while ( t-- ) { - tmp = rtypenamespace.exec( types[ t ] ) || []; - type = origType = tmp[ 1 ]; - namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort(); - - // There *must* be a type, no attaching namespace-only handlers - if ( !type ) { - continue; - } - - // If event changes its type, use the special event handlers for the changed type - special = jQuery.event.special[ type ] || {}; - - // If selector defined, determine special event api type, otherwise given type - type = ( selector ? special.delegateType : special.bindType ) || type; - - // Update special based on newly reset type - special = jQuery.event.special[ type ] || {}; - - // handleObj is passed to all event handlers - handleObj = jQuery.extend( { - type: type, - origType: origType, - data: data, - handler: handler, - guid: handler.guid, - selector: selector, - needsContext: selector && jQuery.expr.match.needsContext.test( selector ), - namespace: namespaces.join( "." ) - }, handleObjIn ); - - // Init the event handler queue if we're the first - if ( !( handlers = events[ type ] ) ) { - handlers = events[ type ] = []; - handlers.delegateCount = 0; - - // Only use addEventListener if the special events handler returns false - if ( !special.setup || - special.setup.call( elem, data, namespaces, eventHandle ) === false ) { - - if ( elem.addEventListener ) { - elem.addEventListener( type, eventHandle ); - } - } - } - - if ( special.add ) { - special.add.call( elem, handleObj ); - - if ( !handleObj.handler.guid ) { - handleObj.handler.guid = handler.guid; - } - } - - // Add to the element's handler list, delegates in front - if ( selector ) { - handlers.splice( handlers.delegateCount++, 0, handleObj ); - } else { - handlers.push( handleObj ); - } - - // Keep track of which events have ever been used, for event optimization - jQuery.event.global[ type ] = true; - } - - }, - - // Detach an event or set of events from an element - remove: function( elem, types, handler, selector, mappedTypes ) { - - var j, origCount, tmp, - events, t, handleObj, - special, handlers, type, namespaces, origType, - elemData = dataPriv.hasData( elem ) && dataPriv.get( elem ); - - if ( !elemData || !( events = elemData.events ) ) { - return; - } - - // Once for each type.namespace in types; type may be omitted - types = ( types || "" ).match( rnothtmlwhite ) || [ "" ]; - t = types.length; - while ( t-- ) { - tmp = rtypenamespace.exec( types[ t ] ) || []; - type = origType = tmp[ 1 ]; - namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort(); - - // Unbind all events (on this namespace, if provided) for the element - if ( !type ) { - for ( type in events ) { - jQuery.event.remove( elem, type + types[ t ], handler, selector, true ); - } - continue; - } - - special = jQuery.event.special[ type ] || {}; - type = ( selector ? special.delegateType : special.bindType ) || type; - handlers = events[ type ] || []; - tmp = tmp[ 2 ] && - new RegExp( "(^|\\.)" + namespaces.join( "\\.(?:.*\\.|)" ) + "(\\.|$)" ); - - // Remove matching events - origCount = j = handlers.length; - while ( j-- ) { - handleObj = handlers[ j ]; - - if ( ( mappedTypes || origType === handleObj.origType ) && - ( !handler || handler.guid === handleObj.guid ) && - ( !tmp || tmp.test( handleObj.namespace ) ) && - ( !selector || selector === handleObj.selector || - selector === "**" && handleObj.selector ) ) { - handlers.splice( j, 1 ); - - if ( handleObj.selector ) { - handlers.delegateCount--; - } - if ( special.remove ) { - special.remove.call( elem, handleObj ); - } - } - } - - // Remove generic event handler if we removed something and no more handlers exist - // (avoids potential for endless recursion during removal of special event handlers) - if ( origCount && !handlers.length ) { - if ( !special.teardown || - special.teardown.call( elem, namespaces, elemData.handle ) === false ) { - - jQuery.removeEvent( elem, type, elemData.handle ); - } - - delete events[ type ]; - } - } - - // Remove data and the expando if it's no longer used - if ( jQuery.isEmptyObject( events ) ) { - dataPriv.remove( elem, "handle events" ); - } - }, - - dispatch: function( nativeEvent ) { - - var i, j, ret, matched, handleObj, handlerQueue, - args = new Array( arguments.length ), - - // Make a writable jQuery.Event from the native event object - event = jQuery.event.fix( nativeEvent ), - - handlers = ( - dataPriv.get( this, "events" ) || Object.create( null ) - )[ event.type ] || [], - special = jQuery.event.special[ event.type ] || {}; - - // Use the fix-ed jQuery.Event rather than the (read-only) native event - args[ 0 ] = event; - - for ( i = 1; i < arguments.length; i++ ) { - args[ i ] = arguments[ i ]; - } - - event.delegateTarget = this; - - // Call the preDispatch hook for the mapped type, and let it bail if desired - if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) { - return; - } - - // Determine handlers - handlerQueue = jQuery.event.handlers.call( this, event, handlers ); - - // Run delegates first; they may want to stop propagation beneath us - i = 0; - while ( ( matched = handlerQueue[ i++ ] ) && !event.isPropagationStopped() ) { - event.currentTarget = matched.elem; - - j = 0; - while ( ( handleObj = matched.handlers[ j++ ] ) && - !event.isImmediatePropagationStopped() ) { - - // If the event is namespaced, then each handler is only invoked if it is - // specially universal or its namespaces are a superset of the event's. - if ( !event.rnamespace || handleObj.namespace === false || - event.rnamespace.test( handleObj.namespace ) ) { - - event.handleObj = handleObj; - event.data = handleObj.data; - - ret = ( ( jQuery.event.special[ handleObj.origType ] || {} ).handle || - handleObj.handler ).apply( matched.elem, args ); - - if ( ret !== undefined ) { - if ( ( event.result = ret ) === false ) { - event.preventDefault(); - event.stopPropagation(); - } - } - } - } - } - - // Call the postDispatch hook for the mapped type - if ( special.postDispatch ) { - special.postDispatch.call( this, event ); - } - - return event.result; - }, - - handlers: function( event, handlers ) { - var i, handleObj, sel, matchedHandlers, matchedSelectors, - handlerQueue = [], - delegateCount = handlers.delegateCount, - cur = event.target; - - // Find delegate handlers - if ( delegateCount && - - // Support: IE <=9 - // Black-hole SVG instance trees (trac-13180) - cur.nodeType && - - // Support: Firefox <=42 - // Suppress spec-violating clicks indicating a non-primary pointer button (trac-3861) - // https://www.w3.org/TR/DOM-Level-3-Events/#event-type-click - // Support: IE 11 only - // ...but not arrow key "clicks" of radio inputs, which can have `button` -1 (gh-2343) - !( event.type === "click" && event.button >= 1 ) ) { - - for ( ; cur !== this; cur = cur.parentNode || this ) { - - // Don't check non-elements (trac-13208) - // Don't process clicks on disabled elements (trac-6911, trac-8165, trac-11382, trac-11764) - if ( cur.nodeType === 1 && !( event.type === "click" && cur.disabled === true ) ) { - matchedHandlers = []; - matchedSelectors = {}; - for ( i = 0; i < delegateCount; i++ ) { - handleObj = handlers[ i ]; - - // Don't conflict with Object.prototype properties (trac-13203) - sel = handleObj.selector + " "; - - if ( matchedSelectors[ sel ] === undefined ) { - matchedSelectors[ sel ] = handleObj.needsContext ? - jQuery( sel, this ).index( cur ) > -1 : - jQuery.find( sel, this, null, [ cur ] ).length; - } - if ( matchedSelectors[ sel ] ) { - matchedHandlers.push( handleObj ); - } - } - if ( matchedHandlers.length ) { - handlerQueue.push( { elem: cur, handlers: matchedHandlers } ); - } - } - } - } - - // Add the remaining (directly-bound) handlers - cur = this; - if ( delegateCount < handlers.length ) { - handlerQueue.push( { elem: cur, handlers: handlers.slice( delegateCount ) } ); - } - - return handlerQueue; - }, - - addProp: function( name, hook ) { - Object.defineProperty( jQuery.Event.prototype, name, { - enumerable: true, - configurable: true, - - get: isFunction( hook ) ? - function() { - if ( this.originalEvent ) { - return hook( this.originalEvent ); - } - } : - function() { - if ( this.originalEvent ) { - return this.originalEvent[ name ]; - } - }, - - set: function( value ) { - Object.defineProperty( this, name, { - enumerable: true, - configurable: true, - writable: true, - value: value - } ); - } - } ); - }, - - fix: function( originalEvent ) { - return originalEvent[ jQuery.expando ] ? - originalEvent : - new jQuery.Event( originalEvent ); - }, - - special: { - load: { - - // Prevent triggered image.load events from bubbling to window.load - noBubble: true - }, - click: { - - // Utilize native event to ensure correct state for checkable inputs - setup: function( data ) { - - // For mutual compressibility with _default, replace `this` access with a local var. - // `|| data` is dead code meant only to preserve the variable through minification. - var el = this || data; - - // Claim the first handler - if ( rcheckableType.test( el.type ) && - el.click && nodeName( el, "input" ) ) { - - // dataPriv.set( el, "click", ... ) - leverageNative( el, "click", true ); - } - - // Return false to allow normal processing in the caller - return false; - }, - trigger: function( data ) { - - // For mutual compressibility with _default, replace `this` access with a local var. - // `|| data` is dead code meant only to preserve the variable through minification. - var el = this || data; - - // Force setup before triggering a click - if ( rcheckableType.test( el.type ) && - el.click && nodeName( el, "input" ) ) { - - leverageNative( el, "click" ); - } - - // Return non-false to allow normal event-path propagation - return true; - }, - - // For cross-browser consistency, suppress native .click() on links - // Also prevent it if we're currently inside a leveraged native-event stack - _default: function( event ) { - var target = event.target; - return rcheckableType.test( target.type ) && - target.click && nodeName( target, "input" ) && - dataPriv.get( target, "click" ) || - nodeName( target, "a" ); - } - }, - - beforeunload: { - postDispatch: function( event ) { - - // Support: Firefox 20+ - // Firefox doesn't alert if the returnValue field is not set. - if ( event.result !== undefined && event.originalEvent ) { - event.originalEvent.returnValue = event.result; - } - } - } - } -}; - -// Ensure the presence of an event listener that handles manually-triggered -// synthetic events by interrupting progress until reinvoked in response to -// *native* events that it fires directly, ensuring that state changes have -// already occurred before other listeners are invoked. -function leverageNative( el, type, isSetup ) { - - // Missing `isSetup` indicates a trigger call, which must force setup through jQuery.event.add - if ( !isSetup ) { - if ( dataPriv.get( el, type ) === undefined ) { - jQuery.event.add( el, type, returnTrue ); - } - return; - } - - // Register the controller as a special universal handler for all event namespaces - dataPriv.set( el, type, false ); - jQuery.event.add( el, type, { - namespace: false, - handler: function( event ) { - var result, - saved = dataPriv.get( this, type ); - - if ( ( event.isTrigger & 1 ) && this[ type ] ) { - - // Interrupt processing of the outer synthetic .trigger()ed event - if ( !saved ) { - - // Store arguments for use when handling the inner native event - // There will always be at least one argument (an event object), so this array - // will not be confused with a leftover capture object. - saved = slice.call( arguments ); - dataPriv.set( this, type, saved ); - - // Trigger the native event and capture its result - this[ type ](); - result = dataPriv.get( this, type ); - dataPriv.set( this, type, false ); - - if ( saved !== result ) { - - // Cancel the outer synthetic event - event.stopImmediatePropagation(); - event.preventDefault(); - - return result; - } - - // If this is an inner synthetic event for an event with a bubbling surrogate - // (focus or blur), assume that the surrogate already propagated from triggering - // the native event and prevent that from happening again here. - // This technically gets the ordering wrong w.r.t. to `.trigger()` (in which the - // bubbling surrogate propagates *after* the non-bubbling base), but that seems - // less bad than duplication. - } else if ( ( jQuery.event.special[ type ] || {} ).delegateType ) { - event.stopPropagation(); - } - - // If this is a native event triggered above, everything is now in order - // Fire an inner synthetic event with the original arguments - } else if ( saved ) { - - // ...and capture the result - dataPriv.set( this, type, jQuery.event.trigger( - saved[ 0 ], - saved.slice( 1 ), - this - ) ); - - // Abort handling of the native event by all jQuery handlers while allowing - // native handlers on the same element to run. On target, this is achieved - // by stopping immediate propagation just on the jQuery event. However, - // the native event is re-wrapped by a jQuery one on each level of the - // propagation so the only way to stop it for jQuery is to stop it for - // everyone via native `stopPropagation()`. This is not a problem for - // focus/blur which don't bubble, but it does also stop click on checkboxes - // and radios. We accept this limitation. - event.stopPropagation(); - event.isImmediatePropagationStopped = returnTrue; - } - } - } ); -} - -jQuery.removeEvent = function( elem, type, handle ) { - - // This "if" is needed for plain objects - if ( elem.removeEventListener ) { - elem.removeEventListener( type, handle ); - } -}; - -jQuery.Event = function( src, props ) { - - // Allow instantiation without the 'new' keyword - if ( !( this instanceof jQuery.Event ) ) { - return new jQuery.Event( src, props ); - } - - // Event object - if ( src && src.type ) { - this.originalEvent = src; - this.type = src.type; - - // Events bubbling up the document may have been marked as prevented - // by a handler lower down the tree; reflect the correct value. - this.isDefaultPrevented = src.defaultPrevented || - src.defaultPrevented === undefined && - - // Support: Android <=2.3 only - src.returnValue === false ? - returnTrue : - returnFalse; - - // Create target properties - // Support: Safari <=6 - 7 only - // Target should not be a text node (trac-504, trac-13143) - this.target = ( src.target && src.target.nodeType === 3 ) ? - src.target.parentNode : - src.target; - - this.currentTarget = src.currentTarget; - this.relatedTarget = src.relatedTarget; - - // Event type - } else { - this.type = src; - } - - // Put explicitly provided properties onto the event object - if ( props ) { - jQuery.extend( this, props ); - } - - // Create a timestamp if incoming event doesn't have one - this.timeStamp = src && src.timeStamp || Date.now(); - - // Mark it as fixed - this[ jQuery.expando ] = true; -}; - -// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding -// https://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html -jQuery.Event.prototype = { - constructor: jQuery.Event, - isDefaultPrevented: returnFalse, - isPropagationStopped: returnFalse, - isImmediatePropagationStopped: returnFalse, - isSimulated: false, - - preventDefault: function() { - var e = this.originalEvent; - - this.isDefaultPrevented = returnTrue; - - if ( e && !this.isSimulated ) { - e.preventDefault(); - } - }, - stopPropagation: function() { - var e = this.originalEvent; - - this.isPropagationStopped = returnTrue; - - if ( e && !this.isSimulated ) { - e.stopPropagation(); - } - }, - stopImmediatePropagation: function() { - var e = this.originalEvent; - - this.isImmediatePropagationStopped = returnTrue; - - if ( e && !this.isSimulated ) { - e.stopImmediatePropagation(); - } - - this.stopPropagation(); - } -}; - -// Includes all common event props including KeyEvent and MouseEvent specific props -jQuery.each( { - altKey: true, - bubbles: true, - cancelable: true, - changedTouches: true, - ctrlKey: true, - detail: true, - eventPhase: true, - metaKey: true, - pageX: true, - pageY: true, - shiftKey: true, - view: true, - "char": true, - code: true, - charCode: true, - key: true, - keyCode: true, - button: true, - buttons: true, - clientX: true, - clientY: true, - offsetX: true, - offsetY: true, - pointerId: true, - pointerType: true, - screenX: true, - screenY: true, - targetTouches: true, - toElement: true, - touches: true, - which: true -}, jQuery.event.addProp ); - -jQuery.each( { focus: "focusin", blur: "focusout" }, function( type, delegateType ) { - - function focusMappedHandler( nativeEvent ) { - if ( document.documentMode ) { - - // Support: IE 11+ - // Attach a single focusin/focusout handler on the document while someone wants - // focus/blur. This is because the former are synchronous in IE while the latter - // are async. In other browsers, all those handlers are invoked synchronously. - - // `handle` from private data would already wrap the event, but we need - // to change the `type` here. - var handle = dataPriv.get( this, "handle" ), - event = jQuery.event.fix( nativeEvent ); - event.type = nativeEvent.type === "focusin" ? "focus" : "blur"; - event.isSimulated = true; - - // First, handle focusin/focusout - handle( nativeEvent ); - - // ...then, handle focus/blur - // - // focus/blur don't bubble while focusin/focusout do; simulate the former by only - // invoking the handler at the lower level. - if ( event.target === event.currentTarget ) { - - // The setup part calls `leverageNative`, which, in turn, calls - // `jQuery.event.add`, so event handle will already have been set - // by this point. - handle( event ); - } - } else { - - // For non-IE browsers, attach a single capturing handler on the document - // while someone wants focusin/focusout. - jQuery.event.simulate( delegateType, nativeEvent.target, - jQuery.event.fix( nativeEvent ) ); - } - } - - jQuery.event.special[ type ] = { - - // Utilize native event if possible so blur/focus sequence is correct - setup: function() { - - var attaches; - - // Claim the first handler - // dataPriv.set( this, "focus", ... ) - // dataPriv.set( this, "blur", ... ) - leverageNative( this, type, true ); - - if ( document.documentMode ) { - - // Support: IE 9 - 11+ - // We use the same native handler for focusin & focus (and focusout & blur) - // so we need to coordinate setup & teardown parts between those events. - // Use `delegateType` as the key as `type` is already used by `leverageNative`. - attaches = dataPriv.get( this, delegateType ); - if ( !attaches ) { - this.addEventListener( delegateType, focusMappedHandler ); - } - dataPriv.set( this, delegateType, ( attaches || 0 ) + 1 ); - } else { - - // Return false to allow normal processing in the caller - return false; - } - }, - trigger: function() { - - // Force setup before trigger - leverageNative( this, type ); - - // Return non-false to allow normal event-path propagation - return true; - }, - - teardown: function() { - var attaches; - - if ( document.documentMode ) { - attaches = dataPriv.get( this, delegateType ) - 1; - if ( !attaches ) { - this.removeEventListener( delegateType, focusMappedHandler ); - dataPriv.remove( this, delegateType ); - } else { - dataPriv.set( this, delegateType, attaches ); - } - } else { - - // Return false to indicate standard teardown should be applied - return false; - } - }, - - // Suppress native focus or blur if we're currently inside - // a leveraged native-event stack - _default: function( event ) { - return dataPriv.get( event.target, type ); - }, - - delegateType: delegateType - }; - - // Support: Firefox <=44 - // Firefox doesn't have focus(in | out) events - // Related ticket - https://bugzilla.mozilla.org/show_bug.cgi?id=687787 - // - // Support: Chrome <=48 - 49, Safari <=9.0 - 9.1 - // focus(in | out) events fire after focus & blur events, - // which is spec violation - http://www.w3.org/TR/DOM-Level-3-Events/#events-focusevent-event-order - // Related ticket - https://bugs.chromium.org/p/chromium/issues/detail?id=449857 - // - // Support: IE 9 - 11+ - // To preserve relative focusin/focus & focusout/blur event order guaranteed on the 3.x branch, - // attach a single handler for both events in IE. - jQuery.event.special[ delegateType ] = { - setup: function() { - - // Handle: regular nodes (via `this.ownerDocument`), window - // (via `this.document`) & document (via `this`). - var doc = this.ownerDocument || this.document || this, - dataHolder = document.documentMode ? this : doc, - attaches = dataPriv.get( dataHolder, delegateType ); - - // Support: IE 9 - 11+ - // We use the same native handler for focusin & focus (and focusout & blur) - // so we need to coordinate setup & teardown parts between those events. - // Use `delegateType` as the key as `type` is already used by `leverageNative`. - if ( !attaches ) { - if ( document.documentMode ) { - this.addEventListener( delegateType, focusMappedHandler ); - } else { - doc.addEventListener( type, focusMappedHandler, true ); - } - } - dataPriv.set( dataHolder, delegateType, ( attaches || 0 ) + 1 ); - }, - teardown: function() { - var doc = this.ownerDocument || this.document || this, - dataHolder = document.documentMode ? this : doc, - attaches = dataPriv.get( dataHolder, delegateType ) - 1; - - if ( !attaches ) { - if ( document.documentMode ) { - this.removeEventListener( delegateType, focusMappedHandler ); - } else { - doc.removeEventListener( type, focusMappedHandler, true ); - } - dataPriv.remove( dataHolder, delegateType ); - } else { - dataPriv.set( dataHolder, delegateType, attaches ); - } - } - }; -} ); - -// Create mouseenter/leave events using mouseover/out and event-time checks -// so that event delegation works in jQuery. -// Do the same for pointerenter/pointerleave and pointerover/pointerout -// -// Support: Safari 7 only -// Safari sends mouseenter too often; see: -// https://bugs.chromium.org/p/chromium/issues/detail?id=470258 -// for the description of the bug (it existed in older Chrome versions as well). -jQuery.each( { - mouseenter: "mouseover", - mouseleave: "mouseout", - pointerenter: "pointerover", - pointerleave: "pointerout" -}, function( orig, fix ) { - jQuery.event.special[ orig ] = { - delegateType: fix, - bindType: fix, - - handle: function( event ) { - var ret, - target = this, - related = event.relatedTarget, - handleObj = event.handleObj; - - // For mouseenter/leave call the handler if related is outside the target. - // NB: No relatedTarget if the mouse left/entered the browser window - if ( !related || ( related !== target && !jQuery.contains( target, related ) ) ) { - event.type = handleObj.origType; - ret = handleObj.handler.apply( this, arguments ); - event.type = fix; - } - return ret; - } - }; -} ); - -jQuery.fn.extend( { - - on: function( types, selector, data, fn ) { - return on( this, types, selector, data, fn ); - }, - one: function( types, selector, data, fn ) { - return on( this, types, selector, data, fn, 1 ); - }, - off: function( types, selector, fn ) { - var handleObj, type; - if ( types && types.preventDefault && types.handleObj ) { - - // ( event ) dispatched jQuery.Event - handleObj = types.handleObj; - jQuery( types.delegateTarget ).off( - handleObj.namespace ? - handleObj.origType + "." + handleObj.namespace : - handleObj.origType, - handleObj.selector, - handleObj.handler - ); - return this; - } - if ( typeof types === "object" ) { - - // ( types-object [, selector] ) - for ( type in types ) { - this.off( type, selector, types[ type ] ); - } - return this; - } - if ( selector === false || typeof selector === "function" ) { - - // ( types [, fn] ) - fn = selector; - selector = undefined; - } - if ( fn === false ) { - fn = returnFalse; - } - return this.each( function() { - jQuery.event.remove( this, types, fn, selector ); - } ); - } -} ); - - -var - - // Support: IE <=10 - 11, Edge 12 - 13 only - // In IE/Edge using regex groups here causes severe slowdowns. - // See https://connect.microsoft.com/IE/feedback/details/1736512/ - rnoInnerhtml = /\s*$/g; - -// Prefer a tbody over its parent table for containing new rows -function manipulationTarget( elem, content ) { - if ( nodeName( elem, "table" ) && - nodeName( content.nodeType !== 11 ? content : content.firstChild, "tr" ) ) { - - return jQuery( elem ).children( "tbody" )[ 0 ] || elem; - } - - return elem; -} - -// Replace/restore the type attribute of script elements for safe DOM manipulation -function disableScript( elem ) { - elem.type = ( elem.getAttribute( "type" ) !== null ) + "/" + elem.type; - return elem; -} -function restoreScript( elem ) { - if ( ( elem.type || "" ).slice( 0, 5 ) === "true/" ) { - elem.type = elem.type.slice( 5 ); - } else { - elem.removeAttribute( "type" ); - } - - return elem; -} - -function cloneCopyEvent( src, dest ) { - var i, l, type, pdataOld, udataOld, udataCur, events; - - if ( dest.nodeType !== 1 ) { - return; - } - - // 1. Copy private data: events, handlers, etc. - if ( dataPriv.hasData( src ) ) { - pdataOld = dataPriv.get( src ); - events = pdataOld.events; - - if ( events ) { - dataPriv.remove( dest, "handle events" ); - - for ( type in events ) { - for ( i = 0, l = events[ type ].length; i < l; i++ ) { - jQuery.event.add( dest, type, events[ type ][ i ] ); - } - } - } - } - - // 2. Copy user data - if ( dataUser.hasData( src ) ) { - udataOld = dataUser.access( src ); - udataCur = jQuery.extend( {}, udataOld ); - - dataUser.set( dest, udataCur ); - } -} - -// Fix IE bugs, see support tests -function fixInput( src, dest ) { - var nodeName = dest.nodeName.toLowerCase(); - - // Fails to persist the checked state of a cloned checkbox or radio button. - if ( nodeName === "input" && rcheckableType.test( src.type ) ) { - dest.checked = src.checked; - - // Fails to return the selected option to the default selected state when cloning options - } else if ( nodeName === "input" || nodeName === "textarea" ) { - dest.defaultValue = src.defaultValue; - } -} - -function domManip( collection, args, callback, ignored ) { - - // Flatten any nested arrays - args = flat( args ); - - var fragment, first, scripts, hasScripts, node, doc, - i = 0, - l = collection.length, - iNoClone = l - 1, - value = args[ 0 ], - valueIsFunction = isFunction( value ); - - // We can't cloneNode fragments that contain checked, in WebKit - if ( valueIsFunction || - ( l > 1 && typeof value === "string" && - !support.checkClone && rchecked.test( value ) ) ) { - return collection.each( function( index ) { - var self = collection.eq( index ); - if ( valueIsFunction ) { - args[ 0 ] = value.call( this, index, self.html() ); - } - domManip( self, args, callback, ignored ); - } ); - } - - if ( l ) { - fragment = buildFragment( args, collection[ 0 ].ownerDocument, false, collection, ignored ); - first = fragment.firstChild; - - if ( fragment.childNodes.length === 1 ) { - fragment = first; - } - - // Require either new content or an interest in ignored elements to invoke the callback - if ( first || ignored ) { - scripts = jQuery.map( getAll( fragment, "script" ), disableScript ); - hasScripts = scripts.length; - - // Use the original fragment for the last item - // instead of the first because it can end up - // being emptied incorrectly in certain situations (trac-8070). - for ( ; i < l; i++ ) { - node = fragment; - - if ( i !== iNoClone ) { - node = jQuery.clone( node, true, true ); - - // Keep references to cloned scripts for later restoration - if ( hasScripts ) { - - // Support: Android <=4.0 only, PhantomJS 1 only - // push.apply(_, arraylike) throws on ancient WebKit - jQuery.merge( scripts, getAll( node, "script" ) ); - } - } - - callback.call( collection[ i ], node, i ); - } - - if ( hasScripts ) { - doc = scripts[ scripts.length - 1 ].ownerDocument; - - // Re-enable scripts - jQuery.map( scripts, restoreScript ); - - // Evaluate executable scripts on first document insertion - for ( i = 0; i < hasScripts; i++ ) { - node = scripts[ i ]; - if ( rscriptType.test( node.type || "" ) && - !dataPriv.access( node, "globalEval" ) && - jQuery.contains( doc, node ) ) { - - if ( node.src && ( node.type || "" ).toLowerCase() !== "module" ) { - - // Optional AJAX dependency, but won't run scripts if not present - if ( jQuery._evalUrl && !node.noModule ) { - jQuery._evalUrl( node.src, { - nonce: node.nonce || node.getAttribute( "nonce" ) - }, doc ); - } - } else { - - // Unwrap a CDATA section containing script contents. This shouldn't be - // needed as in XML documents they're already not visible when - // inspecting element contents and in HTML documents they have no - // meaning but we're preserving that logic for backwards compatibility. - // This will be removed completely in 4.0. See gh-4904. - DOMEval( node.textContent.replace( rcleanScript, "" ), node, doc ); - } - } - } - } - } - } - - return collection; -} - -function remove( elem, selector, keepData ) { - var node, - nodes = selector ? jQuery.filter( selector, elem ) : elem, - i = 0; - - for ( ; ( node = nodes[ i ] ) != null; i++ ) { - if ( !keepData && node.nodeType === 1 ) { - jQuery.cleanData( getAll( node ) ); - } - - if ( node.parentNode ) { - if ( keepData && isAttached( node ) ) { - setGlobalEval( getAll( node, "script" ) ); - } - node.parentNode.removeChild( node ); - } - } - - return elem; -} - -jQuery.extend( { - htmlPrefilter: function( html ) { - return html; - }, - - clone: function( elem, dataAndEvents, deepDataAndEvents ) { - var i, l, srcElements, destElements, - clone = elem.cloneNode( true ), - inPage = isAttached( elem ); - - // Fix IE cloning issues - if ( !support.noCloneChecked && ( elem.nodeType === 1 || elem.nodeType === 11 ) && - !jQuery.isXMLDoc( elem ) ) { - - // We eschew jQuery#find here for performance reasons: - // https://jsperf.com/getall-vs-sizzle/2 - destElements = getAll( clone ); - srcElements = getAll( elem ); - - for ( i = 0, l = srcElements.length; i < l; i++ ) { - fixInput( srcElements[ i ], destElements[ i ] ); - } - } - - // Copy the events from the original to the clone - if ( dataAndEvents ) { - if ( deepDataAndEvents ) { - srcElements = srcElements || getAll( elem ); - destElements = destElements || getAll( clone ); - - for ( i = 0, l = srcElements.length; i < l; i++ ) { - cloneCopyEvent( srcElements[ i ], destElements[ i ] ); - } - } else { - cloneCopyEvent( elem, clone ); - } - } - - // Preserve script evaluation history - destElements = getAll( clone, "script" ); - if ( destElements.length > 0 ) { - setGlobalEval( destElements, !inPage && getAll( elem, "script" ) ); - } - - // Return the cloned set - return clone; - }, - - cleanData: function( elems ) { - var data, elem, type, - special = jQuery.event.special, - i = 0; - - for ( ; ( elem = elems[ i ] ) !== undefined; i++ ) { - if ( acceptData( elem ) ) { - if ( ( data = elem[ dataPriv.expando ] ) ) { - if ( data.events ) { - for ( type in data.events ) { - if ( special[ type ] ) { - jQuery.event.remove( elem, type ); - - // This is a shortcut to avoid jQuery.event.remove's overhead - } else { - jQuery.removeEvent( elem, type, data.handle ); - } - } - } - - // Support: Chrome <=35 - 45+ - // Assign undefined instead of using delete, see Data#remove - elem[ dataPriv.expando ] = undefined; - } - if ( elem[ dataUser.expando ] ) { - - // Support: Chrome <=35 - 45+ - // Assign undefined instead of using delete, see Data#remove - elem[ dataUser.expando ] = undefined; - } - } - } - } -} ); - -jQuery.fn.extend( { - detach: function( selector ) { - return remove( this, selector, true ); - }, - - remove: function( selector ) { - return remove( this, selector ); - }, - - text: function( value ) { - return access( this, function( value ) { - return value === undefined ? - jQuery.text( this ) : - this.empty().each( function() { - if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { - this.textContent = value; - } - } ); - }, null, value, arguments.length ); - }, - - append: function() { - return domManip( this, arguments, function( elem ) { - if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { - var target = manipulationTarget( this, elem ); - target.appendChild( elem ); - } - } ); - }, - - prepend: function() { - return domManip( this, arguments, function( elem ) { - if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { - var target = manipulationTarget( this, elem ); - target.insertBefore( elem, target.firstChild ); - } - } ); - }, - - before: function() { - return domManip( this, arguments, function( elem ) { - if ( this.parentNode ) { - this.parentNode.insertBefore( elem, this ); - } - } ); - }, - - after: function() { - return domManip( this, arguments, function( elem ) { - if ( this.parentNode ) { - this.parentNode.insertBefore( elem, this.nextSibling ); - } - } ); - }, - - empty: function() { - var elem, - i = 0; - - for ( ; ( elem = this[ i ] ) != null; i++ ) { - if ( elem.nodeType === 1 ) { - - // Prevent memory leaks - jQuery.cleanData( getAll( elem, false ) ); - - // Remove any remaining nodes - elem.textContent = ""; - } - } - - return this; - }, - - clone: function( dataAndEvents, deepDataAndEvents ) { - dataAndEvents = dataAndEvents == null ? false : dataAndEvents; - deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents; - - return this.map( function() { - return jQuery.clone( this, dataAndEvents, deepDataAndEvents ); - } ); - }, - - html: function( value ) { - return access( this, function( value ) { - var elem = this[ 0 ] || {}, - i = 0, - l = this.length; - - if ( value === undefined && elem.nodeType === 1 ) { - return elem.innerHTML; - } - - // See if we can take a shortcut and just use innerHTML - if ( typeof value === "string" && !rnoInnerhtml.test( value ) && - !wrapMap[ ( rtagName.exec( value ) || [ "", "" ] )[ 1 ].toLowerCase() ] ) { - - value = jQuery.htmlPrefilter( value ); - - try { - for ( ; i < l; i++ ) { - elem = this[ i ] || {}; - - // Remove element nodes and prevent memory leaks - if ( elem.nodeType === 1 ) { - jQuery.cleanData( getAll( elem, false ) ); - elem.innerHTML = value; - } - } - - elem = 0; - - // If using innerHTML throws an exception, use the fallback method - } catch ( e ) {} - } - - if ( elem ) { - this.empty().append( value ); - } - }, null, value, arguments.length ); - }, - - replaceWith: function() { - var ignored = []; - - // Make the changes, replacing each non-ignored context element with the new content - return domManip( this, arguments, function( elem ) { - var parent = this.parentNode; - - if ( jQuery.inArray( this, ignored ) < 0 ) { - jQuery.cleanData( getAll( this ) ); - if ( parent ) { - parent.replaceChild( elem, this ); - } - } - - // Force callback invocation - }, ignored ); - } -} ); - -jQuery.each( { - appendTo: "append", - prependTo: "prepend", - insertBefore: "before", - insertAfter: "after", - replaceAll: "replaceWith" -}, function( name, original ) { - jQuery.fn[ name ] = function( selector ) { - var elems, - ret = [], - insert = jQuery( selector ), - last = insert.length - 1, - i = 0; - - for ( ; i <= last; i++ ) { - elems = i === last ? this : this.clone( true ); - jQuery( insert[ i ] )[ original ]( elems ); - - // Support: Android <=4.0 only, PhantomJS 1 only - // .get() because push.apply(_, arraylike) throws on ancient WebKit - push.apply( ret, elems.get() ); - } - - return this.pushStack( ret ); - }; -} ); -var rnumnonpx = new RegExp( "^(" + pnum + ")(?!px)[a-z%]+$", "i" ); - -var rcustomProp = /^--/; - - -var getStyles = function( elem ) { - - // Support: IE <=11 only, Firefox <=30 (trac-15098, trac-14150) - // IE throws on elements created in popups - // FF meanwhile throws on frame elements through "defaultView.getComputedStyle" - var view = elem.ownerDocument.defaultView; - - if ( !view || !view.opener ) { - view = window; - } - - return view.getComputedStyle( elem ); - }; - -var swap = function( elem, options, callback ) { - var ret, name, - old = {}; - - // Remember the old values, and insert the new ones - for ( name in options ) { - old[ name ] = elem.style[ name ]; - elem.style[ name ] = options[ name ]; - } - - ret = callback.call( elem ); - - // Revert the old values - for ( name in options ) { - elem.style[ name ] = old[ name ]; - } - - return ret; -}; - - -var rboxStyle = new RegExp( cssExpand.join( "|" ), "i" ); - - - -( function() { - - // Executing both pixelPosition & boxSizingReliable tests require only one layout - // so they're executed at the same time to save the second computation. - function computeStyleTests() { - - // This is a singleton, we need to execute it only once - if ( !div ) { - return; - } - - container.style.cssText = "position:absolute;left:-11111px;width:60px;" + - "margin-top:1px;padding:0;border:0"; - div.style.cssText = - "position:relative;display:block;box-sizing:border-box;overflow:scroll;" + - "margin:auto;border:1px;padding:1px;" + - "width:60%;top:1%"; - documentElement.appendChild( container ).appendChild( div ); - - var divStyle = window.getComputedStyle( div ); - pixelPositionVal = divStyle.top !== "1%"; - - // Support: Android 4.0 - 4.3 only, Firefox <=3 - 44 - reliableMarginLeftVal = roundPixelMeasures( divStyle.marginLeft ) === 12; - - // Support: Android 4.0 - 4.3 only, Safari <=9.1 - 10.1, iOS <=7.0 - 9.3 - // Some styles come back with percentage values, even though they shouldn't - div.style.right = "60%"; - pixelBoxStylesVal = roundPixelMeasures( divStyle.right ) === 36; - - // Support: IE 9 - 11 only - // Detect misreporting of content dimensions for box-sizing:border-box elements - boxSizingReliableVal = roundPixelMeasures( divStyle.width ) === 36; - - // Support: IE 9 only - // Detect overflow:scroll screwiness (gh-3699) - // Support: Chrome <=64 - // Don't get tricked when zoom affects offsetWidth (gh-4029) - div.style.position = "absolute"; - scrollboxSizeVal = roundPixelMeasures( div.offsetWidth / 3 ) === 12; - - documentElement.removeChild( container ); - - // Nullify the div so it wouldn't be stored in the memory and - // it will also be a sign that checks already performed - div = null; - } - - function roundPixelMeasures( measure ) { - return Math.round( parseFloat( measure ) ); - } - - var pixelPositionVal, boxSizingReliableVal, scrollboxSizeVal, pixelBoxStylesVal, - reliableTrDimensionsVal, reliableMarginLeftVal, - container = document.createElement( "div" ), - div = document.createElement( "div" ); - - // Finish early in limited (non-browser) environments - if ( !div.style ) { - return; - } - - // Support: IE <=9 - 11 only - // Style of cloned element affects source element cloned (trac-8908) - div.style.backgroundClip = "content-box"; - div.cloneNode( true ).style.backgroundClip = ""; - support.clearCloneStyle = div.style.backgroundClip === "content-box"; - - jQuery.extend( support, { - boxSizingReliable: function() { - computeStyleTests(); - return boxSizingReliableVal; - }, - pixelBoxStyles: function() { - computeStyleTests(); - return pixelBoxStylesVal; - }, - pixelPosition: function() { - computeStyleTests(); - return pixelPositionVal; - }, - reliableMarginLeft: function() { - computeStyleTests(); - return reliableMarginLeftVal; - }, - scrollboxSize: function() { - computeStyleTests(); - return scrollboxSizeVal; - }, - - // Support: IE 9 - 11+, Edge 15 - 18+ - // IE/Edge misreport `getComputedStyle` of table rows with width/height - // set in CSS while `offset*` properties report correct values. - // Behavior in IE 9 is more subtle than in newer versions & it passes - // some versions of this test; make sure not to make it pass there! - // - // Support: Firefox 70+ - // Only Firefox includes border widths - // in computed dimensions. (gh-4529) - reliableTrDimensions: function() { - var table, tr, trChild, trStyle; - if ( reliableTrDimensionsVal == null ) { - table = document.createElement( "table" ); - tr = document.createElement( "tr" ); - trChild = document.createElement( "div" ); - - table.style.cssText = "position:absolute;left:-11111px;border-collapse:separate"; - tr.style.cssText = "box-sizing:content-box;border:1px solid"; - - // Support: Chrome 86+ - // Height set through cssText does not get applied. - // Computed height then comes back as 0. - tr.style.height = "1px"; - trChild.style.height = "9px"; - - // Support: Android 8 Chrome 86+ - // In our bodyBackground.html iframe, - // display for all div elements is set to "inline", - // which causes a problem only in Android 8 Chrome 86. - // Ensuring the div is `display: block` - // gets around this issue. - trChild.style.display = "block"; - - documentElement - .appendChild( table ) - .appendChild( tr ) - .appendChild( trChild ); - - trStyle = window.getComputedStyle( tr ); - reliableTrDimensionsVal = ( parseInt( trStyle.height, 10 ) + - parseInt( trStyle.borderTopWidth, 10 ) + - parseInt( trStyle.borderBottomWidth, 10 ) ) === tr.offsetHeight; - - documentElement.removeChild( table ); - } - return reliableTrDimensionsVal; - } - } ); -} )(); - - -function curCSS( elem, name, computed ) { - var width, minWidth, maxWidth, ret, - isCustomProp = rcustomProp.test( name ), - - // Support: Firefox 51+ - // Retrieving style before computed somehow - // fixes an issue with getting wrong values - // on detached elements - style = elem.style; - - computed = computed || getStyles( elem ); - - // getPropertyValue is needed for: - // .css('filter') (IE 9 only, trac-12537) - // .css('--customProperty) (gh-3144) - if ( computed ) { - - // Support: IE <=9 - 11+ - // IE only supports `"float"` in `getPropertyValue`; in computed styles - // it's only available as `"cssFloat"`. We no longer modify properties - // sent to `.css()` apart from camelCasing, so we need to check both. - // Normally, this would create difference in behavior: if - // `getPropertyValue` returns an empty string, the value returned - // by `.css()` would be `undefined`. This is usually the case for - // disconnected elements. However, in IE even disconnected elements - // with no styles return `"none"` for `getPropertyValue( "float" )` - ret = computed.getPropertyValue( name ) || computed[ name ]; - - if ( isCustomProp && ret ) { - - // Support: Firefox 105+, Chrome <=105+ - // Spec requires trimming whitespace for custom properties (gh-4926). - // Firefox only trims leading whitespace. Chrome just collapses - // both leading & trailing whitespace to a single space. - // - // Fall back to `undefined` if empty string returned. - // This collapses a missing definition with property defined - // and set to an empty string but there's no standard API - // allowing us to differentiate them without a performance penalty - // and returning `undefined` aligns with older jQuery. - // - // rtrimCSS treats U+000D CARRIAGE RETURN and U+000C FORM FEED - // as whitespace while CSS does not, but this is not a problem - // because CSS preprocessing replaces them with U+000A LINE FEED - // (which *is* CSS whitespace) - // https://www.w3.org/TR/css-syntax-3/#input-preprocessing - ret = ret.replace( rtrimCSS, "$1" ) || undefined; - } - - if ( ret === "" && !isAttached( elem ) ) { - ret = jQuery.style( elem, name ); - } - - // A tribute to the "awesome hack by Dean Edwards" - // Android Browser returns percentage for some values, - // but width seems to be reliably pixels. - // This is against the CSSOM draft spec: - // https://drafts.csswg.org/cssom/#resolved-values - if ( !support.pixelBoxStyles() && rnumnonpx.test( ret ) && rboxStyle.test( name ) ) { - - // Remember the original values - width = style.width; - minWidth = style.minWidth; - maxWidth = style.maxWidth; - - // Put in the new values to get a computed value out - style.minWidth = style.maxWidth = style.width = ret; - ret = computed.width; - - // Revert the changed values - style.width = width; - style.minWidth = minWidth; - style.maxWidth = maxWidth; - } - } - - return ret !== undefined ? - - // Support: IE <=9 - 11 only - // IE returns zIndex value as an integer. - ret + "" : - ret; -} - - -function addGetHookIf( conditionFn, hookFn ) { - - // Define the hook, we'll check on the first run if it's really needed. - return { - get: function() { - if ( conditionFn() ) { - - // Hook not needed (or it's not possible to use it due - // to missing dependency), remove it. - delete this.get; - return; - } - - // Hook needed; redefine it so that the support test is not executed again. - return ( this.get = hookFn ).apply( this, arguments ); - } - }; -} - - -var cssPrefixes = [ "Webkit", "Moz", "ms" ], - emptyStyle = document.createElement( "div" ).style, - vendorProps = {}; - -// Return a vendor-prefixed property or undefined -function vendorPropName( name ) { - - // Check for vendor prefixed names - var capName = name[ 0 ].toUpperCase() + name.slice( 1 ), - i = cssPrefixes.length; - - while ( i-- ) { - name = cssPrefixes[ i ] + capName; - if ( name in emptyStyle ) { - return name; - } - } -} - -// Return a potentially-mapped jQuery.cssProps or vendor prefixed property -function finalPropName( name ) { - var final = jQuery.cssProps[ name ] || vendorProps[ name ]; - - if ( final ) { - return final; - } - if ( name in emptyStyle ) { - return name; - } - return vendorProps[ name ] = vendorPropName( name ) || name; -} - - -var - - // Swappable if display is none or starts with table - // except "table", "table-cell", or "table-caption" - // See here for display values: https://developer.mozilla.org/en-US/docs/CSS/display - rdisplayswap = /^(none|table(?!-c[ea]).+)/, - cssShow = { position: "absolute", visibility: "hidden", display: "block" }, - cssNormalTransform = { - letterSpacing: "0", - fontWeight: "400" - }; - -function setPositiveNumber( _elem, value, subtract ) { - - // Any relative (+/-) values have already been - // normalized at this point - var matches = rcssNum.exec( value ); - return matches ? - - // Guard against undefined "subtract", e.g., when used as in cssHooks - Math.max( 0, matches[ 2 ] - ( subtract || 0 ) ) + ( matches[ 3 ] || "px" ) : - value; -} - -function boxModelAdjustment( elem, dimension, box, isBorderBox, styles, computedVal ) { - var i = dimension === "width" ? 1 : 0, - extra = 0, - delta = 0, - marginDelta = 0; - - // Adjustment may not be necessary - if ( box === ( isBorderBox ? "border" : "content" ) ) { - return 0; - } - - for ( ; i < 4; i += 2 ) { - - // Both box models exclude margin - // Count margin delta separately to only add it after scroll gutter adjustment. - // This is needed to make negative margins work with `outerHeight( true )` (gh-3982). - if ( box === "margin" ) { - marginDelta += jQuery.css( elem, box + cssExpand[ i ], true, styles ); - } - - // If we get here with a content-box, we're seeking "padding" or "border" or "margin" - if ( !isBorderBox ) { - - // Add padding - delta += jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); - - // For "border" or "margin", add border - if ( box !== "padding" ) { - delta += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); - - // But still keep track of it otherwise - } else { - extra += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); - } - - // If we get here with a border-box (content + padding + border), we're seeking "content" or - // "padding" or "margin" - } else { - - // For "content", subtract padding - if ( box === "content" ) { - delta -= jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); - } - - // For "content" or "padding", subtract border - if ( box !== "margin" ) { - delta -= jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); - } - } - } - - // Account for positive content-box scroll gutter when requested by providing computedVal - if ( !isBorderBox && computedVal >= 0 ) { - - // offsetWidth/offsetHeight is a rounded sum of content, padding, scroll gutter, and border - // Assuming integer scroll gutter, subtract the rest and round down - delta += Math.max( 0, Math.ceil( - elem[ "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ) ] - - computedVal - - delta - - extra - - 0.5 - - // If offsetWidth/offsetHeight is unknown, then we can't determine content-box scroll gutter - // Use an explicit zero to avoid NaN (gh-3964) - ) ) || 0; - } - - return delta + marginDelta; -} - -function getWidthOrHeight( elem, dimension, extra ) { - - // Start with computed style - var styles = getStyles( elem ), - - // To avoid forcing a reflow, only fetch boxSizing if we need it (gh-4322). - // Fake content-box until we know it's needed to know the true value. - boxSizingNeeded = !support.boxSizingReliable() || extra, - isBorderBox = boxSizingNeeded && - jQuery.css( elem, "boxSizing", false, styles ) === "border-box", - valueIsBorderBox = isBorderBox, - - val = curCSS( elem, dimension, styles ), - offsetProp = "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ); - - // Support: Firefox <=54 - // Return a confounding non-pixel value or feign ignorance, as appropriate. - if ( rnumnonpx.test( val ) ) { - if ( !extra ) { - return val; - } - val = "auto"; - } - - - // Support: IE 9 - 11 only - // Use offsetWidth/offsetHeight for when box sizing is unreliable. - // In those cases, the computed value can be trusted to be border-box. - if ( ( !support.boxSizingReliable() && isBorderBox || - - // Support: IE 10 - 11+, Edge 15 - 18+ - // IE/Edge misreport `getComputedStyle` of table rows with width/height - // set in CSS while `offset*` properties report correct values. - // Interestingly, in some cases IE 9 doesn't suffer from this issue. - !support.reliableTrDimensions() && nodeName( elem, "tr" ) || - - // Fall back to offsetWidth/offsetHeight when value is "auto" - // This happens for inline elements with no explicit setting (gh-3571) - val === "auto" || - - // Support: Android <=4.1 - 4.3 only - // Also use offsetWidth/offsetHeight for misreported inline dimensions (gh-3602) - !parseFloat( val ) && jQuery.css( elem, "display", false, styles ) === "inline" ) && - - // Make sure the element is visible & connected - elem.getClientRects().length ) { - - isBorderBox = jQuery.css( elem, "boxSizing", false, styles ) === "border-box"; - - // Where available, offsetWidth/offsetHeight approximate border box dimensions. - // Where not available (e.g., SVG), assume unreliable box-sizing and interpret the - // retrieved value as a content box dimension. - valueIsBorderBox = offsetProp in elem; - if ( valueIsBorderBox ) { - val = elem[ offsetProp ]; - } - } - - // Normalize "" and auto - val = parseFloat( val ) || 0; - - // Adjust for the element's box model - return ( val + - boxModelAdjustment( - elem, - dimension, - extra || ( isBorderBox ? "border" : "content" ), - valueIsBorderBox, - styles, - - // Provide the current computed size to request scroll gutter calculation (gh-3589) - val - ) - ) + "px"; -} - -jQuery.extend( { - - // Add in style property hooks for overriding the default - // behavior of getting and setting a style property - cssHooks: { - opacity: { - get: function( elem, computed ) { - if ( computed ) { - - // We should always get a number back from opacity - var ret = curCSS( elem, "opacity" ); - return ret === "" ? "1" : ret; - } - } - } - }, - - // Don't automatically add "px" to these possibly-unitless properties - cssNumber: { - animationIterationCount: true, - aspectRatio: true, - borderImageSlice: true, - columnCount: true, - flexGrow: true, - flexShrink: true, - fontWeight: true, - gridArea: true, - gridColumn: true, - gridColumnEnd: true, - gridColumnStart: true, - gridRow: true, - gridRowEnd: true, - gridRowStart: true, - lineHeight: true, - opacity: true, - order: true, - orphans: true, - scale: true, - widows: true, - zIndex: true, - zoom: true, - - // SVG-related - fillOpacity: true, - floodOpacity: true, - stopOpacity: true, - strokeMiterlimit: true, - strokeOpacity: true - }, - - // Add in properties whose names you wish to fix before - // setting or getting the value - cssProps: {}, - - // Get and set the style property on a DOM Node - style: function( elem, name, value, extra ) { - - // Don't set styles on text and comment nodes - if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) { - return; - } - - // Make sure that we're working with the right name - var ret, type, hooks, - origName = camelCase( name ), - isCustomProp = rcustomProp.test( name ), - style = elem.style; - - // Make sure that we're working with the right name. We don't - // want to query the value if it is a CSS custom property - // since they are user-defined. - if ( !isCustomProp ) { - name = finalPropName( origName ); - } - - // Gets hook for the prefixed version, then unprefixed version - hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; - - // Check if we're setting a value - if ( value !== undefined ) { - type = typeof value; - - // Convert "+=" or "-=" to relative numbers (trac-7345) - if ( type === "string" && ( ret = rcssNum.exec( value ) ) && ret[ 1 ] ) { - value = adjustCSS( elem, name, ret ); - - // Fixes bug trac-9237 - type = "number"; - } - - // Make sure that null and NaN values aren't set (trac-7116) - if ( value == null || value !== value ) { - return; - } - - // If a number was passed in, add the unit (except for certain CSS properties) - // The isCustomProp check can be removed in jQuery 4.0 when we only auto-append - // "px" to a few hardcoded values. - if ( type === "number" && !isCustomProp ) { - value += ret && ret[ 3 ] || ( jQuery.cssNumber[ origName ] ? "" : "px" ); - } - - // background-* props affect original clone's values - if ( !support.clearCloneStyle && value === "" && name.indexOf( "background" ) === 0 ) { - style[ name ] = "inherit"; - } - - // If a hook was provided, use that value, otherwise just set the specified value - if ( !hooks || !( "set" in hooks ) || - ( value = hooks.set( elem, value, extra ) ) !== undefined ) { - - if ( isCustomProp ) { - style.setProperty( name, value ); - } else { - style[ name ] = value; - } - } - - } else { - - // If a hook was provided get the non-computed value from there - if ( hooks && "get" in hooks && - ( ret = hooks.get( elem, false, extra ) ) !== undefined ) { - - return ret; - } - - // Otherwise just get the value from the style object - return style[ name ]; - } - }, - - css: function( elem, name, extra, styles ) { - var val, num, hooks, - origName = camelCase( name ), - isCustomProp = rcustomProp.test( name ); - - // Make sure that we're working with the right name. We don't - // want to modify the value if it is a CSS custom property - // since they are user-defined. - if ( !isCustomProp ) { - name = finalPropName( origName ); - } - - // Try prefixed name followed by the unprefixed name - hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; - - // If a hook was provided get the computed value from there - if ( hooks && "get" in hooks ) { - val = hooks.get( elem, true, extra ); - } - - // Otherwise, if a way to get the computed value exists, use that - if ( val === undefined ) { - val = curCSS( elem, name, styles ); - } - - // Convert "normal" to computed value - if ( val === "normal" && name in cssNormalTransform ) { - val = cssNormalTransform[ name ]; - } - - // Make numeric if forced or a qualifier was provided and val looks numeric - if ( extra === "" || extra ) { - num = parseFloat( val ); - return extra === true || isFinite( num ) ? num || 0 : val; - } - - return val; - } -} ); - -jQuery.each( [ "height", "width" ], function( _i, dimension ) { - jQuery.cssHooks[ dimension ] = { - get: function( elem, computed, extra ) { - if ( computed ) { - - // Certain elements can have dimension info if we invisibly show them - // but it must have a current display style that would benefit - return rdisplayswap.test( jQuery.css( elem, "display" ) ) && - - // Support: Safari 8+ - // Table columns in Safari have non-zero offsetWidth & zero - // getBoundingClientRect().width unless display is changed. - // Support: IE <=11 only - // Running getBoundingClientRect on a disconnected node - // in IE throws an error. - ( !elem.getClientRects().length || !elem.getBoundingClientRect().width ) ? - swap( elem, cssShow, function() { - return getWidthOrHeight( elem, dimension, extra ); - } ) : - getWidthOrHeight( elem, dimension, extra ); - } - }, - - set: function( elem, value, extra ) { - var matches, - styles = getStyles( elem ), - - // Only read styles.position if the test has a chance to fail - // to avoid forcing a reflow. - scrollboxSizeBuggy = !support.scrollboxSize() && - styles.position === "absolute", - - // To avoid forcing a reflow, only fetch boxSizing if we need it (gh-3991) - boxSizingNeeded = scrollboxSizeBuggy || extra, - isBorderBox = boxSizingNeeded && - jQuery.css( elem, "boxSizing", false, styles ) === "border-box", - subtract = extra ? - boxModelAdjustment( - elem, - dimension, - extra, - isBorderBox, - styles - ) : - 0; - - // Account for unreliable border-box dimensions by comparing offset* to computed and - // faking a content-box to get border and padding (gh-3699) - if ( isBorderBox && scrollboxSizeBuggy ) { - subtract -= Math.ceil( - elem[ "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ) ] - - parseFloat( styles[ dimension ] ) - - boxModelAdjustment( elem, dimension, "border", false, styles ) - - 0.5 - ); - } - - // Convert to pixels if value adjustment is needed - if ( subtract && ( matches = rcssNum.exec( value ) ) && - ( matches[ 3 ] || "px" ) !== "px" ) { - - elem.style[ dimension ] = value; - value = jQuery.css( elem, dimension ); - } - - return setPositiveNumber( elem, value, subtract ); - } - }; -} ); - -jQuery.cssHooks.marginLeft = addGetHookIf( support.reliableMarginLeft, - function( elem, computed ) { - if ( computed ) { - return ( parseFloat( curCSS( elem, "marginLeft" ) ) || - elem.getBoundingClientRect().left - - swap( elem, { marginLeft: 0 }, function() { - return elem.getBoundingClientRect().left; - } ) - ) + "px"; - } - } -); - -// These hooks are used by animate to expand properties -jQuery.each( { - margin: "", - padding: "", - border: "Width" -}, function( prefix, suffix ) { - jQuery.cssHooks[ prefix + suffix ] = { - expand: function( value ) { - var i = 0, - expanded = {}, - - // Assumes a single number if not a string - parts = typeof value === "string" ? value.split( " " ) : [ value ]; - - for ( ; i < 4; i++ ) { - expanded[ prefix + cssExpand[ i ] + suffix ] = - parts[ i ] || parts[ i - 2 ] || parts[ 0 ]; - } - - return expanded; - } - }; - - if ( prefix !== "margin" ) { - jQuery.cssHooks[ prefix + suffix ].set = setPositiveNumber; - } -} ); - -jQuery.fn.extend( { - css: function( name, value ) { - return access( this, function( elem, name, value ) { - var styles, len, - map = {}, - i = 0; - - if ( Array.isArray( name ) ) { - styles = getStyles( elem ); - len = name.length; - - for ( ; i < len; i++ ) { - map[ name[ i ] ] = jQuery.css( elem, name[ i ], false, styles ); - } - - return map; - } - - return value !== undefined ? - jQuery.style( elem, name, value ) : - jQuery.css( elem, name ); - }, name, value, arguments.length > 1 ); - } -} ); - - -function Tween( elem, options, prop, end, easing ) { - return new Tween.prototype.init( elem, options, prop, end, easing ); -} -jQuery.Tween = Tween; - -Tween.prototype = { - constructor: Tween, - init: function( elem, options, prop, end, easing, unit ) { - this.elem = elem; - this.prop = prop; - this.easing = easing || jQuery.easing._default; - this.options = options; - this.start = this.now = this.cur(); - this.end = end; - this.unit = unit || ( jQuery.cssNumber[ prop ] ? "" : "px" ); - }, - cur: function() { - var hooks = Tween.propHooks[ this.prop ]; - - return hooks && hooks.get ? - hooks.get( this ) : - Tween.propHooks._default.get( this ); - }, - run: function( percent ) { - var eased, - hooks = Tween.propHooks[ this.prop ]; - - if ( this.options.duration ) { - this.pos = eased = jQuery.easing[ this.easing ]( - percent, this.options.duration * percent, 0, 1, this.options.duration - ); - } else { - this.pos = eased = percent; - } - this.now = ( this.end - this.start ) * eased + this.start; - - if ( this.options.step ) { - this.options.step.call( this.elem, this.now, this ); - } - - if ( hooks && hooks.set ) { - hooks.set( this ); - } else { - Tween.propHooks._default.set( this ); - } - return this; - } -}; - -Tween.prototype.init.prototype = Tween.prototype; - -Tween.propHooks = { - _default: { - get: function( tween ) { - var result; - - // Use a property on the element directly when it is not a DOM element, - // or when there is no matching style property that exists. - if ( tween.elem.nodeType !== 1 || - tween.elem[ tween.prop ] != null && tween.elem.style[ tween.prop ] == null ) { - return tween.elem[ tween.prop ]; - } - - // Passing an empty string as a 3rd parameter to .css will automatically - // attempt a parseFloat and fallback to a string if the parse fails. - // Simple values such as "10px" are parsed to Float; - // complex values such as "rotate(1rad)" are returned as-is. - result = jQuery.css( tween.elem, tween.prop, "" ); - - // Empty strings, null, undefined and "auto" are converted to 0. - return !result || result === "auto" ? 0 : result; - }, - set: function( tween ) { - - // Use step hook for back compat. - // Use cssHook if its there. - // Use .style if available and use plain properties where available. - if ( jQuery.fx.step[ tween.prop ] ) { - jQuery.fx.step[ tween.prop ]( tween ); - } else if ( tween.elem.nodeType === 1 && ( - jQuery.cssHooks[ tween.prop ] || - tween.elem.style[ finalPropName( tween.prop ) ] != null ) ) { - jQuery.style( tween.elem, tween.prop, tween.now + tween.unit ); - } else { - tween.elem[ tween.prop ] = tween.now; - } - } - } -}; - -// Support: IE <=9 only -// Panic based approach to setting things on disconnected nodes -Tween.propHooks.scrollTop = Tween.propHooks.scrollLeft = { - set: function( tween ) { - if ( tween.elem.nodeType && tween.elem.parentNode ) { - tween.elem[ tween.prop ] = tween.now; - } - } -}; - -jQuery.easing = { - linear: function( p ) { - return p; - }, - swing: function( p ) { - return 0.5 - Math.cos( p * Math.PI ) / 2; - }, - _default: "swing" -}; - -jQuery.fx = Tween.prototype.init; - -// Back compat <1.8 extension point -jQuery.fx.step = {}; - - - - -var - fxNow, inProgress, - rfxtypes = /^(?:toggle|show|hide)$/, - rrun = /queueHooks$/; - -function schedule() { - if ( inProgress ) { - if ( document.hidden === false && window.requestAnimationFrame ) { - window.requestAnimationFrame( schedule ); - } else { - window.setTimeout( schedule, jQuery.fx.interval ); - } - - jQuery.fx.tick(); - } -} - -// Animations created synchronously will run synchronously -function createFxNow() { - window.setTimeout( function() { - fxNow = undefined; - } ); - return ( fxNow = Date.now() ); -} - -// Generate parameters to create a standard animation -function genFx( type, includeWidth ) { - var which, - i = 0, - attrs = { height: type }; - - // If we include width, step value is 1 to do all cssExpand values, - // otherwise step value is 2 to skip over Left and Right - includeWidth = includeWidth ? 1 : 0; - for ( ; i < 4; i += 2 - includeWidth ) { - which = cssExpand[ i ]; - attrs[ "margin" + which ] = attrs[ "padding" + which ] = type; - } - - if ( includeWidth ) { - attrs.opacity = attrs.width = type; - } - - return attrs; -} - -function createTween( value, prop, animation ) { - var tween, - collection = ( Animation.tweeners[ prop ] || [] ).concat( Animation.tweeners[ "*" ] ), - index = 0, - length = collection.length; - for ( ; index < length; index++ ) { - if ( ( tween = collection[ index ].call( animation, prop, value ) ) ) { - - // We're done with this property - return tween; - } - } -} - -function defaultPrefilter( elem, props, opts ) { - var prop, value, toggle, hooks, oldfire, propTween, restoreDisplay, display, - isBox = "width" in props || "height" in props, - anim = this, - orig = {}, - style = elem.style, - hidden = elem.nodeType && isHiddenWithinTree( elem ), - dataShow = dataPriv.get( elem, "fxshow" ); - - // Queue-skipping animations hijack the fx hooks - if ( !opts.queue ) { - hooks = jQuery._queueHooks( elem, "fx" ); - if ( hooks.unqueued == null ) { - hooks.unqueued = 0; - oldfire = hooks.empty.fire; - hooks.empty.fire = function() { - if ( !hooks.unqueued ) { - oldfire(); - } - }; - } - hooks.unqueued++; - - anim.always( function() { - - // Ensure the complete handler is called before this completes - anim.always( function() { - hooks.unqueued--; - if ( !jQuery.queue( elem, "fx" ).length ) { - hooks.empty.fire(); - } - } ); - } ); - } - - // Detect show/hide animations - for ( prop in props ) { - value = props[ prop ]; - if ( rfxtypes.test( value ) ) { - delete props[ prop ]; - toggle = toggle || value === "toggle"; - if ( value === ( hidden ? "hide" : "show" ) ) { - - // Pretend to be hidden if this is a "show" and - // there is still data from a stopped show/hide - if ( value === "show" && dataShow && dataShow[ prop ] !== undefined ) { - hidden = true; - - // Ignore all other no-op show/hide data - } else { - continue; - } - } - orig[ prop ] = dataShow && dataShow[ prop ] || jQuery.style( elem, prop ); - } - } - - // Bail out if this is a no-op like .hide().hide() - propTween = !jQuery.isEmptyObject( props ); - if ( !propTween && jQuery.isEmptyObject( orig ) ) { - return; - } - - // Restrict "overflow" and "display" styles during box animations - if ( isBox && elem.nodeType === 1 ) { - - // Support: IE <=9 - 11, Edge 12 - 15 - // Record all 3 overflow attributes because IE does not infer the shorthand - // from identically-valued overflowX and overflowY and Edge just mirrors - // the overflowX value there. - opts.overflow = [ style.overflow, style.overflowX, style.overflowY ]; - - // Identify a display type, preferring old show/hide data over the CSS cascade - restoreDisplay = dataShow && dataShow.display; - if ( restoreDisplay == null ) { - restoreDisplay = dataPriv.get( elem, "display" ); - } - display = jQuery.css( elem, "display" ); - if ( display === "none" ) { - if ( restoreDisplay ) { - display = restoreDisplay; - } else { - - // Get nonempty value(s) by temporarily forcing visibility - showHide( [ elem ], true ); - restoreDisplay = elem.style.display || restoreDisplay; - display = jQuery.css( elem, "display" ); - showHide( [ elem ] ); - } - } - - // Animate inline elements as inline-block - if ( display === "inline" || display === "inline-block" && restoreDisplay != null ) { - if ( jQuery.css( elem, "float" ) === "none" ) { - - // Restore the original display value at the end of pure show/hide animations - if ( !propTween ) { - anim.done( function() { - style.display = restoreDisplay; - } ); - if ( restoreDisplay == null ) { - display = style.display; - restoreDisplay = display === "none" ? "" : display; - } - } - style.display = "inline-block"; - } - } - } - - if ( opts.overflow ) { - style.overflow = "hidden"; - anim.always( function() { - style.overflow = opts.overflow[ 0 ]; - style.overflowX = opts.overflow[ 1 ]; - style.overflowY = opts.overflow[ 2 ]; - } ); - } - - // Implement show/hide animations - propTween = false; - for ( prop in orig ) { - - // General show/hide setup for this element animation - if ( !propTween ) { - if ( dataShow ) { - if ( "hidden" in dataShow ) { - hidden = dataShow.hidden; - } - } else { - dataShow = dataPriv.access( elem, "fxshow", { display: restoreDisplay } ); - } - - // Store hidden/visible for toggle so `.stop().toggle()` "reverses" - if ( toggle ) { - dataShow.hidden = !hidden; - } - - // Show elements before animating them - if ( hidden ) { - showHide( [ elem ], true ); - } - - /* eslint-disable no-loop-func */ - - anim.done( function() { - - /* eslint-enable no-loop-func */ - - // The final step of a "hide" animation is actually hiding the element - if ( !hidden ) { - showHide( [ elem ] ); - } - dataPriv.remove( elem, "fxshow" ); - for ( prop in orig ) { - jQuery.style( elem, prop, orig[ prop ] ); - } - } ); - } - - // Per-property setup - propTween = createTween( hidden ? dataShow[ prop ] : 0, prop, anim ); - if ( !( prop in dataShow ) ) { - dataShow[ prop ] = propTween.start; - if ( hidden ) { - propTween.end = propTween.start; - propTween.start = 0; - } - } - } -} - -function propFilter( props, specialEasing ) { - var index, name, easing, value, hooks; - - // camelCase, specialEasing and expand cssHook pass - for ( index in props ) { - name = camelCase( index ); - easing = specialEasing[ name ]; - value = props[ index ]; - if ( Array.isArray( value ) ) { - easing = value[ 1 ]; - value = props[ index ] = value[ 0 ]; - } - - if ( index !== name ) { - props[ name ] = value; - delete props[ index ]; - } - - hooks = jQuery.cssHooks[ name ]; - if ( hooks && "expand" in hooks ) { - value = hooks.expand( value ); - delete props[ name ]; - - // Not quite $.extend, this won't overwrite existing keys. - // Reusing 'index' because we have the correct "name" - for ( index in value ) { - if ( !( index in props ) ) { - props[ index ] = value[ index ]; - specialEasing[ index ] = easing; - } - } - } else { - specialEasing[ name ] = easing; - } - } -} - -function Animation( elem, properties, options ) { - var result, - stopped, - index = 0, - length = Animation.prefilters.length, - deferred = jQuery.Deferred().always( function() { - - // Don't match elem in the :animated selector - delete tick.elem; - } ), - tick = function() { - if ( stopped ) { - return false; - } - var currentTime = fxNow || createFxNow(), - remaining = Math.max( 0, animation.startTime + animation.duration - currentTime ), - - // Support: Android 2.3 only - // Archaic crash bug won't allow us to use `1 - ( 0.5 || 0 )` (trac-12497) - temp = remaining / animation.duration || 0, - percent = 1 - temp, - index = 0, - length = animation.tweens.length; - - for ( ; index < length; index++ ) { - animation.tweens[ index ].run( percent ); - } - - deferred.notifyWith( elem, [ animation, percent, remaining ] ); - - // If there's more to do, yield - if ( percent < 1 && length ) { - return remaining; - } - - // If this was an empty animation, synthesize a final progress notification - if ( !length ) { - deferred.notifyWith( elem, [ animation, 1, 0 ] ); - } - - // Resolve the animation and report its conclusion - deferred.resolveWith( elem, [ animation ] ); - return false; - }, - animation = deferred.promise( { - elem: elem, - props: jQuery.extend( {}, properties ), - opts: jQuery.extend( true, { - specialEasing: {}, - easing: jQuery.easing._default - }, options ), - originalProperties: properties, - originalOptions: options, - startTime: fxNow || createFxNow(), - duration: options.duration, - tweens: [], - createTween: function( prop, end ) { - var tween = jQuery.Tween( elem, animation.opts, prop, end, - animation.opts.specialEasing[ prop ] || animation.opts.easing ); - animation.tweens.push( tween ); - return tween; - }, - stop: function( gotoEnd ) { - var index = 0, - - // If we are going to the end, we want to run all the tweens - // otherwise we skip this part - length = gotoEnd ? animation.tweens.length : 0; - if ( stopped ) { - return this; - } - stopped = true; - for ( ; index < length; index++ ) { - animation.tweens[ index ].run( 1 ); - } - - // Resolve when we played the last frame; otherwise, reject - if ( gotoEnd ) { - deferred.notifyWith( elem, [ animation, 1, 0 ] ); - deferred.resolveWith( elem, [ animation, gotoEnd ] ); - } else { - deferred.rejectWith( elem, [ animation, gotoEnd ] ); - } - return this; - } - } ), - props = animation.props; - - propFilter( props, animation.opts.specialEasing ); - - for ( ; index < length; index++ ) { - result = Animation.prefilters[ index ].call( animation, elem, props, animation.opts ); - if ( result ) { - if ( isFunction( result.stop ) ) { - jQuery._queueHooks( animation.elem, animation.opts.queue ).stop = - result.stop.bind( result ); - } - return result; - } - } - - jQuery.map( props, createTween, animation ); - - if ( isFunction( animation.opts.start ) ) { - animation.opts.start.call( elem, animation ); - } - - // Attach callbacks from options - animation - .progress( animation.opts.progress ) - .done( animation.opts.done, animation.opts.complete ) - .fail( animation.opts.fail ) - .always( animation.opts.always ); - - jQuery.fx.timer( - jQuery.extend( tick, { - elem: elem, - anim: animation, - queue: animation.opts.queue - } ) - ); - - return animation; -} - -jQuery.Animation = jQuery.extend( Animation, { - - tweeners: { - "*": [ function( prop, value ) { - var tween = this.createTween( prop, value ); - adjustCSS( tween.elem, prop, rcssNum.exec( value ), tween ); - return tween; - } ] - }, - - tweener: function( props, callback ) { - if ( isFunction( props ) ) { - callback = props; - props = [ "*" ]; - } else { - props = props.match( rnothtmlwhite ); - } - - var prop, - index = 0, - length = props.length; - - for ( ; index < length; index++ ) { - prop = props[ index ]; - Animation.tweeners[ prop ] = Animation.tweeners[ prop ] || []; - Animation.tweeners[ prop ].unshift( callback ); - } - }, - - prefilters: [ defaultPrefilter ], - - prefilter: function( callback, prepend ) { - if ( prepend ) { - Animation.prefilters.unshift( callback ); - } else { - Animation.prefilters.push( callback ); - } - } -} ); - -jQuery.speed = function( speed, easing, fn ) { - var opt = speed && typeof speed === "object" ? jQuery.extend( {}, speed ) : { - complete: fn || !fn && easing || - isFunction( speed ) && speed, - duration: speed, - easing: fn && easing || easing && !isFunction( easing ) && easing - }; - - // Go to the end state if fx are off - if ( jQuery.fx.off ) { - opt.duration = 0; - - } else { - if ( typeof opt.duration !== "number" ) { - if ( opt.duration in jQuery.fx.speeds ) { - opt.duration = jQuery.fx.speeds[ opt.duration ]; - - } else { - opt.duration = jQuery.fx.speeds._default; - } - } - } - - // Normalize opt.queue - true/undefined/null -> "fx" - if ( opt.queue == null || opt.queue === true ) { - opt.queue = "fx"; - } - - // Queueing - opt.old = opt.complete; - - opt.complete = function() { - if ( isFunction( opt.old ) ) { - opt.old.call( this ); - } - - if ( opt.queue ) { - jQuery.dequeue( this, opt.queue ); - } - }; - - return opt; -}; - -jQuery.fn.extend( { - fadeTo: function( speed, to, easing, callback ) { - - // Show any hidden elements after setting opacity to 0 - return this.filter( isHiddenWithinTree ).css( "opacity", 0 ).show() - - // Animate to the value specified - .end().animate( { opacity: to }, speed, easing, callback ); - }, - animate: function( prop, speed, easing, callback ) { - var empty = jQuery.isEmptyObject( prop ), - optall = jQuery.speed( speed, easing, callback ), - doAnimation = function() { - - // Operate on a copy of prop so per-property easing won't be lost - var anim = Animation( this, jQuery.extend( {}, prop ), optall ); - - // Empty animations, or finishing resolves immediately - if ( empty || dataPriv.get( this, "finish" ) ) { - anim.stop( true ); - } - }; - - doAnimation.finish = doAnimation; - - return empty || optall.queue === false ? - this.each( doAnimation ) : - this.queue( optall.queue, doAnimation ); - }, - stop: function( type, clearQueue, gotoEnd ) { - var stopQueue = function( hooks ) { - var stop = hooks.stop; - delete hooks.stop; - stop( gotoEnd ); - }; - - if ( typeof type !== "string" ) { - gotoEnd = clearQueue; - clearQueue = type; - type = undefined; - } - if ( clearQueue ) { - this.queue( type || "fx", [] ); - } - - return this.each( function() { - var dequeue = true, - index = type != null && type + "queueHooks", - timers = jQuery.timers, - data = dataPriv.get( this ); - - if ( index ) { - if ( data[ index ] && data[ index ].stop ) { - stopQueue( data[ index ] ); - } - } else { - for ( index in data ) { - if ( data[ index ] && data[ index ].stop && rrun.test( index ) ) { - stopQueue( data[ index ] ); - } - } - } - - for ( index = timers.length; index--; ) { - if ( timers[ index ].elem === this && - ( type == null || timers[ index ].queue === type ) ) { - - timers[ index ].anim.stop( gotoEnd ); - dequeue = false; - timers.splice( index, 1 ); - } - } - - // Start the next in the queue if the last step wasn't forced. - // Timers currently will call their complete callbacks, which - // will dequeue but only if they were gotoEnd. - if ( dequeue || !gotoEnd ) { - jQuery.dequeue( this, type ); - } - } ); - }, - finish: function( type ) { - if ( type !== false ) { - type = type || "fx"; - } - return this.each( function() { - var index, - data = dataPriv.get( this ), - queue = data[ type + "queue" ], - hooks = data[ type + "queueHooks" ], - timers = jQuery.timers, - length = queue ? queue.length : 0; - - // Enable finishing flag on private data - data.finish = true; - - // Empty the queue first - jQuery.queue( this, type, [] ); - - if ( hooks && hooks.stop ) { - hooks.stop.call( this, true ); - } - - // Look for any active animations, and finish them - for ( index = timers.length; index--; ) { - if ( timers[ index ].elem === this && timers[ index ].queue === type ) { - timers[ index ].anim.stop( true ); - timers.splice( index, 1 ); - } - } - - // Look for any animations in the old queue and finish them - for ( index = 0; index < length; index++ ) { - if ( queue[ index ] && queue[ index ].finish ) { - queue[ index ].finish.call( this ); - } - } - - // Turn off finishing flag - delete data.finish; - } ); - } -} ); - -jQuery.each( [ "toggle", "show", "hide" ], function( _i, name ) { - var cssFn = jQuery.fn[ name ]; - jQuery.fn[ name ] = function( speed, easing, callback ) { - return speed == null || typeof speed === "boolean" ? - cssFn.apply( this, arguments ) : - this.animate( genFx( name, true ), speed, easing, callback ); - }; -} ); - -// Generate shortcuts for custom animations -jQuery.each( { - slideDown: genFx( "show" ), - slideUp: genFx( "hide" ), - slideToggle: genFx( "toggle" ), - fadeIn: { opacity: "show" }, - fadeOut: { opacity: "hide" }, - fadeToggle: { opacity: "toggle" } -}, function( name, props ) { - jQuery.fn[ name ] = function( speed, easing, callback ) { - return this.animate( props, speed, easing, callback ); - }; -} ); - -jQuery.timers = []; -jQuery.fx.tick = function() { - var timer, - i = 0, - timers = jQuery.timers; - - fxNow = Date.now(); - - for ( ; i < timers.length; i++ ) { - timer = timers[ i ]; - - // Run the timer and safely remove it when done (allowing for external removal) - if ( !timer() && timers[ i ] === timer ) { - timers.splice( i--, 1 ); - } - } - - if ( !timers.length ) { - jQuery.fx.stop(); - } - fxNow = undefined; -}; - -jQuery.fx.timer = function( timer ) { - jQuery.timers.push( timer ); - jQuery.fx.start(); -}; - -jQuery.fx.interval = 13; -jQuery.fx.start = function() { - if ( inProgress ) { - return; - } - - inProgress = true; - schedule(); -}; - -jQuery.fx.stop = function() { - inProgress = null; -}; - -jQuery.fx.speeds = { - slow: 600, - fast: 200, - - // Default speed - _default: 400 -}; - - -// Based off of the plugin by Clint Helfers, with permission. -jQuery.fn.delay = function( time, type ) { - time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time; - type = type || "fx"; - - return this.queue( type, function( next, hooks ) { - var timeout = window.setTimeout( next, time ); - hooks.stop = function() { - window.clearTimeout( timeout ); - }; - } ); -}; - - -( function() { - var input = document.createElement( "input" ), - select = document.createElement( "select" ), - opt = select.appendChild( document.createElement( "option" ) ); - - input.type = "checkbox"; - - // Support: Android <=4.3 only - // Default value for a checkbox should be "on" - support.checkOn = input.value !== ""; - - // Support: IE <=11 only - // Must access selectedIndex to make default options select - support.optSelected = opt.selected; - - // Support: IE <=11 only - // An input loses its value after becoming a radio - input = document.createElement( "input" ); - input.value = "t"; - input.type = "radio"; - support.radioValue = input.value === "t"; -} )(); - - -var boolHook, - attrHandle = jQuery.expr.attrHandle; - -jQuery.fn.extend( { - attr: function( name, value ) { - return access( this, jQuery.attr, name, value, arguments.length > 1 ); - }, - - removeAttr: function( name ) { - return this.each( function() { - jQuery.removeAttr( this, name ); - } ); - } -} ); - -jQuery.extend( { - attr: function( elem, name, value ) { - var ret, hooks, - nType = elem.nodeType; - - // Don't get/set attributes on text, comment and attribute nodes - if ( nType === 3 || nType === 8 || nType === 2 ) { - return; - } - - // Fallback to prop when attributes are not supported - if ( typeof elem.getAttribute === "undefined" ) { - return jQuery.prop( elem, name, value ); - } - - // Attribute hooks are determined by the lowercase version - // Grab necessary hook if one is defined - if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) { - hooks = jQuery.attrHooks[ name.toLowerCase() ] || - ( jQuery.expr.match.bool.test( name ) ? boolHook : undefined ); - } - - if ( value !== undefined ) { - if ( value === null ) { - jQuery.removeAttr( elem, name ); - return; - } - - if ( hooks && "set" in hooks && - ( ret = hooks.set( elem, value, name ) ) !== undefined ) { - return ret; - } - - elem.setAttribute( name, value + "" ); - return value; - } - - if ( hooks && "get" in hooks && ( ret = hooks.get( elem, name ) ) !== null ) { - return ret; - } - - ret = jQuery.find.attr( elem, name ); - - // Non-existent attributes return null, we normalize to undefined - return ret == null ? undefined : ret; - }, - - attrHooks: { - type: { - set: function( elem, value ) { - if ( !support.radioValue && value === "radio" && - nodeName( elem, "input" ) ) { - var val = elem.value; - elem.setAttribute( "type", value ); - if ( val ) { - elem.value = val; - } - return value; - } - } - } - }, - - removeAttr: function( elem, value ) { - var name, - i = 0, - - // Attribute names can contain non-HTML whitespace characters - // https://html.spec.whatwg.org/multipage/syntax.html#attributes-2 - attrNames = value && value.match( rnothtmlwhite ); - - if ( attrNames && elem.nodeType === 1 ) { - while ( ( name = attrNames[ i++ ] ) ) { - elem.removeAttribute( name ); - } - } - } -} ); - -// Hooks for boolean attributes -boolHook = { - set: function( elem, value, name ) { - if ( value === false ) { - - // Remove boolean attributes when set to false - jQuery.removeAttr( elem, name ); - } else { - elem.setAttribute( name, name ); - } - return name; - } -}; - -jQuery.each( jQuery.expr.match.bool.source.match( /\w+/g ), function( _i, name ) { - var getter = attrHandle[ name ] || jQuery.find.attr; - - attrHandle[ name ] = function( elem, name, isXML ) { - var ret, handle, - lowercaseName = name.toLowerCase(); - - if ( !isXML ) { - - // Avoid an infinite loop by temporarily removing this function from the getter - handle = attrHandle[ lowercaseName ]; - attrHandle[ lowercaseName ] = ret; - ret = getter( elem, name, isXML ) != null ? - lowercaseName : - null; - attrHandle[ lowercaseName ] = handle; - } - return ret; - }; -} ); - - - - -var rfocusable = /^(?:input|select|textarea|button)$/i, - rclickable = /^(?:a|area)$/i; - -jQuery.fn.extend( { - prop: function( name, value ) { - return access( this, jQuery.prop, name, value, arguments.length > 1 ); - }, - - removeProp: function( name ) { - return this.each( function() { - delete this[ jQuery.propFix[ name ] || name ]; - } ); - } -} ); - -jQuery.extend( { - prop: function( elem, name, value ) { - var ret, hooks, - nType = elem.nodeType; - - // Don't get/set properties on text, comment and attribute nodes - if ( nType === 3 || nType === 8 || nType === 2 ) { - return; - } - - if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) { - - // Fix name and attach hooks - name = jQuery.propFix[ name ] || name; - hooks = jQuery.propHooks[ name ]; - } - - if ( value !== undefined ) { - if ( hooks && "set" in hooks && - ( ret = hooks.set( elem, value, name ) ) !== undefined ) { - return ret; - } - - return ( elem[ name ] = value ); - } - - if ( hooks && "get" in hooks && ( ret = hooks.get( elem, name ) ) !== null ) { - return ret; - } - - return elem[ name ]; - }, - - propHooks: { - tabIndex: { - get: function( elem ) { - - // Support: IE <=9 - 11 only - // elem.tabIndex doesn't always return the - // correct value when it hasn't been explicitly set - // Use proper attribute retrieval (trac-12072) - var tabindex = jQuery.find.attr( elem, "tabindex" ); - - if ( tabindex ) { - return parseInt( tabindex, 10 ); - } - - if ( - rfocusable.test( elem.nodeName ) || - rclickable.test( elem.nodeName ) && - elem.href - ) { - return 0; - } - - return -1; - } - } - }, - - propFix: { - "for": "htmlFor", - "class": "className" - } -} ); - -// Support: IE <=11 only -// Accessing the selectedIndex property -// forces the browser to respect setting selected -// on the option -// The getter ensures a default option is selected -// when in an optgroup -// eslint rule "no-unused-expressions" is disabled for this code -// since it considers such accessions noop -if ( !support.optSelected ) { - jQuery.propHooks.selected = { - get: function( elem ) { - - /* eslint no-unused-expressions: "off" */ - - var parent = elem.parentNode; - if ( parent && parent.parentNode ) { - parent.parentNode.selectedIndex; - } - return null; - }, - set: function( elem ) { - - /* eslint no-unused-expressions: "off" */ - - var parent = elem.parentNode; - if ( parent ) { - parent.selectedIndex; - - if ( parent.parentNode ) { - parent.parentNode.selectedIndex; - } - } - } - }; -} - -jQuery.each( [ - "tabIndex", - "readOnly", - "maxLength", - "cellSpacing", - "cellPadding", - "rowSpan", - "colSpan", - "useMap", - "frameBorder", - "contentEditable" -], function() { - jQuery.propFix[ this.toLowerCase() ] = this; -} ); - - - - - // Strip and collapse whitespace according to HTML spec - // https://infra.spec.whatwg.org/#strip-and-collapse-ascii-whitespace - function stripAndCollapse( value ) { - var tokens = value.match( rnothtmlwhite ) || []; - return tokens.join( " " ); - } - - -function getClass( elem ) { - return elem.getAttribute && elem.getAttribute( "class" ) || ""; -} - -function classesToArray( value ) { - if ( Array.isArray( value ) ) { - return value; - } - if ( typeof value === "string" ) { - return value.match( rnothtmlwhite ) || []; - } - return []; -} - -jQuery.fn.extend( { - addClass: function( value ) { - var classNames, cur, curValue, className, i, finalValue; - - if ( isFunction( value ) ) { - return this.each( function( j ) { - jQuery( this ).addClass( value.call( this, j, getClass( this ) ) ); - } ); - } - - classNames = classesToArray( value ); - - if ( classNames.length ) { - return this.each( function() { - curValue = getClass( this ); - cur = this.nodeType === 1 && ( " " + stripAndCollapse( curValue ) + " " ); - - if ( cur ) { - for ( i = 0; i < classNames.length; i++ ) { - className = classNames[ i ]; - if ( cur.indexOf( " " + className + " " ) < 0 ) { - cur += className + " "; - } - } - - // Only assign if different to avoid unneeded rendering. - finalValue = stripAndCollapse( cur ); - if ( curValue !== finalValue ) { - this.setAttribute( "class", finalValue ); - } - } - } ); - } - - return this; - }, - - removeClass: function( value ) { - var classNames, cur, curValue, className, i, finalValue; - - if ( isFunction( value ) ) { - return this.each( function( j ) { - jQuery( this ).removeClass( value.call( this, j, getClass( this ) ) ); - } ); - } - - if ( !arguments.length ) { - return this.attr( "class", "" ); - } - - classNames = classesToArray( value ); - - if ( classNames.length ) { - return this.each( function() { - curValue = getClass( this ); - - // This expression is here for better compressibility (see addClass) - cur = this.nodeType === 1 && ( " " + stripAndCollapse( curValue ) + " " ); - - if ( cur ) { - for ( i = 0; i < classNames.length; i++ ) { - className = classNames[ i ]; - - // Remove *all* instances - while ( cur.indexOf( " " + className + " " ) > -1 ) { - cur = cur.replace( " " + className + " ", " " ); - } - } - - // Only assign if different to avoid unneeded rendering. - finalValue = stripAndCollapse( cur ); - if ( curValue !== finalValue ) { - this.setAttribute( "class", finalValue ); - } - } - } ); - } - - return this; - }, - - toggleClass: function( value, stateVal ) { - var classNames, className, i, self, - type = typeof value, - isValidValue = type === "string" || Array.isArray( value ); - - if ( isFunction( value ) ) { - return this.each( function( i ) { - jQuery( this ).toggleClass( - value.call( this, i, getClass( this ), stateVal ), - stateVal - ); - } ); - } - - if ( typeof stateVal === "boolean" && isValidValue ) { - return stateVal ? this.addClass( value ) : this.removeClass( value ); - } - - classNames = classesToArray( value ); - - return this.each( function() { - if ( isValidValue ) { - - // Toggle individual class names - self = jQuery( this ); - - for ( i = 0; i < classNames.length; i++ ) { - className = classNames[ i ]; - - // Check each className given, space separated list - if ( self.hasClass( className ) ) { - self.removeClass( className ); - } else { - self.addClass( className ); - } - } - - // Toggle whole class name - } else if ( value === undefined || type === "boolean" ) { - className = getClass( this ); - if ( className ) { - - // Store className if set - dataPriv.set( this, "__className__", className ); - } - - // If the element has a class name or if we're passed `false`, - // then remove the whole classname (if there was one, the above saved it). - // Otherwise bring back whatever was previously saved (if anything), - // falling back to the empty string if nothing was stored. - if ( this.setAttribute ) { - this.setAttribute( "class", - className || value === false ? - "" : - dataPriv.get( this, "__className__" ) || "" - ); - } - } - } ); - }, - - hasClass: function( selector ) { - var className, elem, - i = 0; - - className = " " + selector + " "; - while ( ( elem = this[ i++ ] ) ) { - if ( elem.nodeType === 1 && - ( " " + stripAndCollapse( getClass( elem ) ) + " " ).indexOf( className ) > -1 ) { - return true; - } - } - - return false; - } -} ); - - - - -var rreturn = /\r/g; - -jQuery.fn.extend( { - val: function( value ) { - var hooks, ret, valueIsFunction, - elem = this[ 0 ]; - - if ( !arguments.length ) { - if ( elem ) { - hooks = jQuery.valHooks[ elem.type ] || - jQuery.valHooks[ elem.nodeName.toLowerCase() ]; - - if ( hooks && - "get" in hooks && - ( ret = hooks.get( elem, "value" ) ) !== undefined - ) { - return ret; - } - - ret = elem.value; - - // Handle most common string cases - if ( typeof ret === "string" ) { - return ret.replace( rreturn, "" ); - } - - // Handle cases where value is null/undef or number - return ret == null ? "" : ret; - } - - return; - } - - valueIsFunction = isFunction( value ); - - return this.each( function( i ) { - var val; - - if ( this.nodeType !== 1 ) { - return; - } - - if ( valueIsFunction ) { - val = value.call( this, i, jQuery( this ).val() ); - } else { - val = value; - } - - // Treat null/undefined as ""; convert numbers to string - if ( val == null ) { - val = ""; - - } else if ( typeof val === "number" ) { - val += ""; - - } else if ( Array.isArray( val ) ) { - val = jQuery.map( val, function( value ) { - return value == null ? "" : value + ""; - } ); - } - - hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ]; - - // If set returns undefined, fall back to normal setting - if ( !hooks || !( "set" in hooks ) || hooks.set( this, val, "value" ) === undefined ) { - this.value = val; - } - } ); - } -} ); - -jQuery.extend( { - valHooks: { - option: { - get: function( elem ) { - - var val = jQuery.find.attr( elem, "value" ); - return val != null ? - val : - - // Support: IE <=10 - 11 only - // option.text throws exceptions (trac-14686, trac-14858) - // Strip and collapse whitespace - // https://html.spec.whatwg.org/#strip-and-collapse-whitespace - stripAndCollapse( jQuery.text( elem ) ); - } - }, - select: { - get: function( elem ) { - var value, option, i, - options = elem.options, - index = elem.selectedIndex, - one = elem.type === "select-one", - values = one ? null : [], - max = one ? index + 1 : options.length; - - if ( index < 0 ) { - i = max; - - } else { - i = one ? index : 0; - } - - // Loop through all the selected options - for ( ; i < max; i++ ) { - option = options[ i ]; - - // Support: IE <=9 only - // IE8-9 doesn't update selected after form reset (trac-2551) - if ( ( option.selected || i === index ) && - - // Don't return options that are disabled or in a disabled optgroup - !option.disabled && - ( !option.parentNode.disabled || - !nodeName( option.parentNode, "optgroup" ) ) ) { - - // Get the specific value for the option - value = jQuery( option ).val(); - - // We don't need an array for one selects - if ( one ) { - return value; - } - - // Multi-Selects return an array - values.push( value ); - } - } - - return values; - }, - - set: function( elem, value ) { - var optionSet, option, - options = elem.options, - values = jQuery.makeArray( value ), - i = options.length; - - while ( i-- ) { - option = options[ i ]; - - /* eslint-disable no-cond-assign */ - - if ( option.selected = - jQuery.inArray( jQuery.valHooks.option.get( option ), values ) > -1 - ) { - optionSet = true; - } - - /* eslint-enable no-cond-assign */ - } - - // Force browsers to behave consistently when non-matching value is set - if ( !optionSet ) { - elem.selectedIndex = -1; - } - return values; - } - } - } -} ); - -// Radios and checkboxes getter/setter -jQuery.each( [ "radio", "checkbox" ], function() { - jQuery.valHooks[ this ] = { - set: function( elem, value ) { - if ( Array.isArray( value ) ) { - return ( elem.checked = jQuery.inArray( jQuery( elem ).val(), value ) > -1 ); - } - } - }; - if ( !support.checkOn ) { - jQuery.valHooks[ this ].get = function( elem ) { - return elem.getAttribute( "value" ) === null ? "on" : elem.value; - }; - } -} ); - - - - -// Return jQuery for attributes-only inclusion -var location = window.location; - -var nonce = { guid: Date.now() }; - -var rquery = ( /\?/ ); - - - -// Cross-browser xml parsing -jQuery.parseXML = function( data ) { - var xml, parserErrorElem; - if ( !data || typeof data !== "string" ) { - return null; - } - - // Support: IE 9 - 11 only - // IE throws on parseFromString with invalid input. - try { - xml = ( new window.DOMParser() ).parseFromString( data, "text/xml" ); - } catch ( e ) {} - - parserErrorElem = xml && xml.getElementsByTagName( "parsererror" )[ 0 ]; - if ( !xml || parserErrorElem ) { - jQuery.error( "Invalid XML: " + ( - parserErrorElem ? - jQuery.map( parserErrorElem.childNodes, function( el ) { - return el.textContent; - } ).join( "\n" ) : - data - ) ); - } - return xml; -}; - - -var rfocusMorph = /^(?:focusinfocus|focusoutblur)$/, - stopPropagationCallback = function( e ) { - e.stopPropagation(); - }; - -jQuery.extend( jQuery.event, { - - trigger: function( event, data, elem, onlyHandlers ) { - - var i, cur, tmp, bubbleType, ontype, handle, special, lastElement, - eventPath = [ elem || document ], - type = hasOwn.call( event, "type" ) ? event.type : event, - namespaces = hasOwn.call( event, "namespace" ) ? event.namespace.split( "." ) : []; - - cur = lastElement = tmp = elem = elem || document; - - // Don't do events on text and comment nodes - if ( elem.nodeType === 3 || elem.nodeType === 8 ) { - return; - } - - // focus/blur morphs to focusin/out; ensure we're not firing them right now - if ( rfocusMorph.test( type + jQuery.event.triggered ) ) { - return; - } - - if ( type.indexOf( "." ) > -1 ) { - - // Namespaced trigger; create a regexp to match event type in handle() - namespaces = type.split( "." ); - type = namespaces.shift(); - namespaces.sort(); - } - ontype = type.indexOf( ":" ) < 0 && "on" + type; - - // Caller can pass in a jQuery.Event object, Object, or just an event type string - event = event[ jQuery.expando ] ? - event : - new jQuery.Event( type, typeof event === "object" && event ); - - // Trigger bitmask: & 1 for native handlers; & 2 for jQuery (always true) - event.isTrigger = onlyHandlers ? 2 : 3; - event.namespace = namespaces.join( "." ); - event.rnamespace = event.namespace ? - new RegExp( "(^|\\.)" + namespaces.join( "\\.(?:.*\\.|)" ) + "(\\.|$)" ) : - null; - - // Clean up the event in case it is being reused - event.result = undefined; - if ( !event.target ) { - event.target = elem; - } - - // Clone any incoming data and prepend the event, creating the handler arg list - data = data == null ? - [ event ] : - jQuery.makeArray( data, [ event ] ); - - // Allow special events to draw outside the lines - special = jQuery.event.special[ type ] || {}; - if ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) { - return; - } - - // Determine event propagation path in advance, per W3C events spec (trac-9951) - // Bubble up to document, then to window; watch for a global ownerDocument var (trac-9724) - if ( !onlyHandlers && !special.noBubble && !isWindow( elem ) ) { - - bubbleType = special.delegateType || type; - if ( !rfocusMorph.test( bubbleType + type ) ) { - cur = cur.parentNode; - } - for ( ; cur; cur = cur.parentNode ) { - eventPath.push( cur ); - tmp = cur; - } - - // Only add window if we got to document (e.g., not plain obj or detached DOM) - if ( tmp === ( elem.ownerDocument || document ) ) { - eventPath.push( tmp.defaultView || tmp.parentWindow || window ); - } - } - - // Fire handlers on the event path - i = 0; - while ( ( cur = eventPath[ i++ ] ) && !event.isPropagationStopped() ) { - lastElement = cur; - event.type = i > 1 ? - bubbleType : - special.bindType || type; - - // jQuery handler - handle = ( dataPriv.get( cur, "events" ) || Object.create( null ) )[ event.type ] && - dataPriv.get( cur, "handle" ); - if ( handle ) { - handle.apply( cur, data ); - } - - // Native handler - handle = ontype && cur[ ontype ]; - if ( handle && handle.apply && acceptData( cur ) ) { - event.result = handle.apply( cur, data ); - if ( event.result === false ) { - event.preventDefault(); - } - } - } - event.type = type; - - // If nobody prevented the default action, do it now - if ( !onlyHandlers && !event.isDefaultPrevented() ) { - - if ( ( !special._default || - special._default.apply( eventPath.pop(), data ) === false ) && - acceptData( elem ) ) { - - // Call a native DOM method on the target with the same name as the event. - // Don't do default actions on window, that's where global variables be (trac-6170) - if ( ontype && isFunction( elem[ type ] ) && !isWindow( elem ) ) { - - // Don't re-trigger an onFOO event when we call its FOO() method - tmp = elem[ ontype ]; - - if ( tmp ) { - elem[ ontype ] = null; - } - - // Prevent re-triggering of the same event, since we already bubbled it above - jQuery.event.triggered = type; - - if ( event.isPropagationStopped() ) { - lastElement.addEventListener( type, stopPropagationCallback ); - } - - elem[ type ](); - - if ( event.isPropagationStopped() ) { - lastElement.removeEventListener( type, stopPropagationCallback ); - } - - jQuery.event.triggered = undefined; - - if ( tmp ) { - elem[ ontype ] = tmp; - } - } - } - } - - return event.result; - }, - - // Piggyback on a donor event to simulate a different one - // Used only for `focus(in | out)` events - simulate: function( type, elem, event ) { - var e = jQuery.extend( - new jQuery.Event(), - event, - { - type: type, - isSimulated: true - } - ); - - jQuery.event.trigger( e, null, elem ); - } - -} ); - -jQuery.fn.extend( { - - trigger: function( type, data ) { - return this.each( function() { - jQuery.event.trigger( type, data, this ); - } ); - }, - triggerHandler: function( type, data ) { - var elem = this[ 0 ]; - if ( elem ) { - return jQuery.event.trigger( type, data, elem, true ); - } - } -} ); - - -var - rbracket = /\[\]$/, - rCRLF = /\r?\n/g, - rsubmitterTypes = /^(?:submit|button|image|reset|file)$/i, - rsubmittable = /^(?:input|select|textarea|keygen)/i; - -function buildParams( prefix, obj, traditional, add ) { - var name; - - if ( Array.isArray( obj ) ) { - - // Serialize array item. - jQuery.each( obj, function( i, v ) { - if ( traditional || rbracket.test( prefix ) ) { - - // Treat each array item as a scalar. - add( prefix, v ); - - } else { - - // Item is non-scalar (array or object), encode its numeric index. - buildParams( - prefix + "[" + ( typeof v === "object" && v != null ? i : "" ) + "]", - v, - traditional, - add - ); - } - } ); - - } else if ( !traditional && toType( obj ) === "object" ) { - - // Serialize object item. - for ( name in obj ) { - buildParams( prefix + "[" + name + "]", obj[ name ], traditional, add ); - } - - } else { - - // Serialize scalar item. - add( prefix, obj ); - } -} - -// Serialize an array of form elements or a set of -// key/values into a query string -jQuery.param = function( a, traditional ) { - var prefix, - s = [], - add = function( key, valueOrFunction ) { - - // If value is a function, invoke it and use its return value - var value = isFunction( valueOrFunction ) ? - valueOrFunction() : - valueOrFunction; - - s[ s.length ] = encodeURIComponent( key ) + "=" + - encodeURIComponent( value == null ? "" : value ); - }; - - if ( a == null ) { - return ""; - } - - // If an array was passed in, assume that it is an array of form elements. - if ( Array.isArray( a ) || ( a.jquery && !jQuery.isPlainObject( a ) ) ) { - - // Serialize the form elements - jQuery.each( a, function() { - add( this.name, this.value ); - } ); - - } else { - - // If traditional, encode the "old" way (the way 1.3.2 or older - // did it), otherwise encode params recursively. - for ( prefix in a ) { - buildParams( prefix, a[ prefix ], traditional, add ); - } - } - - // Return the resulting serialization - return s.join( "&" ); -}; - -jQuery.fn.extend( { - serialize: function() { - return jQuery.param( this.serializeArray() ); - }, - serializeArray: function() { - return this.map( function() { - - // Can add propHook for "elements" to filter or add form elements - var elements = jQuery.prop( this, "elements" ); - return elements ? jQuery.makeArray( elements ) : this; - } ).filter( function() { - var type = this.type; - - // Use .is( ":disabled" ) so that fieldset[disabled] works - return this.name && !jQuery( this ).is( ":disabled" ) && - rsubmittable.test( this.nodeName ) && !rsubmitterTypes.test( type ) && - ( this.checked || !rcheckableType.test( type ) ); - } ).map( function( _i, elem ) { - var val = jQuery( this ).val(); - - if ( val == null ) { - return null; - } - - if ( Array.isArray( val ) ) { - return jQuery.map( val, function( val ) { - return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) }; - } ); - } - - return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) }; - } ).get(); - } -} ); - - -var - r20 = /%20/g, - rhash = /#.*$/, - rantiCache = /([?&])_=[^&]*/, - rheaders = /^(.*?):[ \t]*([^\r\n]*)$/mg, - - // trac-7653, trac-8125, trac-8152: local protocol detection - rlocalProtocol = /^(?:about|app|app-storage|.+-extension|file|res|widget):$/, - rnoContent = /^(?:GET|HEAD)$/, - rprotocol = /^\/\//, - - /* Prefilters - * 1) They are useful to introduce custom dataTypes (see ajax/jsonp.js for an example) - * 2) These are called: - * - BEFORE asking for a transport - * - AFTER param serialization (s.data is a string if s.processData is true) - * 3) key is the dataType - * 4) the catchall symbol "*" can be used - * 5) execution will start with transport dataType and THEN continue down to "*" if needed - */ - prefilters = {}, - - /* Transports bindings - * 1) key is the dataType - * 2) the catchall symbol "*" can be used - * 3) selection will start with transport dataType and THEN go to "*" if needed - */ - transports = {}, - - // Avoid comment-prolog char sequence (trac-10098); must appease lint and evade compression - allTypes = "*/".concat( "*" ), - - // Anchor tag for parsing the document origin - originAnchor = document.createElement( "a" ); - -originAnchor.href = location.href; - -// Base "constructor" for jQuery.ajaxPrefilter and jQuery.ajaxTransport -function addToPrefiltersOrTransports( structure ) { - - // dataTypeExpression is optional and defaults to "*" - return function( dataTypeExpression, func ) { - - if ( typeof dataTypeExpression !== "string" ) { - func = dataTypeExpression; - dataTypeExpression = "*"; - } - - var dataType, - i = 0, - dataTypes = dataTypeExpression.toLowerCase().match( rnothtmlwhite ) || []; - - if ( isFunction( func ) ) { - - // For each dataType in the dataTypeExpression - while ( ( dataType = dataTypes[ i++ ] ) ) { - - // Prepend if requested - if ( dataType[ 0 ] === "+" ) { - dataType = dataType.slice( 1 ) || "*"; - ( structure[ dataType ] = structure[ dataType ] || [] ).unshift( func ); - - // Otherwise append - } else { - ( structure[ dataType ] = structure[ dataType ] || [] ).push( func ); - } - } - } - }; -} - -// Base inspection function for prefilters and transports -function inspectPrefiltersOrTransports( structure, options, originalOptions, jqXHR ) { - - var inspected = {}, - seekingTransport = ( structure === transports ); - - function inspect( dataType ) { - var selected; - inspected[ dataType ] = true; - jQuery.each( structure[ dataType ] || [], function( _, prefilterOrFactory ) { - var dataTypeOrTransport = prefilterOrFactory( options, originalOptions, jqXHR ); - if ( typeof dataTypeOrTransport === "string" && - !seekingTransport && !inspected[ dataTypeOrTransport ] ) { - - options.dataTypes.unshift( dataTypeOrTransport ); - inspect( dataTypeOrTransport ); - return false; - } else if ( seekingTransport ) { - return !( selected = dataTypeOrTransport ); - } - } ); - return selected; - } - - return inspect( options.dataTypes[ 0 ] ) || !inspected[ "*" ] && inspect( "*" ); -} - -// A special extend for ajax options -// that takes "flat" options (not to be deep extended) -// Fixes trac-9887 -function ajaxExtend( target, src ) { - var key, deep, - flatOptions = jQuery.ajaxSettings.flatOptions || {}; - - for ( key in src ) { - if ( src[ key ] !== undefined ) { - ( flatOptions[ key ] ? target : ( deep || ( deep = {} ) ) )[ key ] = src[ key ]; - } - } - if ( deep ) { - jQuery.extend( true, target, deep ); - } - - return target; -} - -/* Handles responses to an ajax request: - * - finds the right dataType (mediates between content-type and expected dataType) - * - returns the corresponding response - */ -function ajaxHandleResponses( s, jqXHR, responses ) { - - var ct, type, finalDataType, firstDataType, - contents = s.contents, - dataTypes = s.dataTypes; - - // Remove auto dataType and get content-type in the process - while ( dataTypes[ 0 ] === "*" ) { - dataTypes.shift(); - if ( ct === undefined ) { - ct = s.mimeType || jqXHR.getResponseHeader( "Content-Type" ); - } - } - - // Check if we're dealing with a known content-type - if ( ct ) { - for ( type in contents ) { - if ( contents[ type ] && contents[ type ].test( ct ) ) { - dataTypes.unshift( type ); - break; - } - } - } - - // Check to see if we have a response for the expected dataType - if ( dataTypes[ 0 ] in responses ) { - finalDataType = dataTypes[ 0 ]; - } else { - - // Try convertible dataTypes - for ( type in responses ) { - if ( !dataTypes[ 0 ] || s.converters[ type + " " + dataTypes[ 0 ] ] ) { - finalDataType = type; - break; - } - if ( !firstDataType ) { - firstDataType = type; - } - } - - // Or just use first one - finalDataType = finalDataType || firstDataType; - } - - // If we found a dataType - // We add the dataType to the list if needed - // and return the corresponding response - if ( finalDataType ) { - if ( finalDataType !== dataTypes[ 0 ] ) { - dataTypes.unshift( finalDataType ); - } - return responses[ finalDataType ]; - } -} - -/* Chain conversions given the request and the original response - * Also sets the responseXXX fields on the jqXHR instance - */ -function ajaxConvert( s, response, jqXHR, isSuccess ) { - var conv2, current, conv, tmp, prev, - converters = {}, - - // Work with a copy of dataTypes in case we need to modify it for conversion - dataTypes = s.dataTypes.slice(); - - // Create converters map with lowercased keys - if ( dataTypes[ 1 ] ) { - for ( conv in s.converters ) { - converters[ conv.toLowerCase() ] = s.converters[ conv ]; - } - } - - current = dataTypes.shift(); - - // Convert to each sequential dataType - while ( current ) { - - if ( s.responseFields[ current ] ) { - jqXHR[ s.responseFields[ current ] ] = response; - } - - // Apply the dataFilter if provided - if ( !prev && isSuccess && s.dataFilter ) { - response = s.dataFilter( response, s.dataType ); - } - - prev = current; - current = dataTypes.shift(); - - if ( current ) { - - // There's only work to do if current dataType is non-auto - if ( current === "*" ) { - - current = prev; - - // Convert response if prev dataType is non-auto and differs from current - } else if ( prev !== "*" && prev !== current ) { - - // Seek a direct converter - conv = converters[ prev + " " + current ] || converters[ "* " + current ]; - - // If none found, seek a pair - if ( !conv ) { - for ( conv2 in converters ) { - - // If conv2 outputs current - tmp = conv2.split( " " ); - if ( tmp[ 1 ] === current ) { - - // If prev can be converted to accepted input - conv = converters[ prev + " " + tmp[ 0 ] ] || - converters[ "* " + tmp[ 0 ] ]; - if ( conv ) { - - // Condense equivalence converters - if ( conv === true ) { - conv = converters[ conv2 ]; - - // Otherwise, insert the intermediate dataType - } else if ( converters[ conv2 ] !== true ) { - current = tmp[ 0 ]; - dataTypes.unshift( tmp[ 1 ] ); - } - break; - } - } - } - } - - // Apply converter (if not an equivalence) - if ( conv !== true ) { - - // Unless errors are allowed to bubble, catch and return them - if ( conv && s.throws ) { - response = conv( response ); - } else { - try { - response = conv( response ); - } catch ( e ) { - return { - state: "parsererror", - error: conv ? e : "No conversion from " + prev + " to " + current - }; - } - } - } - } - } - } - - return { state: "success", data: response }; -} - -jQuery.extend( { - - // Counter for holding the number of active queries - active: 0, - - // Last-Modified header cache for next request - lastModified: {}, - etag: {}, - - ajaxSettings: { - url: location.href, - type: "GET", - isLocal: rlocalProtocol.test( location.protocol ), - global: true, - processData: true, - async: true, - contentType: "application/x-www-form-urlencoded; charset=UTF-8", - - /* - timeout: 0, - data: null, - dataType: null, - username: null, - password: null, - cache: null, - throws: false, - traditional: false, - headers: {}, - */ - - accepts: { - "*": allTypes, - text: "text/plain", - html: "text/html", - xml: "application/xml, text/xml", - json: "application/json, text/javascript" - }, - - contents: { - xml: /\bxml\b/, - html: /\bhtml/, - json: /\bjson\b/ - }, - - responseFields: { - xml: "responseXML", - text: "responseText", - json: "responseJSON" - }, - - // Data converters - // Keys separate source (or catchall "*") and destination types with a single space - converters: { - - // Convert anything to text - "* text": String, - - // Text to html (true = no transformation) - "text html": true, - - // Evaluate text as a json expression - "text json": JSON.parse, - - // Parse text as xml - "text xml": jQuery.parseXML - }, - - // For options that shouldn't be deep extended: - // you can add your own custom options here if - // and when you create one that shouldn't be - // deep extended (see ajaxExtend) - flatOptions: { - url: true, - context: true - } - }, - - // Creates a full fledged settings object into target - // with both ajaxSettings and settings fields. - // If target is omitted, writes into ajaxSettings. - ajaxSetup: function( target, settings ) { - return settings ? - - // Building a settings object - ajaxExtend( ajaxExtend( target, jQuery.ajaxSettings ), settings ) : - - // Extending ajaxSettings - ajaxExtend( jQuery.ajaxSettings, target ); - }, - - ajaxPrefilter: addToPrefiltersOrTransports( prefilters ), - ajaxTransport: addToPrefiltersOrTransports( transports ), - - // Main method - ajax: function( url, options ) { - - // If url is an object, simulate pre-1.5 signature - if ( typeof url === "object" ) { - options = url; - url = undefined; - } - - // Force options to be an object - options = options || {}; - - var transport, - - // URL without anti-cache param - cacheURL, - - // Response headers - responseHeadersString, - responseHeaders, - - // timeout handle - timeoutTimer, - - // Url cleanup var - urlAnchor, - - // Request state (becomes false upon send and true upon completion) - completed, - - // To know if global events are to be dispatched - fireGlobals, - - // Loop variable - i, - - // uncached part of the url - uncached, - - // Create the final options object - s = jQuery.ajaxSetup( {}, options ), - - // Callbacks context - callbackContext = s.context || s, - - // Context for global events is callbackContext if it is a DOM node or jQuery collection - globalEventContext = s.context && - ( callbackContext.nodeType || callbackContext.jquery ) ? - jQuery( callbackContext ) : - jQuery.event, - - // Deferreds - deferred = jQuery.Deferred(), - completeDeferred = jQuery.Callbacks( "once memory" ), - - // Status-dependent callbacks - statusCode = s.statusCode || {}, - - // Headers (they are sent all at once) - requestHeaders = {}, - requestHeadersNames = {}, - - // Default abort message - strAbort = "canceled", - - // Fake xhr - jqXHR = { - readyState: 0, - - // Builds headers hashtable if needed - getResponseHeader: function( key ) { - var match; - if ( completed ) { - if ( !responseHeaders ) { - responseHeaders = {}; - while ( ( match = rheaders.exec( responseHeadersString ) ) ) { - responseHeaders[ match[ 1 ].toLowerCase() + " " ] = - ( responseHeaders[ match[ 1 ].toLowerCase() + " " ] || [] ) - .concat( match[ 2 ] ); - } - } - match = responseHeaders[ key.toLowerCase() + " " ]; - } - return match == null ? null : match.join( ", " ); - }, - - // Raw string - getAllResponseHeaders: function() { - return completed ? responseHeadersString : null; - }, - - // Caches the header - setRequestHeader: function( name, value ) { - if ( completed == null ) { - name = requestHeadersNames[ name.toLowerCase() ] = - requestHeadersNames[ name.toLowerCase() ] || name; - requestHeaders[ name ] = value; - } - return this; - }, - - // Overrides response content-type header - overrideMimeType: function( type ) { - if ( completed == null ) { - s.mimeType = type; - } - return this; - }, - - // Status-dependent callbacks - statusCode: function( map ) { - var code; - if ( map ) { - if ( completed ) { - - // Execute the appropriate callbacks - jqXHR.always( map[ jqXHR.status ] ); - } else { - - // Lazy-add the new callbacks in a way that preserves old ones - for ( code in map ) { - statusCode[ code ] = [ statusCode[ code ], map[ code ] ]; - } - } - } - return this; - }, - - // Cancel the request - abort: function( statusText ) { - var finalText = statusText || strAbort; - if ( transport ) { - transport.abort( finalText ); - } - done( 0, finalText ); - return this; - } - }; - - // Attach deferreds - deferred.promise( jqXHR ); - - // Add protocol if not provided (prefilters might expect it) - // Handle falsy url in the settings object (trac-10093: consistency with old signature) - // We also use the url parameter if available - s.url = ( ( url || s.url || location.href ) + "" ) - .replace( rprotocol, location.protocol + "//" ); - - // Alias method option to type as per ticket trac-12004 - s.type = options.method || options.type || s.method || s.type; - - // Extract dataTypes list - s.dataTypes = ( s.dataType || "*" ).toLowerCase().match( rnothtmlwhite ) || [ "" ]; - - // A cross-domain request is in order when the origin doesn't match the current origin. - if ( s.crossDomain == null ) { - urlAnchor = document.createElement( "a" ); - - // Support: IE <=8 - 11, Edge 12 - 15 - // IE throws exception on accessing the href property if url is malformed, - // e.g. http://example.com:80x/ - try { - urlAnchor.href = s.url; - - // Support: IE <=8 - 11 only - // Anchor's host property isn't correctly set when s.url is relative - urlAnchor.href = urlAnchor.href; - s.crossDomain = originAnchor.protocol + "//" + originAnchor.host !== - urlAnchor.protocol + "//" + urlAnchor.host; - } catch ( e ) { - - // If there is an error parsing the URL, assume it is crossDomain, - // it can be rejected by the transport if it is invalid - s.crossDomain = true; - } - } - - // Convert data if not already a string - if ( s.data && s.processData && typeof s.data !== "string" ) { - s.data = jQuery.param( s.data, s.traditional ); - } - - // Apply prefilters - inspectPrefiltersOrTransports( prefilters, s, options, jqXHR ); - - // If request was aborted inside a prefilter, stop there - if ( completed ) { - return jqXHR; - } - - // We can fire global events as of now if asked to - // Don't fire events if jQuery.event is undefined in an AMD-usage scenario (trac-15118) - fireGlobals = jQuery.event && s.global; - - // Watch for a new set of requests - if ( fireGlobals && jQuery.active++ === 0 ) { - jQuery.event.trigger( "ajaxStart" ); - } - - // Uppercase the type - s.type = s.type.toUpperCase(); - - // Determine if request has content - s.hasContent = !rnoContent.test( s.type ); - - // Save the URL in case we're toying with the If-Modified-Since - // and/or If-None-Match header later on - // Remove hash to simplify url manipulation - cacheURL = s.url.replace( rhash, "" ); - - // More options handling for requests with no content - if ( !s.hasContent ) { - - // Remember the hash so we can put it back - uncached = s.url.slice( cacheURL.length ); - - // If data is available and should be processed, append data to url - if ( s.data && ( s.processData || typeof s.data === "string" ) ) { - cacheURL += ( rquery.test( cacheURL ) ? "&" : "?" ) + s.data; - - // trac-9682: remove data so that it's not used in an eventual retry - delete s.data; - } - - // Add or update anti-cache param if needed - if ( s.cache === false ) { - cacheURL = cacheURL.replace( rantiCache, "$1" ); - uncached = ( rquery.test( cacheURL ) ? "&" : "?" ) + "_=" + ( nonce.guid++ ) + - uncached; - } - - // Put hash and anti-cache on the URL that will be requested (gh-1732) - s.url = cacheURL + uncached; - - // Change '%20' to '+' if this is encoded form body content (gh-2658) - } else if ( s.data && s.processData && - ( s.contentType || "" ).indexOf( "application/x-www-form-urlencoded" ) === 0 ) { - s.data = s.data.replace( r20, "+" ); - } - - // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode. - if ( s.ifModified ) { - if ( jQuery.lastModified[ cacheURL ] ) { - jqXHR.setRequestHeader( "If-Modified-Since", jQuery.lastModified[ cacheURL ] ); - } - if ( jQuery.etag[ cacheURL ] ) { - jqXHR.setRequestHeader( "If-None-Match", jQuery.etag[ cacheURL ] ); - } - } - - // Set the correct header, if data is being sent - if ( s.data && s.hasContent && s.contentType !== false || options.contentType ) { - jqXHR.setRequestHeader( "Content-Type", s.contentType ); - } - - // Set the Accepts header for the server, depending on the dataType - jqXHR.setRequestHeader( - "Accept", - s.dataTypes[ 0 ] && s.accepts[ s.dataTypes[ 0 ] ] ? - s.accepts[ s.dataTypes[ 0 ] ] + - ( s.dataTypes[ 0 ] !== "*" ? ", " + allTypes + "; q=0.01" : "" ) : - s.accepts[ "*" ] - ); - - // Check for headers option - for ( i in s.headers ) { - jqXHR.setRequestHeader( i, s.headers[ i ] ); - } - - // Allow custom headers/mimetypes and early abort - if ( s.beforeSend && - ( s.beforeSend.call( callbackContext, jqXHR, s ) === false || completed ) ) { - - // Abort if not done already and return - return jqXHR.abort(); - } - - // Aborting is no longer a cancellation - strAbort = "abort"; - - // Install callbacks on deferreds - completeDeferred.add( s.complete ); - jqXHR.done( s.success ); - jqXHR.fail( s.error ); - - // Get transport - transport = inspectPrefiltersOrTransports( transports, s, options, jqXHR ); - - // If no transport, we auto-abort - if ( !transport ) { - done( -1, "No Transport" ); - } else { - jqXHR.readyState = 1; - - // Send global event - if ( fireGlobals ) { - globalEventContext.trigger( "ajaxSend", [ jqXHR, s ] ); - } - - // If request was aborted inside ajaxSend, stop there - if ( completed ) { - return jqXHR; - } - - // Timeout - if ( s.async && s.timeout > 0 ) { - timeoutTimer = window.setTimeout( function() { - jqXHR.abort( "timeout" ); - }, s.timeout ); - } - - try { - completed = false; - transport.send( requestHeaders, done ); - } catch ( e ) { - - // Rethrow post-completion exceptions - if ( completed ) { - throw e; - } - - // Propagate others as results - done( -1, e ); - } - } - - // Callback for when everything is done - function done( status, nativeStatusText, responses, headers ) { - var isSuccess, success, error, response, modified, - statusText = nativeStatusText; - - // Ignore repeat invocations - if ( completed ) { - return; - } - - completed = true; - - // Clear timeout if it exists - if ( timeoutTimer ) { - window.clearTimeout( timeoutTimer ); - } - - // Dereference transport for early garbage collection - // (no matter how long the jqXHR object will be used) - transport = undefined; - - // Cache response headers - responseHeadersString = headers || ""; - - // Set readyState - jqXHR.readyState = status > 0 ? 4 : 0; - - // Determine if successful - isSuccess = status >= 200 && status < 300 || status === 304; - - // Get response data - if ( responses ) { - response = ajaxHandleResponses( s, jqXHR, responses ); - } - - // Use a noop converter for missing script but not if jsonp - if ( !isSuccess && - jQuery.inArray( "script", s.dataTypes ) > -1 && - jQuery.inArray( "json", s.dataTypes ) < 0 ) { - s.converters[ "text script" ] = function() {}; - } - - // Convert no matter what (that way responseXXX fields are always set) - response = ajaxConvert( s, response, jqXHR, isSuccess ); - - // If successful, handle type chaining - if ( isSuccess ) { - - // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode. - if ( s.ifModified ) { - modified = jqXHR.getResponseHeader( "Last-Modified" ); - if ( modified ) { - jQuery.lastModified[ cacheURL ] = modified; - } - modified = jqXHR.getResponseHeader( "etag" ); - if ( modified ) { - jQuery.etag[ cacheURL ] = modified; - } - } - - // if no content - if ( status === 204 || s.type === "HEAD" ) { - statusText = "nocontent"; - - // if not modified - } else if ( status === 304 ) { - statusText = "notmodified"; - - // If we have data, let's convert it - } else { - statusText = response.state; - success = response.data; - error = response.error; - isSuccess = !error; - } - } else { - - // Extract error from statusText and normalize for non-aborts - error = statusText; - if ( status || !statusText ) { - statusText = "error"; - if ( status < 0 ) { - status = 0; - } - } - } - - // Set data for the fake xhr object - jqXHR.status = status; - jqXHR.statusText = ( nativeStatusText || statusText ) + ""; - - // Success/Error - if ( isSuccess ) { - deferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] ); - } else { - deferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] ); - } - - // Status-dependent callbacks - jqXHR.statusCode( statusCode ); - statusCode = undefined; - - if ( fireGlobals ) { - globalEventContext.trigger( isSuccess ? "ajaxSuccess" : "ajaxError", - [ jqXHR, s, isSuccess ? success : error ] ); - } - - // Complete - completeDeferred.fireWith( callbackContext, [ jqXHR, statusText ] ); - - if ( fireGlobals ) { - globalEventContext.trigger( "ajaxComplete", [ jqXHR, s ] ); - - // Handle the global AJAX counter - if ( !( --jQuery.active ) ) { - jQuery.event.trigger( "ajaxStop" ); - } - } - } - - return jqXHR; - }, - - getJSON: function( url, data, callback ) { - return jQuery.get( url, data, callback, "json" ); - }, - - getScript: function( url, callback ) { - return jQuery.get( url, undefined, callback, "script" ); - } -} ); - -jQuery.each( [ "get", "post" ], function( _i, method ) { - jQuery[ method ] = function( url, data, callback, type ) { - - // Shift arguments if data argument was omitted - if ( isFunction( data ) ) { - type = type || callback; - callback = data; - data = undefined; - } - - // The url can be an options object (which then must have .url) - return jQuery.ajax( jQuery.extend( { - url: url, - type: method, - dataType: type, - data: data, - success: callback - }, jQuery.isPlainObject( url ) && url ) ); - }; -} ); - -jQuery.ajaxPrefilter( function( s ) { - var i; - for ( i in s.headers ) { - if ( i.toLowerCase() === "content-type" ) { - s.contentType = s.headers[ i ] || ""; - } - } -} ); - - -jQuery._evalUrl = function( url, options, doc ) { - return jQuery.ajax( { - url: url, - - // Make this explicit, since user can override this through ajaxSetup (trac-11264) - type: "GET", - dataType: "script", - cache: true, - async: false, - global: false, - - // Only evaluate the response if it is successful (gh-4126) - // dataFilter is not invoked for failure responses, so using it instead - // of the default converter is kludgy but it works. - converters: { - "text script": function() {} - }, - dataFilter: function( response ) { - jQuery.globalEval( response, options, doc ); - } - } ); -}; - - -jQuery.fn.extend( { - wrapAll: function( html ) { - var wrap; - - if ( this[ 0 ] ) { - if ( isFunction( html ) ) { - html = html.call( this[ 0 ] ); - } - - // The elements to wrap the target around - wrap = jQuery( html, this[ 0 ].ownerDocument ).eq( 0 ).clone( true ); - - if ( this[ 0 ].parentNode ) { - wrap.insertBefore( this[ 0 ] ); - } - - wrap.map( function() { - var elem = this; - - while ( elem.firstElementChild ) { - elem = elem.firstElementChild; - } - - return elem; - } ).append( this ); - } - - return this; - }, - - wrapInner: function( html ) { - if ( isFunction( html ) ) { - return this.each( function( i ) { - jQuery( this ).wrapInner( html.call( this, i ) ); - } ); - } - - return this.each( function() { - var self = jQuery( this ), - contents = self.contents(); - - if ( contents.length ) { - contents.wrapAll( html ); - - } else { - self.append( html ); - } - } ); - }, - - wrap: function( html ) { - var htmlIsFunction = isFunction( html ); - - return this.each( function( i ) { - jQuery( this ).wrapAll( htmlIsFunction ? html.call( this, i ) : html ); - } ); - }, - - unwrap: function( selector ) { - this.parent( selector ).not( "body" ).each( function() { - jQuery( this ).replaceWith( this.childNodes ); - } ); - return this; - } -} ); - - -jQuery.expr.pseudos.hidden = function( elem ) { - return !jQuery.expr.pseudos.visible( elem ); -}; -jQuery.expr.pseudos.visible = function( elem ) { - return !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length ); -}; - - - - -jQuery.ajaxSettings.xhr = function() { - try { - return new window.XMLHttpRequest(); - } catch ( e ) {} -}; - -var xhrSuccessStatus = { - - // File protocol always yields status code 0, assume 200 - 0: 200, - - // Support: IE <=9 only - // trac-1450: sometimes IE returns 1223 when it should be 204 - 1223: 204 - }, - xhrSupported = jQuery.ajaxSettings.xhr(); - -support.cors = !!xhrSupported && ( "withCredentials" in xhrSupported ); -support.ajax = xhrSupported = !!xhrSupported; - -jQuery.ajaxTransport( function( options ) { - var callback, errorCallback; - - // Cross domain only allowed if supported through XMLHttpRequest - if ( support.cors || xhrSupported && !options.crossDomain ) { - return { - send: function( headers, complete ) { - var i, - xhr = options.xhr(); - - xhr.open( - options.type, - options.url, - options.async, - options.username, - options.password - ); - - // Apply custom fields if provided - if ( options.xhrFields ) { - for ( i in options.xhrFields ) { - xhr[ i ] = options.xhrFields[ i ]; - } - } - - // Override mime type if needed - if ( options.mimeType && xhr.overrideMimeType ) { - xhr.overrideMimeType( options.mimeType ); - } - - // X-Requested-With header - // For cross-domain requests, seeing as conditions for a preflight are - // akin to a jigsaw puzzle, we simply never set it to be sure. - // (it can always be set on a per-request basis or even using ajaxSetup) - // For same-domain requests, won't change header if already provided. - if ( !options.crossDomain && !headers[ "X-Requested-With" ] ) { - headers[ "X-Requested-With" ] = "XMLHttpRequest"; - } - - // Set headers - for ( i in headers ) { - xhr.setRequestHeader( i, headers[ i ] ); - } - - // Callback - callback = function( type ) { - return function() { - if ( callback ) { - callback = errorCallback = xhr.onload = - xhr.onerror = xhr.onabort = xhr.ontimeout = - xhr.onreadystatechange = null; - - if ( type === "abort" ) { - xhr.abort(); - } else if ( type === "error" ) { - - // Support: IE <=9 only - // On a manual native abort, IE9 throws - // errors on any property access that is not readyState - if ( typeof xhr.status !== "number" ) { - complete( 0, "error" ); - } else { - complete( - - // File: protocol always yields status 0; see trac-8605, trac-14207 - xhr.status, - xhr.statusText - ); - } - } else { - complete( - xhrSuccessStatus[ xhr.status ] || xhr.status, - xhr.statusText, - - // Support: IE <=9 only - // IE9 has no XHR2 but throws on binary (trac-11426) - // For XHR2 non-text, let the caller handle it (gh-2498) - ( xhr.responseType || "text" ) !== "text" || - typeof xhr.responseText !== "string" ? - { binary: xhr.response } : - { text: xhr.responseText }, - xhr.getAllResponseHeaders() - ); - } - } - }; - }; - - // Listen to events - xhr.onload = callback(); - errorCallback = xhr.onerror = xhr.ontimeout = callback( "error" ); - - // Support: IE 9 only - // Use onreadystatechange to replace onabort - // to handle uncaught aborts - if ( xhr.onabort !== undefined ) { - xhr.onabort = errorCallback; - } else { - xhr.onreadystatechange = function() { - - // Check readyState before timeout as it changes - if ( xhr.readyState === 4 ) { - - // Allow onerror to be called first, - // but that will not handle a native abort - // Also, save errorCallback to a variable - // as xhr.onerror cannot be accessed - window.setTimeout( function() { - if ( callback ) { - errorCallback(); - } - } ); - } - }; - } - - // Create the abort callback - callback = callback( "abort" ); - - try { - - // Do send the request (this may raise an exception) - xhr.send( options.hasContent && options.data || null ); - } catch ( e ) { - - // trac-14683: Only rethrow if this hasn't been notified as an error yet - if ( callback ) { - throw e; - } - } - }, - - abort: function() { - if ( callback ) { - callback(); - } - } - }; - } -} ); - - - - -// Prevent auto-execution of scripts when no explicit dataType was provided (See gh-2432) -jQuery.ajaxPrefilter( function( s ) { - if ( s.crossDomain ) { - s.contents.script = false; - } -} ); - -// Install script dataType -jQuery.ajaxSetup( { - accepts: { - script: "text/javascript, application/javascript, " + - "application/ecmascript, application/x-ecmascript" - }, - contents: { - script: /\b(?:java|ecma)script\b/ - }, - converters: { - "text script": function( text ) { - jQuery.globalEval( text ); - return text; - } - } -} ); - -// Handle cache's special case and crossDomain -jQuery.ajaxPrefilter( "script", function( s ) { - if ( s.cache === undefined ) { - s.cache = false; - } - if ( s.crossDomain ) { - s.type = "GET"; - } -} ); - -// Bind script tag hack transport -jQuery.ajaxTransport( "script", function( s ) { - - // This transport only deals with cross domain or forced-by-attrs requests - if ( s.crossDomain || s.scriptAttrs ) { - var script, callback; - return { - send: function( _, complete ) { - script = jQuery( " -
      -

      {{ NOMBRE_SERVIDOR }}

      - - - -
      - - -
      -
      -
      -
      - - - - - - -
      -
      -

      Calculadora de talentos

      -
      - - - - - - - - - - -
      -
      - - - - - - - - - - - diff --git a/wotlk_db/tests.py b/wotlk_db/tests.py deleted file mode 100644 index 7ce503c..0000000 --- a/wotlk_db/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/wotlk_db/urls.py b/wotlk_db/urls.py deleted file mode 100644 index 9ec8361..0000000 --- a/wotlk_db/urls.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.urls import path -from . import views - -urlpatterns = [ - path('', views.home, name='home'), -] diff --git a/wotlk_db/views.py b/wotlk_db/views.py deleted file mode 100644 index b6bbef7..0000000 --- a/wotlk_db/views.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.shortcuts import render - - -def home(request): - return render(request, 'home/inicio.html') \ No newline at end of file