diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index 2b92c31..ed29034 100644 --- a/README.md +++ b/README.md @@ -12,13 +12,10 @@ Python 3.8+ must be installed, and a SQL database must be available. ## Notes -- The project settings are split into 3 files inside the "settings" directory. "base.py" contains the common - definitions, while "dev.py" and "prod.py" contain specific parameters for development and production environments - (see - - [Strategy based on multiple settings files](https://simpleisbetterthancomplex.com/tips/2017/07/03/django-tip-20-working-with-multiple-settings-modules.html) - ). -- The website and API are in the same Django project and app. The `HAS_API` and `HAS_WEBSITE` flags in the - `settings/prod.py` allow to control what is deployed. +- The project settings are split into 2 files inside the "settings" directory. "base.py" contains the common + definitions, while "dev.py" contains an example configuration for development. +- The website and API are in the same Django project and app. The `HAS_API` and `HAS_WEBSITE` flags allow + control over what is deployed. ## Running the application locally @@ -90,16 +87,16 @@ The database schema must be initialized and updated with: Notes: -- This will also create the "default" library, which can be customized through a few settings in `settings/base.py`: +- This will also create the "default" library, which can be customized through a few settings in `settings/dev.py`: - DEFAULT_SUPERUSER_LIBRARY_NAME = "Lyrasis" - DEFAULT_SUPERUSER_LIBRARY_IDENTIFIER = "lyra" - DEFAULT_SUPERUSER_LIBRARY_PREFIX = "0123" - DEFAULT_SUPERUSER_LIBRARY_STATE = "NY" + DEFAULT_SUPERUSER_LIBRARY_NAME + DEFAULT_SUPERUSER_LIBRARY_IDENTIFIER + DEFAULT_SUPERUSER_LIBRARY_PREFIX + DEFAULT_SUPERUSER_LIBRARY_STATE - The name of the superuser can be also configured in the settings files: - DEFAULT_SUPERUSER_FIRST_NAME = "superuser" + DEFAULT_SUPERUSER_FIRST_NAME ### 8. Select what (website or/and API) will be deployed diff --git a/pyproject.toml b/pyproject.toml index d6b4c8d..9a03cc0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,9 +9,10 @@ profile = "black" authors = ["The Palace Project "] description = "Virtual Library Card Creator" homepage = "https://thepalaceproject.org" +license = "Apache-2.0" name = "Virtual Library Card" readme = "README.md" -repository = "https://github.com/ThePalaceProject/ virtual-library-card" +repository = "https://github.com/ThePalaceProject/virtual-library-card" version = "0" # Version number is managed with tags in git [tool.poetry.dependencies] diff --git a/virtual_library_card/settings/__init__.py b/virtual_library_card/settings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/virtual_library_card/settings/base.py b/virtual_library_card/settings/base.py new file mode 100644 index 0000000..58b9217 --- /dev/null +++ b/virtual_library_card/settings/base.py @@ -0,0 +1,207 @@ +""" +Django settings for virtual_library_card project. + +Generated by 'django-admin startproject' using Django 2.2.5. + +For more information on this file, see +https://docs.djangoproject.com/en/2.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/2.2/ref/settings/ +""" + +import os + +from django.utils.translation import gettext_lazy as _ + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) + +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + +# Application definition + +INSTALLED_APPS = [ + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "dal", + "dal_select2", + "django.contrib.admin", + "localflavor", + "rest_framework", + "bootstrap4", + "crispy_forms", + "material", + "compressor", + "absoluteuri", + "virtuallibrarycard.apps.VirtuallibrarycardConfig", + "captcha", +] + +# Log level is taken from the env +LOGLEVEL = os.getenv("DJANGO_LOG_LEVEL", "INFO") +LOGGING = { + "version": 1, + "disable_existing_loggers": False, + "handlers": { + "console": { + "class": "logging.StreamHandler", + "formatter": "full", + } + }, + "formatters": { + "full": { + "format": "{asctime} [{levelname}] {name}: {message}", + "style": "{", + } + }, + "loggers": { + "django": { + "handlers": ["console"], + "level": "INFO", + }, + "app": { + "handlers": ["console"], + "level": LOGLEVEL, + "propagate": False, + }, + "root": { + "handlers": ["console"], + "level": LOGLEVEL, + }, + }, +} + +REST_FRAMEWORK = { + "DEFAULT_PERMISSION_CLASSES": [ + "rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly" + ], + "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", + "PAGE_SIZE": 10, +} + +CRISPY_TEMPLATE_PACK = "bootstrap4" + +CRISPY_ALLOWED_TEMPLATE_PACKS = ( + "bootstrap", + "uni_form", + "bootstrap3", + "bootstrap4", + "materialize_css_forms", +) + +AUTH_USER_MODEL = "virtuallibrarycard.CustomUser" + +MIDDLEWARE = [ + "django.middleware.csrf.CsrfViewMiddleware", + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.locale.LocaleMiddleware", + "django.middleware.common.CommonMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", +] + +USE_I18N = True + +ROOT_URLCONF = "virtual_library_card.urls" + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [os.path.join(BASE_DIR, "../templates/django")], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.template.context_processors.i18n", + "django.template.context_processors.media", + "django.template.context_processors.static", + "django.contrib.messages.context_processors.messages", + ], + }, + }, + { + "BACKEND": "django.template.backends.jinja2.Jinja2", + "DIRS": [os.path.join(BASE_DIR, "../templates/jinja")], + "APP_DIRS": True, + "OPTIONS": { + "environment": "virtual_library_card.jinja2.Environment", + "lstrip_blocks": True, + "trim_blocks": True, + }, + }, +] +WSGI_APPLICATION = "virtual_library_card.wsgi.application" + +# Password validation +# https://docs.djangoproject.com/en/2.2/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/2.2/topics/i18n/ + +LANGUAGE_CODE = "en" + +LANGUAGES = [ + ("en", _("English")), +] + +TIME_ZONE = "UTC" + +USE_L10N = True + +USE_TZ = True + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/2.2/howto/static-files/ + +PROJECT_PATH = os.path.dirname(os.path.abspath(__file__ + "/../../")) + +STATICFILES_DIRS = [ + os.path.join(PROJECT_PATH, "static"), +] + +STATICFILES_FINDERS = ( + "django.contrib.staticfiles.finders.FileSystemFinder", + "django.contrib.staticfiles.finders.AppDirectoriesFinder", + # other finders.. + "compressor.finders.CompressorFinder", +) + +MEDIA_URL = "/media/" +STATIC_URL = "/static/" +MEDIA_ROOT = os.path.join(os.path.dirname(BASE_DIR), "compiled/media") +STATIC_ROOT = os.path.join(os.path.dirname(BASE_DIR), "compiled/static") + +LOCALE_PATHS = (os.path.join(BASE_DIR, "../../locale"),) + +ACCOUNT_LOGIN_URL = "/accounts/login" + +COMPRESS_CSS_FILTERS = ("compressor_postcss.PostCSSFilter",) + +COMPRESS_POSTCSS_PLUGINS = ("autoprefixer", "postcss-font-magician") + +SITE_ID = 1 + +DATE_INPUT_FORMATS = ["%m-%d-%Y"] diff --git a/virtual_library_card/settings/dev.py b/virtual_library_card/settings/dev.py new file mode 100644 index 0000000..ba60f27 --- /dev/null +++ b/virtual_library_card/settings/dev.py @@ -0,0 +1,95 @@ +from .base import * + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = ["localhost"] + +## +# Set this setting to True when deploying the API. It will only make available the API urls. +## +HAS_API = True +HAS_WEBSITE = True + +# Database +# https://docs.djangoproject.com/en/2.2/ref/settings/#databases + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.postgresql_psycopg2", + "NAME": "virtual_library_card_dev", + "USER": "vlc", + "PASSWORD": "test", + "HOST": os.environ.get("VLC_DEV_DB_HOST", "pg"), + "PORT": os.environ.get("VLC_DEV_DB_PORT", "5432"), + } +} + +# Testing ONLY +SILENCED_SYSTEM_CHECKS = ["captcha.recaptcha_test_key_error"] + +ABSOLUTEURI_PROTOCOL = "http" +DATE_INPUT_FORMATS = ["%m-%d-%Y"] +CSRF_COOKIE_DOMAIN = None + +ROOT_URL = "http://localhost:8000" # Needed for self referential links (like emails) + +# The default value for the HTML_MINIFY setting is not DEBUG. You only need to set it to True if you want to minify your HTML code when DEBUG is enabled. +HTML_MINIFY = True + +# These are all dummy values for testing +MAPQUEST_API_KEY = "xxx" + +EMAIL_HOST = "xxx" +EMAIL_PORT = "xxx" +EMAIL_HOST_USER = "xxx" +EMAIL_HOST_PASSWORD = "xxx" +EMAIL_USE_TLS = True +DEFAULT_FROM_EMAIL = "xxx" + +SECRET_KEY = "xxx" + +ABSOLUTEURI_PROTOCOL = "http" + +# Testing specific keys +# https://developers.google.com/recaptcha/docs/faq#id-like-to-run-automated-tests-with-recaptcha.-what-should-i-do +RECAPTCHA_PUBLIC_KEY = "6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI" +RECAPTCHA_PRIVATE_KEY = "6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe" + +DEFAULT_FILE_STORAGE = "virtual_library_card.storage.S3PublicStorage" +STATICFILES_STORAGE = "virtual_library_card.storage.S3StaticStorage" +AWS_STORAGE_BUCKET_NAME = "vlc-test" +AWS_S3_ENDPOINT_URL = os.environ.get( + "VLC_DEV_AWS_S3_ENDPOINT_URL", "http://localhost:9000" +) +AWS_S3_CUSTOM_DOMAIN = os.environ.get("VLC_DEV_AWS_S3_CUSTOM_DOMAIN", False) +AWS_S3_URL_PROTOCOL = os.environ.get("VLC_DEV_AWS_S3_URL_PROTOCOL", "https:") + +# Either profile session name or a key/secret MUST be present +# In case key/secret is the way to go, please delete the session profile setting +# as it will take priority in the django-storages order +# If neither is provided the "default" profile will be used from the AWS credentials file +# AWS_S3_SESSION_PROFILE = "default" +AWS_S3_ACCESS_KEY_ID = "vlc-minio" +AWS_S3_SECRET_ACCESS_KEY = "123456789" + +# Dynamic link data +DYNAMIC_LINKS = { + "web_api_key": "test_web_key", + "domain_uri_prefix": "https://palacetest.page.link", + "android_package_name": "com.thepalaceproject.circulation", + "ios_bundle_id": "123456789", +} + +DYNAMIC_LINKS_SIGNUP_URL = "https://thepalaceproject.org/app" + +# To adjust depending on deployment +DEFAULT_PRIVACY_URL = "https://legal.palaceproject.io/Privacy%20Policy.html" + +# To allow specifying the name of the default library the superuser is associated with: +DEFAULT_SUPERUSER_LIBRARY_NAME = "Lyrasis" +DEFAULT_SUPERUSER_LIBRARY_IDENTIFIER = "lyra" +DEFAULT_SUPERUSER_LIBRARY_PREFIX = "0123" +DEFAULT_SUPERUSER_LIBRARY_STATE = "GA" +DEFAULT_SUPERUSER_FIRST_NAME = "superuser" +DEFAULT_LIBRARY_LOGO = "logo.png" diff --git a/virtuallibrarycard/migrations/0001_squashed_0092_remove_library_sequence_down_and_more.py b/virtuallibrarycard/migrations/0001_squashed_0092_remove_library_sequence_down_and_more.py new file mode 100644 index 0000000..1f2e57e --- /dev/null +++ b/virtuallibrarycard/migrations/0001_squashed_0092_remove_library_sequence_down_and_more.py @@ -0,0 +1,815 @@ +# Generated by Django 4.2.7 on 2024-02-09 14:55 +import logging + +import django.core.validators +import django.db.models.deletion +import django.utils.timezone +import localflavor.us.models +from django.conf import settings +from django.db import migrations, models + +import virtual_library_card.storage +import virtuallibrarycard.models +from virtuallibrarycard.business_rules.place_import import PlaceImport + + +def seed_places_ndjson(apps, schemaeditor): + Place = apps.get_model("virtuallibrarycard", "Place") + with open("static/ndjson/seed_places.ndjson") as fp: + PlaceImport(Place).import_ndjson(fp) + + +def migrate_states_to_places(apps, schemaeditor): + Place = apps.get_model("virtuallibrarycard", "Place") + LibraryStates = apps.get_model("virtuallibrarycard", "LibraryStates") + LibraryPlace = apps.get_model("virtuallibrarycard", "LibraryPlace") + Library = apps.get_model("virtuallibrarycard", "Library") + + ls_associations = LibraryStates.objects.all() + us_country = Place.objects.get(type="country", abbreviation="US") + + log = logging.getLogger(__name__) + for ls in ls_associations: + place = Place.objects.filter(abbreviation=ls.us_state).first() + if not place: + raise RuntimeError(f"Could not find '{ls.us_state}' in the places table") + + if LibraryPlace.objects.filter(library=ls.library, place=place).exists(): + continue + + # Save the new association + LibraryPlace(library=ls.library, place=place).save() + + # Any library that allows all US states shoudl associate with the country + for library in Library.objects.all(): + if library.allow_all_us_states: + if not LibraryPlace.objects.filter( + library=library, place=us_country + ).exists(): + LibraryPlace(library=library, place=us_country).save() + else: + log.info(f"US country already associated with {library.name}") + + +def bb_places_ndjson(apps, schemaeditor): + Place = apps.get_model("virtuallibrarycard", "Place") + with open("static/ndjson/bb-places.ndjson") as fp: + PlaceImport(Place).import_ndjson(fp) + + +class Migration(migrations.Migration): + + replaces = [ + ("virtuallibrarycard", "0001_initial"), + ("virtuallibrarycard", "0002_auto_20190923_1654"), + ("virtuallibrarycard", "0003_auto_20190923_1657"), + ("virtuallibrarycard", "0004_delete_libraryadmin"), + ("virtuallibrarycard", "0005_auto_20190923_1809"), + ("virtuallibrarycard", "0006_librarycard_library"), + ("virtuallibrarycard", "0007_customuser_local_public_library"), + ("virtuallibrarycard", "0008_customuser_virtuallibrarycard"), + ("virtuallibrarycard", "0009_auto_20190923_1904"), + ("virtuallibrarycard", "0010_auto_20190926_1726"), + ("virtuallibrarycard", "0011_customuser_usstate"), + ("virtuallibrarycard", "0012_auto_20190926_1756"), + ("virtuallibrarycard", "0013_auto_20190927_0945"), + ("virtuallibrarycard", "0014_auto_20190930_0900"), + ("virtuallibrarycard", "0015_auto_20190930_0932"), + ("virtuallibrarycard", "0016_auto_20190930_0941"), + ("virtuallibrarycard", "0017_auto_20190930_1009"), + ("virtuallibrarycard", "0018_auto_20190930_1057"), + ("virtuallibrarycard", "0019_customuser_country_code"), + ("virtuallibrarycard", "0020_auto_20190930_1302"), + ("virtuallibrarycard", "0021_remove_customuser_country_code"), + ("virtuallibrarycard", "0022_auto_20190930_1758"), + ("virtuallibrarycard", "0023_auto_20190930_1835"), + ("virtuallibrarycard", "0024_auto_20191001_1533"), + ("virtuallibrarycard", "0025_auto_20191001_1843"), + ("virtuallibrarycard", "0026_auto_20191001_1845"), + ("virtuallibrarycard", "0027_auto_20191001_1846"), + ("virtuallibrarycard", "0028_auto_20191002_1316"), + ("virtuallibrarycard", "0029_auto_20191002_2033"), + ("virtuallibrarycard", "0030_auto_20191002_2045"), + ("virtuallibrarycard", "0031_auto_20191002_2047"), + ("virtuallibrarycard", "0032_auto_20191003_0938"), + ("virtuallibrarycard", "0033_auto_20191003_0942"), + ("virtuallibrarycard", "0034_auto_20191007_0916"), + ("virtuallibrarycard", "0035_customuser_country_code"), + ("virtuallibrarycard", "0036_auto_20191008_1820"), + ("virtuallibrarycard", "0037_auto_20191009_1003"), + ("virtuallibrarycard", "0038_auto_20191009_1047"), + ("virtuallibrarycard", "0039_auto_20191011_1700"), + ("virtuallibrarycard", "0040_auto_20191014_1353"), + ("virtuallibrarycard", "0041_auto_20191016_1602"), + ("virtuallibrarycard", "0042_auto_20191016_1821"), + ("virtuallibrarycard", "0043_auto_20191018_1657"), + ("virtuallibrarycard", "0044_auto_20191025_1815"), + ("virtuallibrarycard", "0045_auto_20191029_1359"), + ("virtuallibrarycard", "0046_auto_20191030_0940"), + ("virtuallibrarycard", "0047_auto_20191030_0943"), + ("virtuallibrarycard", "0048_library_pin_requirement"), + ("virtuallibrarycard", "0049_auto_20191030_1445"), + ("virtuallibrarycard", "0050_auto_20191030_1448"), + ("virtuallibrarycard", "0051_auto_20191030_1450"), + ("virtuallibrarycard", "0052_auto_20191030_1454"), + ("virtuallibrarycard", "0053_auto_20191030_1614"), + ("virtuallibrarycard", "0054_auto_20191030_1633"), + ("virtuallibrarycard", "0055_auto_20191030_1701"), + ("virtuallibrarycard", "0056_auto_20191105_1502"), + ("virtuallibrarycard", "0057_auto_20191205_0948"), + ("virtuallibrarycard", "0058_remove_library_pin_requirement"), + ("virtuallibrarycard", "0059_librarystates"), + ("virtuallibrarycard", "0059_library_patron_address_mandatory"), + ("virtuallibrarycard", "0060_auto_20220411_1153"), + ("virtuallibrarycard", "0061_auto_20220602_0814"), + ("virtuallibrarycard", "0061_auto_20220526_0732"), + ("virtuallibrarycard", "0062_merge_20220624_0518"), + ("virtuallibrarycard", "0062_auto_20220622_0724"), + ("virtuallibrarycard", "0063_merge_20220624_0557"), + ("virtuallibrarycard", "0064_auto_20220624_0600"), + ("virtuallibrarycard", "0062_auto_20220623_0819"), + ("virtuallibrarycard", "0065_merge_20220711_0858"), + ("virtuallibrarycard", "0066_library_allow_all_us_states"), + ("virtuallibrarycard", "0066_library_age_verification_mandatory"), + ("virtuallibrarycard", "0067_merge_20220802_0554"), + ("virtuallibrarycard", "0068_library_bulk_upload_prefix"), + ("virtuallibrarycard", "0069_library_allow_bulk_card_uploads"), + ("virtuallibrarycard", "0070_auto_20220919_1157"), + ("virtuallibrarycard", "0071_place"), + ("virtuallibrarycard", "0072_auto_20221018_0638"), + ("virtuallibrarycard", "0073_auto_20221018_0808"), + ("virtuallibrarycard", "0074_auto_20221018_0809"), + ("virtuallibrarycard", "0075_alter_customuser_us_state"), + ("virtuallibrarycard", "0076_auto_20221019_1011"), + ("virtuallibrarycard", "0077_libraryplace_unique_library_place"), + ("virtuallibrarycard", "0071_alter_library_logo"), + ("virtuallibrarycard", "0072_auto_20221003_0552"), + ("virtuallibrarycard", "0073_auto_20221019_2313"), + ("virtuallibrarycard", "0074_auto_20221025_1146"), + ("virtuallibrarycard", "0078_merge_20221116_0945"), + ("virtuallibrarycard", "0071_auto_20221012_0655"), + ( + "virtuallibrarycard", + "0075_merge_0071_auto_20221012_0655_0074_auto_20221025_1146", + ), + ("virtuallibrarycard", "0079_merge_20221116_1125"), + ("virtuallibrarycard", "0080_alter_customuser_zip"), + ("virtuallibrarycard", "0081_auto_20230323_0723"), + ("virtuallibrarycard", "0081_auto_20230323_0700"), + ( + "virtuallibrarycard", + "0082_merge_0081_auto_20230323_0700_0081_auto_20230323_0723", + ), + ("virtuallibrarycard", "0083_auto_20230331_0748"), + ("virtuallibrarycard", "0084_alter_place_external_id"), + ("virtuallibrarycard", "0085_auto_20230412_1214"), + ("virtuallibrarycard", "0086_alter_library_customization"), + ("virtuallibrarycard", "0087_auto_20230509_0628"), + ("virtuallibrarycard", "0088_library_has_survey_consent"), + ("virtuallibrarycard", "0087_place_name_fixes"), + ("virtuallibrarycard", "0089_merge_20230512_0736"), + ("virtuallibrarycard", "0090_alter_library_privacy_url"), + ("virtuallibrarycard", "0091_library_uuid"), + ("virtuallibrarycard", "0092_remove_library_sequence_down_and_more"), + ] + + initial = True + + dependencies = [ + ("auth", "0011_update_proxy_permissions"), + ] + + operations = [ + migrations.CreateModel( + name="Library", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "card_validity_months", + models.PositiveSmallIntegerField(blank=True, null=True), + ), + ( + "logo", + models.ImageField( + null=True, + storage=virtual_library_card.storage.OverwriteStorage(), + upload_to=virtuallibrarycard.models.Library.generate_filename, + ), + ), + ("name", models.CharField(max_length=255, null=True)), + ("prefix", models.CharField(max_length=10, null=True)), + ("email", models.CharField(blank=True, max_length=255, null=True)), + ( + "identifier", + models.CharField(max_length=255, null=True, unique=True), + ), + ("phone", models.CharField(blank=True, max_length=50, null=True)), + ("terms_conditions_url", models.CharField(max_length=255)), + ( + "social_facebook", + models.CharField(blank=True, max_length=255, null=True), + ), + ( + "social_twitter", + models.CharField(blank=True, max_length=255, null=True), + ), + ("sequence_start_number", models.IntegerField(default=0)), + ( + "sequence_down", + models.BooleanField( + choices=[(True, "Descending"), (False, "Ascending")], + default=False, + ), + ), + ("sequence_end_number", models.IntegerField(blank=True, null=True)), + ( + "us_state", + localflavor.us.models.USStateField( + max_length=2, verbose_name="State" + ), + ), + ( + "privacy_url", + models.CharField( + default="https://legal.palaceproject.io/Privacy%20Policy.html", + max_length=255, + ), + ), + ("patron_address_mandatory", models.BooleanField(default=True)), + ], + options={ + "verbose_name_plural": "libraries", + }, + ), + migrations.CreateModel( + name="CustomUser", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("password", models.CharField(max_length=128, verbose_name="password")), + ( + "last_login", + models.DateTimeField( + blank=True, null=True, verbose_name="last login" + ), + ), + ( + "is_superuser", + models.BooleanField( + default=False, + help_text="Designates that this user has all permissions without explicitly assigning them.", + verbose_name="superuser status", + ), + ), + ( + "username", + models.CharField( + blank=True, + editable=False, + max_length=50, + null=True, + verbose_name="username", + ), + ), + ( + "first_name", + models.CharField(max_length=30, verbose_name="first name"), + ), + ( + "last_name", + models.CharField( + blank=True, max_length=150, verbose_name="last name" + ), + ), + ( + "email", + models.EmailField( + max_length=254, unique=True, verbose_name="Email address" + ), + ), + ( + "is_staff", + models.BooleanField( + default=False, + help_text="Designates whether the user can log into this admin site.", + verbose_name="staff status", + ), + ), + ( + "is_active", + models.BooleanField( + default=True, + help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.", + verbose_name="active", + ), + ), + ( + "date_joined", + models.DateTimeField( + default=django.utils.timezone.now, verbose_name="date joined" + ), + ), + ("external_type", models.CharField(max_length=255, null=True)), + ( + "groups", + models.ManyToManyField( + blank=True, + help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", + related_name="user_set", + related_query_name="user", + to="auth.group", + verbose_name="groups", + ), + ), + ( + "user_permissions", + models.ManyToManyField( + blank=True, + help_text="Specific permissions for this user.", + related_name="user_set", + related_query_name="user", + to="auth.permission", + verbose_name="user permissions", + ), + ), + ("authorization_expires", models.DateField(null=True)), + ( + "authorization_identifier", + models.CharField(max_length=255, null=True), + ), + ("city", models.CharField(max_length=255, null=True)), + ("permanent_id", models.CharField(max_length=255, null=True)), + ( + "street_address_line1", + models.CharField( + max_length=255, null=True, verbose_name="Street address line 1" + ), + ), + ( + "street_address_line2", + models.CharField( + blank=True, + max_length=255, + null=True, + verbose_name="Street address line 2", + ), + ), + ( + "zip", + localflavor.us.models.USZipCodeField( + default="0", max_length=10, verbose_name="Zip code" + ), + ), + ( + "library", + models.ForeignKey( + default=1, + on_delete=django.db.models.deletion.PROTECT, + to="virtuallibrarycard.library", + ), + ), + ( + "us_state", + localflavor.us.models.USStateField( + max_length=2, verbose_name="State" + ), + ), + ( + "country_code", + models.CharField(default="US", max_length=255, null=True), + ), + ( + "over13", + models.BooleanField( + default=True, + verbose_name=" I certify that I am over 13 years old", + ), + ), + ], + options={ + "verbose_name": "user", + "verbose_name_plural": "users", + "abstract": False, + }, + ), + migrations.CreateModel( + name="LibraryCard", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("expiration_date", models.DateTimeField(blank=True, null=True)), + ("number", models.CharField(max_length=100, null=True)), + ( + "library", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="virtuallibrarycard.library", + ), + ), + ( + "user", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.PROTECT, + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "created", + models.DateTimeField( + default=django.utils.timezone.now, verbose_name="created" + ), + ), + ( + "canceled_by_user", + models.CharField(blank=True, max_length=255, null=True), + ), + ("canceled_date", models.DateTimeField(blank=True, null=True)), + ], + ), + migrations.CreateModel( + name="LibraryStates", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "us_state", + localflavor.us.models.USStateField( + max_length=2, verbose_name="State" + ), + ), + ( + "library", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="virtuallibrarycard.library", + ), + ), + ], + ), + migrations.AddField( + model_name="customuser", + name="email_verified", + field=models.BooleanField(default=True), + ), + migrations.AddField( + model_name="library", + name="barcode_text", + field=models.CharField( + default="barcode", max_length=255, verbose_name="Barcode Text" + ), + ), + migrations.AddField( + model_name="library", + name="pin_text", + field=models.CharField( + default="pin", max_length=255, verbose_name="Pin Text" + ), + ), + migrations.AlterField( + model_name="librarystates", + name="library", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="library_states", + to="virtuallibrarycard.library", + ), + ), + migrations.CreateModel( + name="LibraryAllowedEmailDomains", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "domain", + virtuallibrarycard.models.LowerCharField( + max_length=100, + validators=[virtuallibrarycard.models.validate_domain], + ), + ), + ( + "library", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="library_email_domains", + to="virtuallibrarycard.library", + ), + ), + ], + ), + migrations.AddConstraint( + model_name="libraryallowedemaildomains", + constraint=models.UniqueConstraint( + fields=("library", "domain"), + name="virtuallibrarycard_library_domain_unique", + ), + ), + migrations.AlterField( + model_name="customuser", + name="library", + field=models.ForeignKey( + default=virtuallibrarycard.models.default_library, + on_delete=django.db.models.deletion.PROTECT, + to="virtuallibrarycard.library", + ), + ), + migrations.AlterField( + model_name="customuser", + name="zip", + field=localflavor.us.models.USZipCodeField( + max_length=10, null=True, verbose_name="Zip code" + ), + ), + migrations.AddField( + model_name="library", + name="allow_all_us_states", + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name="library", + name="age_verification_mandatory", + field=models.BooleanField(default=True), + ), + migrations.AddField( + model_name="library", + name="bulk_upload_prefix", + field=models.CharField(max_length=10, null=True), + ), + migrations.AddField( + model_name="library", + name="allow_bulk_card_uploads", + field=models.BooleanField(default=False), + ), + migrations.AlterField( + model_name="librarycard", + name="user", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.CreateModel( + name="Place", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, primary_key=True, serialize=False + ), + ), + ("external_id", models.CharField(max_length=20, unique=True)), + ("abbreviation", models.CharField(blank=True, max_length=5)), + ("name", models.CharField(max_length=100)), + ("type", models.CharField(max_length=20)), + ("latitude", models.FloatField(null=True)), + ("longitude", models.FloatField(null=True)), + ( + "parent", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="virtuallibrarycard.place", + ), + ), + ], + ), + migrations.RunPython( + code=seed_places_ndjson, + ), + migrations.AddField( + model_name="customuser", + name="place", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="virtuallibrarycard.place", + ), + ), + migrations.CreateModel( + name="LibraryPlace", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, primary_key=True, serialize=False + ), + ), + ( + "library", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="library_places", + to="virtuallibrarycard.library", + ), + ), + ( + "place", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="place_libraries", + to="virtuallibrarycard.place", + ), + ), + ], + ), + migrations.RunPython( + code=migrate_states_to_places, + ), + migrations.AlterField( + model_name="customuser", + name="us_state", + field=localflavor.us.models.USStateField(max_length=2, null=True), + ), + migrations.AddConstraint( + model_name="libraryplace", + constraint=models.UniqueConstraint( + fields=("library", "place"), name="unique_library_place" + ), + ), + migrations.AlterField( + model_name="library", + name="logo", + field=models.ImageField( + null=True, upload_to=virtuallibrarycard.models.Library.generate_filename + ), + ), + migrations.CreateModel( + name="LibraryCustomization", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, primary_key=True, serialize=False + ), + ), + ( + "welcome_email_top_text", + models.TextField(blank=True, max_length=512, null=True), + ), + ( + "welcome_email_bottom_text", + models.TextField(blank=True, max_length=512, null=True), + ), + ], + ), + migrations.AddField( + model_name="library", + name="customization", + field=models.ForeignKey( + default=virtuallibrarycard.models.default_customization, + on_delete=django.db.models.deletion.PROTECT, + related_name="library", + to="virtuallibrarycard.librarycustomization", + ), + ), + migrations.AlterField( + model_name="customuser", + name="zip", + field=models.CharField( + max_length=10, + null=True, + validators=[ + django.core.validators.RegexValidator( + "^([0-9A-Z]{3}(?: [A-Z0-9]{3})?)|(\\d{5}(?:-\\d{4})?)$", + message="Enter a zip code in the format [XXX or XXX XXX](Canada) or [XXXXX or XXXXX-XXXX](USA)", + ) + ], + ), + ), + migrations.RemoveField( + model_name="customuser", + name="us_state", + ), + migrations.RemoveField( + model_name="library", + name="allow_all_us_states", + ), + migrations.RemoveField( + model_name="library", + name="us_state", + ), + migrations.DeleteModel( + name="LibraryStates", + ), + migrations.RemoveField( + model_name="place", + name="latitude", + ), + migrations.RemoveField( + model_name="place", + name="longitude", + ), + migrations.AlterField( + model_name="place", + name="external_id", + field=models.CharField(max_length=128, unique=True), + ), + migrations.RunPython( + code=bb_places_ndjson, + ), + migrations.AlterField( + model_name="library", + name="customization", + field=models.ForeignKey( + default=virtuallibrarycard.models.default_customization, + on_delete=django.db.models.deletion.PROTECT, + related_name="library", + to="virtuallibrarycard.librarycustomization", + unique=True, + ), + ), + migrations.CreateModel( + name="UserConsent", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, primary_key=True, serialize=False + ), + ), + ( + "timestamp", + models.DateTimeField( + default=virtuallibrarycard.models.default_timestamp + ), + ), + ("method", models.CharField(max_length=50)), + ("type", models.CharField(max_length=50)), + ("version", models.CharField(max_length=10)), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="consents", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + migrations.AddConstraint( + model_name="userconsent", + constraint=models.UniqueConstraint( + fields=("user", "type"), name="virtuallibrarycard_unique_type_user" + ), + ), + migrations.AddField( + model_name="library", + name="has_survey_consent", + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name="library", + name="uuid", + field=models.CharField(blank=True, max_length=255, null=True, unique=True), + ), + migrations.RemoveField( + model_name="library", + name="sequence_down", + ), + migrations.RemoveField( + model_name="library", + name="sequence_end_number", + ), + migrations.RemoveField( + model_name="library", + name="sequence_start_number", + ), + ] diff --git a/virtuallibrarycard/migrations/__init__.py b/virtuallibrarycard/migrations/__init__.py new file mode 100644 index 0000000..e69de29