diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..d55c83d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,166 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# Ide +.idea/ +.vscode/ + +# docker specific +.data/ + +# project specific +/static/ + +# Docker image specific +/.github/ \ No newline at end of file diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..989e489 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,126 @@ +name: Tagged Deploy + +# based on: https://docs.docker.com/build/ci/github-actions/multi-platform/#distribute-build-across-multiple-runners + +on: + push: + tags: + - 'v*' + +env: + REGISTRY_IMAGE: dhilsfu/intertidal + + +jobs: + build: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + platform: + # - linux/386 + - linux/amd64 + # - linux/arm/v6 + # - linux/arm/v7 # psycopg[binary] issue + - linux/arm64 + - linux/arm64/v8 + # - linux/ppc64le # Pillow issue + # - linux/s390x # psycopg[binary] issue + steps: + - name: Prepare + run: | + platform=${{ matrix.platform }} + echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV + + - name: Checkout source code + uses: actions/checkout@v4 + + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY_IMAGE }} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} + + - name: Build and push by digest + id: build + uses: docker/build-push-action@v5 + with: + context: . + platforms: ${{ matrix.platform }} + labels: ${{ steps.meta.outputs.labels }} + outputs: type=image,name=${{ env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true + cache-from: type=registry,ref=${{ env.REGISTRY_IMAGE }}:buildcache-${{ env.PLATFORM_PAIR }} + cache-to: type=registry,ref=${{ env.REGISTRY_IMAGE }}:buildcache-${{ env.PLATFORM_PAIR }},mode=max + + - name: Export digest + run: | + mkdir -p /tmp/digests + digest="${{ steps.build.outputs.digest }}" + touch "/tmp/digests/${digest#sha256:}" + + - name: Upload digest + uses: actions/upload-artifact@v4 + with: + name: digests-${{ env.PLATFORM_PAIR }} + path: /tmp/digests/* + if-no-files-found: error + retention-days: 1 + + push: + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/') + needs: + - build + steps: + - name: Download digests + uses: actions/download-artifact@v4 + with: + path: /tmp/digests + pattern: digests-* + merge-multiple: true + + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY_IMAGE }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} + + - name: Create manifest list and push + working-directory: /tmp/digests + run: | + docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ + $(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *) + + - name: Inspect image + run: | + docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.meta.outputs.version }} + + - name: Trigger Gitlab Deploy Job + run: | + curl -X POST \ + --fail \ + -F token=${{ secrets.GITLAB_CI_TOKEN }} \ + -F "ref=main" \ + -F "variables[APP_RELEASE_TAG]=${{github.ref_name}}" \ + https://git.lib.sfu.ca/api/v4/projects/587/trigger/pipeline \ No newline at end of file diff --git a/.gitignore b/.gitignore index 82f9275..b83a404 100644 --- a/.gitignore +++ b/.gitignore @@ -106,10 +106,8 @@ ipython_config.py #pdm.lock # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it # in version control. -# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +# https://pdm.fming.dev/#use-with-ide .pdm.toml -.pdm-python -.pdm-build/ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm __pypackages__/ @@ -154,9 +152,12 @@ dmypy.json # Cython debug symbols cython_debug/ -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ +# Ide +.idea/ +.vscode/ + +# docker specific +.data/ + +# project specific +/static/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ea9556f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,25 @@ +# Django app +FROM python:3.11-alpine AS intertidal +EXPOSE 80 +WORKDIR /app + +# add system deps +RUN apk update \ + && apk add git libmagic curl \ + && pip install --no-cache-dir --upgrade pip \ + && rm -rf /var/cache/apk/* + +# install python deps +COPY requirements.txt /app +RUN pip install -r requirements.txt --no-cache-dir + +# add project files +COPY . /app + +# add prod assets + +# collect static assets for production +RUN python manage.py collectstatic --noinput + +# run migrations and start server +CMD ["docker/docker-entrypoint.sh"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..9f07fe8 --- /dev/null +++ b/README.md @@ -0,0 +1,112 @@ +# Intertidal + + + + +## Requirements + +- [Docker Desktop](https://www.docker.com/products/docker-desktop/) + +## Initialize the Application + + docker compose up -d --build + +Intertidal Frontend will be available at `http://localhost:8080/` +Intertidal Admin will be available at `http://localhost:8080/admin/` + +### Install/Switch the admin theme + + # Bootstrap + docker exec -it intertidal_app python manage.py loaddata admin_interface_theme_bootstrap.json + + # Django + docker exec -it intertidal_app python manage.py loaddata admin_interface_theme_django.json + + # Foundation + docker exec -it intertidal_app python manage.py loaddata admin_interface_theme_foundation.json + + # U.S. Web Design Standards + docker exec -it intertidal_app python manage.py loaddata admin_interface_theme_uswds.json + +### Create your superuser + + docker exec -it intertidal_app python manage.py createsuperuser + +Enter `username`, `email`, and `password` as prompted + +example: + + docker exec -it intertidal_app python manage.py createsuperuser --username="admin" --email="dhil@sfu.ca" + +## General Usage + +### Starting the Application + + docker compose up -d + +### Stopping the Application + + docker compose down + +### Rebuilding the Application (after upstream or js/python package changes) + + docker compose up -d --build + +### Viewing logs (each container) + + docker logs -f intertidal_app + docker logs -f intertidal_db + docker logs -f intertidal_mail + +### Accessing the Application + + http://localhost:8080/ + +### Accessing the Database + +Command line: + + docker exec -it intertidal_db mysql -u intertidal -ppassword intertidal + +Through a database management tool: +- Host:`127.0.0.1` +- Port: `15432` +- Username: `intertidal` +- Password: `password` + +### Accessing Mailhog (catches emails sent by the app) + + http://localhost:8025/ + +### Database Migrations + +Migrate up to latest + + docker exec -it intertidal_app python manage.py migrate + +Create new migrations + + docker exec -it intertidal_app python manage.py makemigrations + +## Updating Application Dependencies + +### Pip (python) + +Manage python dependencies in `requirements.txt` +>All packages should be locked to a specific version number if possible (Ex `Django==4.2.7`) +>Some special packages cannot be locked and should be noted as such (Ex `psycopg[binary]`) + +After making changes, you need to run pip or rebuild the image + + docker exec -it intertidal_app pip install -r requirements.txt + # or + docker compose up -d --build + +#### Update a package + +Edit version number in `requirements.txt` with new locked version number +>Ex `pip==24.0.0` + + docker exec -it intertidal_app pip install -r requirements.txt + # or + docker compose up -d --build diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..4f9162f --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,75 @@ +services: + db: + container_name: intertidal_db + image: postgres:16.2 + ports: + - "15432:5432" + volumes: + - .data/postgres:/var/lib/postgresql/data/pgdata + environment: + PGDATA: /var/lib/postgresql/data/pgdata + POSTGRES_DB: intertidal + POSTGRES_USER: intertidal + POSTGRES_PASSWORD: password + healthcheck: + test: ["CMD-SHELL", "pg_isready", "-U", "intertidal", "-d", "intertidal"] + interval: 2s + retries: 120 + + nginx: + container_name: intertidal_nginx + image: nginx:1.25 + volumes: + # config + - ./docker/nginx.conf:/etc/nginx/nginx.conf + # mount app media and static content + - .data/media:/media:ro + # - .data/static:/static:ro # do not mount in development mode + ports: + - "8080:80" + depends_on: + app: + condition: service_healthy + + app: + container_name: intertidal_app + # image: dhil/intertidal + build: + context: . + target: intertidal + ports: + - "8888:80" + volumes: + # code for development + - .:/app + + # files uploads + - .data/media:/media + + # # persist static outside of container so it can be shared with nginx + # - .data/static:/app/static # do not mount in development mode + environment: + DEBUG: True + DB_HOST: db + DB_NAME: intertidal + DB_USER: intertidal + DB_PASSWORD: password + EMAIL_HOST: mail + EMAIL_HOST_USER: intertidal + EMAIL_HOST_PASSWORD: password + MEDIA_FOLDER_UID: 101 + MEDIA_FOLDER_GID: 101 + GUNICORN_RELOAD: True + healthcheck: + test: ["CMD-SHELL", "curl --fail http://localhost/health_check/ || exit 1"] + interval: 2s + retries: 120 + depends_on: + db: + condition: service_healthy + + mail: + container_name: intertidal_mail + image: jcalonso/mailhog:v1.0.1 + ports: + - "8025:8025" \ No newline at end of file diff --git a/docker/docker-entrypoint.sh b/docker/docker-entrypoint.sh new file mode 100755 index 0000000..86c54e8 --- /dev/null +++ b/docker/docker-entrypoint.sh @@ -0,0 +1,26 @@ +#!/bin/sh +set -e + +# app specific setup here +python manage.py migrate + +mkdir -p /app/static +chown $MEDIA_FOLDER_UID:$MEDIA_FOLDER_GID /app/static +# collect static if needed +python manage.py collectstatic --noinput + +export GIT_COMMIT=$(git rev-parse HEAD) +export GIT_COMMIT_SHORT=$(git rev-parse --short HEAD) +export GIT_BRANCH=$(git branch --show-current) +export GIT_TAG=$(git tag --points-at HEAD | head -n 1) + +# fix media folder permissions for nginx +MEDIA_FOLDER_UID=${MEDIA_FOLDER_UID-101} +MEDIA_FOLDER_GID=${MEDIA_FOLDER_GID-101} +mkdir -p /media/audio /media/videos /media/captions /media/images /media/thumbnails +chown $MEDIA_FOLDER_UID:$MEDIA_FOLDER_GID /media /media/audio /media/videos /media/captions /media/images /media/thumbnails + +# ensure django file cache directory exists +mkdir -p /django_cache + +gunicorn --config /app/gunicorn.config.py intertidal_app.wsgi:application \ No newline at end of file diff --git a/docker/nginx.conf b/docker/nginx.conf new file mode 100644 index 0000000..77a2690 --- /dev/null +++ b/docker/nginx.conf @@ -0,0 +1,44 @@ +events { + worker_connections 1024; +} + +http { + include mime.types; + sendfile on; + + server { + listen 80; + autoindex off; + gzip_static on; + server_tokens off; + client_max_body_size 256M; + + # access_log /var/log/nginx/access.log; + access_log /dev/stdout; + # error_log /var/log/nginx/error.log; + error_log /dev/stderr; + + location / { + # update docker swarm network ips resolvers (valid for 10s). + # needed for referenced processes are quickly added/removed/changed over time + resolver 127.0.0.11 valid=10s ipv6=off; + set $django_app app:80; + proxy_pass http://$django_app; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + # proxy_set_header X-Forwarded-Proto https; + # proxy_set_header X-Forwarded-Port 443; + proxy_set_header Host $host; + proxy_redirect off; + } + + location /media/ { + alias /media/; + } + + # # dont use in development mode + # location /static/ { + # alias /static/; + # try_files $uri $uri.js =404; + # } + } +} \ No newline at end of file diff --git a/gunicorn.config.py b/gunicorn.config.py new file mode 100644 index 0000000..b6e1fa5 --- /dev/null +++ b/gunicorn.config.py @@ -0,0 +1,25 @@ +from environ import FileAwareEnv +from dotenv import load_dotenv, find_dotenv +from multiprocessing import cpu_count +from pathlib import Path + +env = FileAwareEnv() +load_dotenv(find_dotenv()) + +bind = '0.0.0.0:80' +workers = min(cpu_count(), 4) # don't hog system resources + +# accesslog = '-' # skip access log (can get from nginx) +errorlog = '-' +loglevel = 'info' if env.bool('DEBUG', default=False) else 'error' +capture_output = True + +# handle dev reloading +reload = env.bool('GUNICORN_RELOAD', default=False) +# handle extra files for reload +def getDirExtraFiles(dir): + return [ + str(path) for path in list((dir / 'static/').rglob('*.*')) + list((dir / 'templates/').rglob('*.*')) + ] +BASE_DIR = Path(__file__).parent +reload_extra_files = getDirExtraFiles(BASE_DIR / 'intertidal/') + getDirExtraFiles(BASE_DIR / 'intertidal_app/') if reload else [] \ No newline at end of file diff --git a/intertidal/__init__.py b/intertidal/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/intertidal/admin.py b/intertidal/admin.py new file mode 100644 index 0000000..208a38f --- /dev/null +++ b/intertidal/admin.py @@ -0,0 +1,213 @@ +from django.contrib import admin +from partial_date import PartialDateField +from django.contrib.postgres.fields import ArrayField +from nested_admin.nested import NestedModelAdmin, NestedTabularInline, NestedGenericTabularInline +from datetime import datetime +from django.utils.safestring import mark_safe + +from intertidal.models import Resource, Edition, Occurrence, \ + PersonResponsibilityStatement, Person, \ + OrganizationResponsibilityStatement, Organization +from intertidal.widgets import PartialDateWidget, Select2ChoiceArrayWidget, Select2TagArrayWidget +from intertidal.marc_relators import MarcRelator +from intertidal.cls_types import ClsTypes + +PARTIAL_DATE_WIDGET_YEARS = list(reversed(range(0, datetime.today().year+1))) + +class PersonResponsibilityStatementInline(NestedGenericTabularInline): + fields = ['person', 'marc_relators', 'note'] + autocomplete_fields = ['person'] + model = PersonResponsibilityStatement + ordering = ['id'] + extra = 0 + classes = ['collapse'] + readonly_fields = ['note'] + formfield_overrides = { + ArrayField: { + 'widget': Select2ChoiceArrayWidget( + attrs={ + 'data-placeholder': 'Click to select one or more responsibilities', + }, + choices=MarcRelator.choices, + ), + }, + } + + def note(self, obj): + return mark_safe('See the list of MARC Relators for descriptions of each responsibility') + +class OrganizationResponsibilityStatementInline(NestedGenericTabularInline): + fields = ['organization', 'marc_relators', 'note'] + autocomplete_fields = ['organization'] + model = OrganizationResponsibilityStatement + ordering = ['id'] + extra = 0 + classes = ['collapse'] + readonly_fields = ['note'] + formfield_overrides = { + ArrayField: { + 'widget': Select2ChoiceArrayWidget( + attrs={ + 'data-placeholder': 'Click to select one or more responsibilities', + }, + choices=MarcRelator.choices, + ), + }, + } + + def note(self, obj): + return mark_safe('See the list of MARC Relators for descriptions of each responsibility/role') + +class EditionInlineAdmin(NestedTabularInline): + fields = ['date', 'name', 'translation', 'translation_language'] + ordering = ['id'] + model = Edition + extra = 0 + classes = ['collapse'] + formfield_overrides = { + PartialDateField: { + 'widget': PartialDateWidget(years=PARTIAL_DATE_WIDGET_YEARS), + } + } + + inlines = [ + PersonResponsibilityStatementInline, + OrganizationResponsibilityStatementInline, + ] + +class OccurrenceInlineAdmin(NestedTabularInline): + fields = [ + ('date', 'date_end', 'date_current'), + ('location', 'address') + ] + ordering = ['id'] + model = Occurrence + extra = 0 + classes = ['collapse'] + formfield_overrides = { + PartialDateField: { + 'widget': PartialDateWidget(years=PARTIAL_DATE_WIDGET_YEARS), + } + } + + inlines = [ + PersonResponsibilityStatementInline, + OrganizationResponsibilityStatementInline, + ] + +@admin.register(Person) +class PersonAdmin(admin.ModelAdmin): + fields = [ + ('name', 'alternative_names'), + 'links', + 'emails', + ] + list_display = ('id', 'name', 'alternative_names') + list_display_links = ('id', 'name', 'alternative_names') + ordering = ['id'] + search_fields = ['name', 'alternative_names'] + + def formfield_for_dbfield(self, db_field, **kwargs): + if db_field.name == 'alternative_names': + kwargs['widget'] = Select2TagArrayWidget(attrs={ + 'data-placeholder': 'Click to add one or more alternative names', + }) + elif db_field.name == 'links': + kwargs['widget'] = Select2TagArrayWidget(attrs={ + 'data-placeholder': 'Click to add one or more links', + }) + elif db_field.name == 'emails': + kwargs['widget'] = Select2TagArrayWidget(attrs={ + 'data-placeholder': 'Click to add one or more emails', + }) + return super().formfield_for_dbfield(db_field, **kwargs) + + +@admin.register(Organization) +class OrganizationAdmin(admin.ModelAdmin): + fields = [ + ('name', 'alternative_names'), + 'address', + 'links', + 'emails', + ] + list_display = ('id', 'name', 'alternative_names') + list_display_links = ('id', 'name', 'alternative_names') + ordering = ['id'] + search_fields = ['name', 'alternative_names'] + + def formfield_for_dbfield(self, db_field, **kwargs): + if db_field.name == 'alternative_names': + kwargs['widget'] = Select2TagArrayWidget(attrs={ + 'data-placeholder': 'Click to add one or more alternative names', + }) + elif db_field.name == 'links': + kwargs['widget'] = Select2TagArrayWidget(attrs={ + 'data-placeholder': 'Click to add one or more links', + }) + elif db_field.name == 'emails': + kwargs['widget'] = Select2TagArrayWidget(attrs={ + 'data-placeholder': 'Click to add one or more emails', + }) + return super().formfield_for_dbfield(db_field, **kwargs) + +@admin.register(Resource) +class ResourceAdmin(NestedModelAdmin): + fieldsets = ( + (None, { + 'fields': ( + ('locale', 'display_category'), + ('name', 'alternative_names'), + ('date', 'date_end', 'date_current'), + 'forms', + 'genres', + 'keywords', + 'links', + ) + }), + ('Description/Notes', { + 'classes': ('collapse',), + 'fields': ('description', 'notes'), + }), + ) + list_display = ('id', 'locale', 'display_category', 'name', 'alternative_names', 'date') + list_display_links = ('id', 'locale', 'display_category', 'name', 'alternative_names', 'date') + ordering = ['locale', 'display_category', 'name'] + search_fields = ['locale', 'display_category', 'name', 'alternative_names'] + formfield_overrides = { + PartialDateField: { + 'widget': PartialDateWidget(years=PARTIAL_DATE_WIDGET_YEARS), + } + } + inlines = [ + PersonResponsibilityStatementInline, + OrganizationResponsibilityStatementInline, + EditionInlineAdmin, + OccurrenceInlineAdmin, + ] + + def formfield_for_dbfield(self, db_field, **kwargs): + if db_field.name == 'alternative_names': + kwargs['widget'] = Select2TagArrayWidget(attrs={ + 'data-placeholder': 'Click to add one or more alternative names', + }) + elif db_field.name == 'forms': + kwargs['widget'] = Select2ChoiceArrayWidget( + attrs={ + 'data-placeholder': 'Click to select one or more forms', + }, + choices=ClsTypes.choices, + ) + elif db_field.name == 'genres': + kwargs['widget'] = Select2TagArrayWidget(attrs={ + 'data-placeholder': 'Click to add one or more genres', + }) + elif db_field.name == 'keywords': + kwargs['widget'] = Select2TagArrayWidget(attrs={ + 'data-placeholder': 'Click to add one or more keywords', + }) + elif db_field.name == 'links': + kwargs['widget'] = Select2TagArrayWidget(attrs={ + 'data-placeholder': 'Click to add one or more links', + }) + return super().formfield_for_dbfield(db_field, **kwargs) diff --git a/intertidal/apps.py b/intertidal/apps.py new file mode 100644 index 0000000..82ef001 --- /dev/null +++ b/intertidal/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class IntertidalConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'intertidal' diff --git a/intertidal/cls_types.py b/intertidal/cls_types.py new file mode 100644 index 0000000..139d2f0 --- /dev/null +++ b/intertidal/cls_types.py @@ -0,0 +1,48 @@ +from django.db import models + +class ClsTypes(models.TextChoices): + ARTICLE = "article", "Article" + ARTICLE_JOURNAL = "article_journal", "Article Journal" + ARTICLE_MAGAZINE = "article_magazine", "Article Magazine" + ARTICLE_NEWSPAPER = "article_newspaper", "Article Newspaper" + BILL = "bill", "Bill" + BOOK = "book", "Book" + BROADCAST = "broadcast", "Broadcast" + CHAPTER = "chapter", "Chapter" + CLASSIC = "classic", "Classic" + COLLECTION = "collection", "Collection" + DATASET = "dataset", "Dataset" + DOCUMENT = "document", "Document" + ENTRY = "entry", "Entry" + ENTRY_DICTIONARY = "entry_dictionary", "Entry Dictionary" + ENTRY_ENCYCLOPEDIA = "entry_encyclopedia", "Entry Encyclopedia" + EVENT = "event", "Event" + FIGURE = "figure", "Figure" + GRAPHIC = "graphic", "Graphic" + HEARING = "hearing", "Hearing" + INTERVIEW = "interview", "Interview" + LEGAL_CASE = "legal_case", "Legal Case" + LEGISLATION = "legislation", "Legislation" + MANUSCRIPT = "manuscript", "Manuscript" + MAP = "map", "Map" + MOTION_PICTURE = "motion_picture", "Motion Picture" + MUSICAL_SCORE = "musical_score", "Musical Score" + PAMPHLET = "pamphlet", "Pamphlet" + PAPER_CONFERENCE = "paper_conference", "Paper Conference" + PATENT = "patent", "Patent" + PERFORMANCE = "performance", "Performance" + PERIODICAL = "periodical", "Periodical" + PERSONAL_COMMUNICATION = "personal_communication", "Personal Communication" + POST = "post", "Post" + POST_WEBLOG = "post_weblog", "Post Weblog" + REGULATION = "regulation", "Regulation" + REPORT = "report", "Report" + REVIEW = "review", "Review" + REVIEW_BOOK = "review_book", "Review Book" + SOFTWARE = "software", "Software" + SONG = "song", "Song" + SPEECH = "speech", "Speech" + STANDARD = "standard", "Standard" + THESIS = "thesis", "Thesis" + TREATY = "treaty", "Treaty" + WEBPAGE = "webpage", "Webpage" diff --git a/intertidal/marc_relators.py b/intertidal/marc_relators.py new file mode 100644 index 0000000..a6b0422 --- /dev/null +++ b/intertidal/marc_relators.py @@ -0,0 +1,296 @@ +from django.db import models + +class MarcRelator(models.TextChoices): + ABR = "abr", "Abridger" + ACT = "act", "Actor" + ADP = "adp", "Adapter" + RCP = "rcp", "Addressee" + ANL = "anl", "Analyst" + ANM = "anm", "Animator" + ANN = "ann", "Annotator" + ANC = "anc", "Announcer" + APL = "apl", "Appellant" + APE = "ape", "Appellee" + APP = "app", "Applicant" + ARC = "arc", "Architect" + ARR = "arr", "Arranger" + ACP = "acp", "Art copyist" + ADI = "adi", "Art director" + ART = "art", "Artist" + ARD = "ard", "Artistic director" + ASG = "asg", "Assignee" + ASN = "asn", "Associated name" + ATT = "att", "Attributed name" + AUC = "auc", "Auctioneer" + AUE = "aue", "Audio engineer" + AUP = "aup", "Audio producer" + AUT = "aut", "Author" + AQT = "aqt", "Author in quotations or text abstracts" + AFT = "aft", "Author of afterword, colophon, etc." + AUD = "aud", "Author of dialog" + AUI = "aui", "Author of introduction, etc." + ATO = "ato", "Autographer" + ANT = "ant", "Bibliographic antecedent" + BND = "bnd", "Binder" + BDD = "bdd", "Binding designer" + BLW = "blw", "Blurb writer" + BKA = "bka", "Book artist" + BKD = "bkd", "Book designer" + BKP = "bkp", "Book producer" + BJD = "bjd", "Bookjacket designer" + BPD = "bpd", "Bookplate designer" + BSL = "bsl", "Bookseller" + BRL = "brl", "Braille embosser" + BRD = "brd", "Broadcaster" + CLL = "cll", "Calligrapher" + COP = "cop", "Camera operator" + CTG = "ctg", "Cartographer" + CAS = "cas", "Caster" + CAD = "cad", "Casting director" + CNS = "cns", "Censor" + CHR = "chr", "Choreographer" + CNG = "cng", "Cinematographer" + CLI = "cli", "Client" + COR = "cor", "Collection registrar" + COL = "col", "Collector" + CLT = "clt", "Collotyper" + CLR = "clr", "Colorist" + CMM = "cmm", "Commentator" + CWT = "cwt", "Commentator for written text" + COM = "com", "Compiler" + CPL = "cpl", "Complainant" + CPT = "cpt", "Complainant-appellant" + CPE = "cpe", "Complainant-appellee" + CMP = "cmp", "Composer" + CMT = "cmt", "Compositor" + CCP = "ccp", "Conceptor" + CND = "cnd", "Conductor" + CON = "con", "Conservator" + CSL = "csl", "Consultant" + CSP = "csp", "Consultant to a project" + COS = "cos", "Contestant" + COT = "cot", "Contestant-appellant" + COE = "coe", "Contestant-appellee" + CTS = "cts", "Contestee" + CTT = "ctt", "Contestee-appellant" + CTE = "cte", "Contestee-appellee" + CTR = "ctr", "Contractor" + CTB = "ctb", "Contributor" + CPC = "cpc", "Copyright claimant" + CPH = "cph", "Copyright holder" + CRR = "crr", "Corrector" + CRP = "crp", "Correspondent" + CST = "cst", "Costume designer" + COU = "cou", "Court governed" + CRT = "crt", "Court reporter" + COV = "cov", "Cover designer" + CRE = "cre", "Creator" + CUR = "cur", "Curator" + DNC = "dnc", "Dancer" + DTC = "dtc", "Data contributor" + DTM = "dtm", "Data manager" + DTE = "dte", "Dedicatee" + DTO = "dto", "Dedicator" + DFD = "dfd", "Defendant" + DFT = "dft", "Defendant-appellant" + DFE = "dfe", "Defendant-appellee" + DGC = "dgc", "Degree committee member" + DGG = "dgg", "Degree granting institution" + DGS = "dgs", "Degree supervisor" + DLN = "dln", "Delineator" + DPC = "dpc", "Depicted" + DPT = "dpt", "Depositor" + DSR = "dsr", "Designer" + DRT = "drt", "Director" + DIS = "dis", "Dissertant" + DBP = "dbp", "Distribution place" + DJO = "djo", "DJ" + DNR = "dnr", "Donor" + DRM = "drm", "Draftsman" + DBD = "dbd", "Dubbing director" + DUB = "dub", "Dubious author" + EDT = "edt", "Editor" + EDC = "edc", "Editor of compilation" + EDM = "edm", "Editor of moving image work" + EDD = "edd", "Editorial director" + ELG = "elg", "Electrician" + ELT = "elt", "Electrotyper" + ENJ = "enj", "Enacting jurisdiction" + ENG = "eng", "Engineer" + EGR = "egr", "Engraver" + ETR = "etr", "Etcher" + EVP = "evp", "Event place" + EXP = "exp", "Expert" + FAC = "fac", "Facsimilist" + FLD = "fld", "Field director" + FMD = "fmd", "Film director" + FDS = "fds", "Film distributor" + FLM = "flm", "Film editor" + FMP = "fmp", "Film producer" + FMK = "fmk", "Filmmaker" + FPY = "fpy", "First party" + FRG = "frg", "Forger" + FMO = "fmo", "Former owner" + FON = "fon", "Founder" + FND = "fnd", "Funder" + GIS = "gis", "Geographic information specialist" + HNR = "hnr", "Honoree" + HST = "hst", "Host" + HIS = "his", "Host institution" + ILU = "ilu", "Illuminator" + ILL = "ill", "Illustrator" + INS = "ins", "Inscriber" + ITR = "itr", "Instrumentalist" + IVE = "ive", "Interviewee" + IVR = "ivr", "Interviewer" + INV = "inv", "Inventor" + ISB = "isb", "Issuing body" + JUD = "jud", "Judge" + JUG = "jug", "Jurisdiction governed" + LBR = "lbr", "Laboratory" + LDR = "ldr", "Laboratory director" + LSA = "lsa", "Landscape architect" + LED = "led", "Lead" + LEN = "len", "Lender" + LIL = "lil", "Libelant" + LIT = "lit", "Libelant-appellant" + LIE = "lie", "Libelant-appellee" + LEL = "lel", "Libelee" + LET = "let", "Libelee-appellant" + LEE = "lee", "Libelee-appellee" + LBT = "lbt", "Librettist" + LSE = "lse", "Licensee" + LSO = "lso", "Licensor" + LGD = "lgd", "Lighting designer" + LTG = "ltg", "Lithographer" + LYR = "lyr", "Lyricist" + MKA = "mka", "Makeup artist" + MFP = "mfp", "Manufacture place" + MFR = "mfr", "Manufacturer" + MRB = "mrb", "Marbler" + MRK = "mrk", "Markup editor" + MED = "med", "Medium" + MDC = "mdc", "Metadata contact" + MTE = "mte", "Metal-engraver" + MTK = "mtk", "Minute taker" + MXE = "mxe", "Mixing engineer" + MOD = "mod", "Moderator" + MON = "mon", "Monitor" + MCP = "mcp", "Music copyist" + MUP = "mup", "Music programmer" + MSD = "msd", "Musical director" + MUS = "mus", "Musician" + NRT = "nrt", "Narrator" + NAN = "nan", "News anchor" + ONP = "onp", "Onscreen participant" + OSP = "osp", "Onscreen presenter" + OPN = "opn", "Opponent" + ORM = "orm", "Organizer" + ORG = "org", "Originator" + OTH = "oth", "Other" + OWN = "own", "Owner" + PAN = "pan", "Panelist" + PPM = "ppm", "Papermaker" + PTA = "pta", "Patent applicant" + PTH = "pth", "Patent holder" + PAT = "pat", "Patron" + PRF = "prf", "Performer" + PMA = "pma", "Permitting agency" + PHT = "pht", "Photographer" + PAD = "pad", "Place of address" + PTF = "ptf", "Plaintiff" + PTT = "ptt", "Plaintiff-appellant" + PTE = "pte", "Plaintiff-appellee" + PLT = "plt", "Platemaker" + PRA = "pra", "Praeses" + PRE = "pre", "Presenter" + PRT = "prt", "Printer" + POP = "pop", "Printer of plates" + PRM = "prm", "Printmaker" + PRC = "prc", "Process contact" + PRO = "pro", "Producer" + PRN = "prn", "Production company" + PRS = "prs", "Production designer" + PMN = "pmn", "Production manager" + PRD = "prd", "Production personnel" + PRP = "prp", "Production place" + PRG = "prg", "Programmer" + PDR = "pdr", "Project director" + PFR = "pfr", "Proofreader" + PRV = "prv", "Provider" + PUP = "pup", "Publication place" + PBL = "pbl", "Publisher" + PBD = "pbd", "Publishing director" + PPT = "ppt", "Puppeteer" + RDD = "rdd", "Radio director" + RPC = "rpc", "Radio producer" + RAP = "rap", "Rapporteur" + RCE = "rce", "Recording engineer" + RCD = "rcd", "Recordist" + RED = "red", "Redaktor" + RXA = "rxa", "Remix artist" + REN = "ren", "Renderer" + RPT = "rpt", "Reporter" + RPS = "rps", "Repository" + RTH = "rth", "Research team head" + RTM = "rtm", "Research team member" + RES = "res", "Researcher" + RSP = "rsp", "Respondent" + RST = "rst", "Respondent-appellant" + RSE = "rse", "Respondent-appellee" + RPY = "rpy", "Responsible party" + RSG = "rsg", "Restager" + RSR = "rsr", "Restorationist" + REV = "rev", "Reviewer" + RBR = "rbr", "Rubricator" + SCE = "sce", "Scenarist" + SAD = "sad", "Scientific advisor" + AUS = "aus", "Screenwriter" + SCR = "scr", "Scribe" + SCL = "scl", "Sculptor" + SPY = "spy", "Second party" + SEC = "sec", "Secretary" + SLL = "sll", "Seller" + STD = "std", "Set designer" + STG = "stg", "Setting" + SGN = "sgn", "Signer" + SNG = "sng", "Singer" + SWD = "swd", "Software developer" + SDS = "sds", "Sound designer" + SDE = "sde", "Sound engineer" + SPK = "spk", "Speaker" + SFX = "sfx", "Special effects provider" + SPN = "spn", "Sponsor" + SGD = "sgd", "Stage director" + STM = "stm", "Stage manager" + STN = "stn", "Standards body" + STR = "str", "Stereotyper" + STL = "stl", "Storyteller" + SHT = "sht", "Supporting host" + SRV = "srv", "Surveyor" + TCH = "tch", "Teacher" + TCD = "tcd", "Technical director" + TLD = "tld", "Television director" + TLG = "tlg", "Television guest" + TLH = "tlh", "Television host" + TLP = "tlp", "Television producer" + TAU = "tau", "Television writer" + THS = "ths", "Thesis advisor" + TRC = "trc", "Transcriber" + TRL = "trl", "Translator" + TYD = "tyd", "Type designer" + TYG = "tyg", "Typographer" + UVP = "uvp", "University place" + VDG = "vdg", "Videographer" + VFX = "vfx", "Visual effects provider" + VAC = "vac", "Voice actor" + WIT = "wit", "Witness" + WDE = "wde", "Wood engraver" + WDC = "wdc", "Woodcutter" + WAM = "wam", "Writer of accompanying material" + WAC = "wac", "Writer of added commentary" + WAL = "wal", "Writer of added lyrics" + WAT = "wat", "Writer of added text" + WIN = "win", "Writer of introduction" + WPR = "wpr", "Writer of preface" + WST = "wst", "Writer of supplementary textual content" \ No newline at end of file diff --git a/intertidal/migrations/0001_initial.py b/intertidal/migrations/0001_initial.py new file mode 100644 index 0000000..077ff83 --- /dev/null +++ b/intertidal/migrations/0001_initial.py @@ -0,0 +1,126 @@ +# Generated by Django 5.1.3 on 2024-11-07 00:23 + +import django.db.models.deletion +import intertidal.models +import partial_date.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ] + + operations = [ + migrations.CreateModel( + name='Organization', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(db_index=True)), + ('alternative_names', intertidal.models.ArrayField(base_field=models.CharField(), blank=True, default=list, size=None)), + ('address', models.CharField(blank=True)), + ('links', intertidal.models.ArrayField(base_field=models.CharField(), blank=True, default=list, size=None)), + ('emails', intertidal.models.ArrayField(base_field=models.CharField(), blank=True, default=list, size=None)), + ('created', models.DateTimeField(auto_now_add=True)), + ('modified', models.DateTimeField(auto_now=True)), + ], + ), + migrations.CreateModel( + name='Person', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(db_index=True)), + ('alternative_names', intertidal.models.ArrayField(base_field=models.CharField(), blank=True, default=list, size=None)), + ('links', intertidal.models.ArrayField(base_field=models.CharField(), blank=True, default=list, size=None)), + ('emails', intertidal.models.ArrayField(base_field=models.CharField(), blank=True, default=list, size=None)), + ('created', models.DateTimeField(auto_now_add=True)), + ('modified', models.DateTimeField(auto_now=True)), + ], + options={ + 'verbose_name_plural': 'people', + }, + ), + migrations.CreateModel( + name='Resource', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('locale', models.CharField(choices=[('VANCOUVER', 'Vancouver'), ('SINGAPORE', 'Singapore'), ('HONG_KONG', 'Hong Kong')], db_index=True, default='VANCOUVER')), + ('display_category', models.CharField(choices=[('LITERARY_WORK', 'Literary Work'), ('ART_PERFORMANCE', 'Art/Performance'), ('STATE_ARCHITECTURE_MISC', 'State/Architecture/Misc.'), ('NEWS_DOCUMENTARY', 'News/Documentary'), ('ACADEMIC_RESEARCH', 'Academic Research'), ('SOCIAL_MEDIA', 'Social Media')], db_index=True, default='LITERARY_WORK')), + ('name', models.CharField(db_index=True, verbose_name='Name/Title')), + ('alternative_names', intertidal.models.ArrayField(base_field=models.CharField(), blank=True, default=list, size=None, verbose_name='Alternative Names/Titles')), + ('forms', intertidal.models.ArrayField(base_field=models.CharField(choices=[('article', 'Article'), ('article_journal', 'Article Journal'), ('article_magazine', 'Article Magazine'), ('article_newspaper', 'Article Newspaper'), ('bill', 'Bill'), ('book', 'Book'), ('broadcast', 'Broadcast'), ('chapter', 'Chapter'), ('classic', 'Classic'), ('collection', 'Collection'), ('dataset', 'Dataset'), ('document', 'Document'), ('entry', 'Entry'), ('entry_dictionary', 'Entry Dictionary'), ('entry_encyclopedia', 'Entry Encyclopedia'), ('event', 'Event'), ('figure', 'Figure'), ('graphic', 'Graphic'), ('hearing', 'Hearing'), ('interview', 'Interview'), ('legal_case', 'Legal Case'), ('legislation', 'Legislation'), ('manuscript', 'Manuscript'), ('map', 'Map'), ('motion_picture', 'Motion Picture'), ('musical_score', 'Musical Score'), ('pamphlet', 'Pamphlet'), ('paper_conference', 'Paper Conference'), ('patent', 'Patent'), ('performance', 'Performance'), ('periodical', 'Periodical'), ('personal_communication', 'Personal Communication'), ('post', 'Post'), ('post_weblog', 'Post Weblog'), ('regulation', 'Regulation'), ('report', 'Report'), ('review', 'Review'), ('review_book', 'Review Book'), ('software', 'Software'), ('song', 'Song'), ('speech', 'Speech'), ('standard', 'Standard'), ('thesis', 'Thesis'), ('treaty', 'Treaty'), ('webpage', 'Webpage')]), blank=True, default=list, help_text='See the list of Citation Style Language Types for descriptions of each form', size=None, verbose_name='Physical/Digital Forms')), + ('genres', intertidal.models.ArrayField(base_field=models.CharField(), blank=True, default=list, size=None)), + ('keywords', intertidal.models.ArrayField(base_field=models.CharField(), blank=True, default=list, size=None)), + ('date', partial_date.fields.PartialDateField(blank=True, db_index=True, null=True)), + ('date_end', partial_date.fields.PartialDateField(blank=True, null=True)), + ('date_current', models.BooleanField(default=False)), + ('description', models.TextField(blank=True)), + ('notes', models.TextField(blank=True)), + ('links', intertidal.models.ArrayField(base_field=models.CharField(), blank=True, default=list, size=None)), + ('created', models.DateTimeField(auto_now_add=True)), + ('modified', models.DateTimeField(auto_now=True)), + ], + ), + migrations.CreateModel( + name='Occurrence', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('location', models.CharField(blank=True, verbose_name='Location/Venue')), + ('address', models.CharField(blank=True)), + ('date', partial_date.fields.PartialDateField(blank=True, db_index=True, null=True)), + ('date_end', partial_date.fields.PartialDateField(blank=True, null=True)), + ('date_current', models.BooleanField(default=False)), + ('created', models.DateTimeField(auto_now_add=True)), + ('modified', models.DateTimeField(auto_now=True)), + ('resource', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='occurrences', to='intertidal.resource')), + ], + ), + migrations.CreateModel( + name='Edition', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(blank=True, verbose_name='Name/Title')), + ('translation', models.BooleanField(default=False)), + ('translation_language', models.CharField(blank=True, choices=[('af', 'Afrikaans'), ('ar', 'Arabic'), ('ar-dz', 'Algerian Arabic'), ('ast', 'Asturian'), ('az', 'Azerbaijani'), ('bg', 'Bulgarian'), ('be', 'Belarusian'), ('bn', 'Bengali'), ('br', 'Breton'), ('bs', 'Bosnian'), ('ca', 'Catalan'), ('ckb', 'Central Kurdish (Sorani)'), ('cs', 'Czech'), ('cy', 'Welsh'), ('da', 'Danish'), ('de', 'German'), ('dsb', 'Lower Sorbian'), ('el', 'Greek'), ('en', 'English'), ('en-au', 'Australian English'), ('en-gb', 'British English'), ('eo', 'Esperanto'), ('es', 'Spanish'), ('es-ar', 'Argentinian Spanish'), ('es-co', 'Colombian Spanish'), ('es-mx', 'Mexican Spanish'), ('es-ni', 'Nicaraguan Spanish'), ('es-ve', 'Venezuelan Spanish'), ('et', 'Estonian'), ('eu', 'Basque'), ('fa', 'Persian'), ('fi', 'Finnish'), ('fr', 'French'), ('fy', 'Frisian'), ('ga', 'Irish'), ('gd', 'Scottish Gaelic'), ('gl', 'Galician'), ('he', 'Hebrew'), ('hi', 'Hindi'), ('hr', 'Croatian'), ('hsb', 'Upper Sorbian'), ('hu', 'Hungarian'), ('hy', 'Armenian'), ('ia', 'Interlingua'), ('id', 'Indonesian'), ('ig', 'Igbo'), ('io', 'Ido'), ('is', 'Icelandic'), ('it', 'Italian'), ('ja', 'Japanese'), ('ka', 'Georgian'), ('kab', 'Kabyle'), ('kk', 'Kazakh'), ('km', 'Khmer'), ('kn', 'Kannada'), ('ko', 'Korean'), ('ky', 'Kyrgyz'), ('lb', 'Luxembourgish'), ('lt', 'Lithuanian'), ('lv', 'Latvian'), ('mk', 'Macedonian'), ('ml', 'Malayalam'), ('mn', 'Mongolian'), ('mr', 'Marathi'), ('ms', 'Malay'), ('my', 'Burmese'), ('nb', 'Norwegian Bokmål'), ('ne', 'Nepali'), ('nl', 'Dutch'), ('nn', 'Norwegian Nynorsk'), ('os', 'Ossetic'), ('pa', 'Punjabi'), ('pl', 'Polish'), ('pt', 'Portuguese'), ('pt-br', 'Brazilian Portuguese'), ('ro', 'Romanian'), ('ru', 'Russian'), ('sk', 'Slovak'), ('sl', 'Slovenian'), ('sq', 'Albanian'), ('sr', 'Serbian'), ('sr-latn', 'Serbian Latin'), ('sv', 'Swedish'), ('sw', 'Swahili'), ('ta', 'Tamil'), ('te', 'Telugu'), ('tg', 'Tajik'), ('th', 'Thai'), ('tk', 'Turkmen'), ('tr', 'Turkish'), ('tt', 'Tatar'), ('udm', 'Udmurt'), ('ug', 'Uyghur'), ('uk', 'Ukrainian'), ('ur', 'Urdu'), ('uz', 'Uzbek'), ('vi', 'Vietnamese'), ('zh-hans', 'Simplified Chinese'), ('zh-hant', 'Traditional Chinese')], default='en', verbose_name='language')), + ('date', partial_date.fields.PartialDateField(blank=True, db_index=True, null=True)), + ('created', models.DateTimeField(auto_now_add=True)), + ('modified', models.DateTimeField(auto_now=True)), + ('resource', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='editions', to='intertidal.resource')), + ], + ), + migrations.CreateModel( + name='OrganizationResponsibilityStatement', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('object_id', models.BigIntegerField()), + ('marc_relators', intertidal.models.ArrayField(base_field=models.CharField(choices=[('abr', 'Abridger'), ('act', 'Actor'), ('adp', 'Adapter'), ('rcp', 'Addressee'), ('anl', 'Analyst'), ('anm', 'Animator'), ('ann', 'Annotator'), ('anc', 'Announcer'), ('apl', 'Appellant'), ('ape', 'Appellee'), ('app', 'Applicant'), ('arc', 'Architect'), ('arr', 'Arranger'), ('acp', 'Art copyist'), ('adi', 'Art director'), ('art', 'Artist'), ('ard', 'Artistic director'), ('asg', 'Assignee'), ('asn', 'Associated name'), ('att', 'Attributed name'), ('auc', 'Auctioneer'), ('aue', 'Audio engineer'), ('aup', 'Audio producer'), ('aut', 'Author'), ('aqt', 'Author in quotations or text abstracts'), ('aft', 'Author of afterword, colophon, etc.'), ('aud', 'Author of dialog'), ('aui', 'Author of introduction, etc.'), ('ato', 'Autographer'), ('ant', 'Bibliographic antecedent'), ('bnd', 'Binder'), ('bdd', 'Binding designer'), ('blw', 'Blurb writer'), ('bka', 'Book artist'), ('bkd', 'Book designer'), ('bkp', 'Book producer'), ('bjd', 'Bookjacket designer'), ('bpd', 'Bookplate designer'), ('bsl', 'Bookseller'), ('brl', 'Braille embosser'), ('brd', 'Broadcaster'), ('cll', 'Calligrapher'), ('cop', 'Camera operator'), ('ctg', 'Cartographer'), ('cas', 'Caster'), ('cad', 'Casting director'), ('cns', 'Censor'), ('chr', 'Choreographer'), ('cng', 'Cinematographer'), ('cli', 'Client'), ('cor', 'Collection registrar'), ('col', 'Collector'), ('clt', 'Collotyper'), ('clr', 'Colorist'), ('cmm', 'Commentator'), ('cwt', 'Commentator for written text'), ('com', 'Compiler'), ('cpl', 'Complainant'), ('cpt', 'Complainant-appellant'), ('cpe', 'Complainant-appellee'), ('cmp', 'Composer'), ('cmt', 'Compositor'), ('ccp', 'Conceptor'), ('cnd', 'Conductor'), ('con', 'Conservator'), ('csl', 'Consultant'), ('csp', 'Consultant to a project'), ('cos', 'Contestant'), ('cot', 'Contestant-appellant'), ('coe', 'Contestant-appellee'), ('cts', 'Contestee'), ('ctt', 'Contestee-appellant'), ('cte', 'Contestee-appellee'), ('ctr', 'Contractor'), ('ctb', 'Contributor'), ('cpc', 'Copyright claimant'), ('cph', 'Copyright holder'), ('crr', 'Corrector'), ('crp', 'Correspondent'), ('cst', 'Costume designer'), ('cou', 'Court governed'), ('crt', 'Court reporter'), ('cov', 'Cover designer'), ('cre', 'Creator'), ('cur', 'Curator'), ('dnc', 'Dancer'), ('dtc', 'Data contributor'), ('dtm', 'Data manager'), ('dte', 'Dedicatee'), ('dto', 'Dedicator'), ('dfd', 'Defendant'), ('dft', 'Defendant-appellant'), ('dfe', 'Defendant-appellee'), ('dgc', 'Degree committee member'), ('dgg', 'Degree granting institution'), ('dgs', 'Degree supervisor'), ('dln', 'Delineator'), ('dpc', 'Depicted'), ('dpt', 'Depositor'), ('dsr', 'Designer'), ('drt', 'Director'), ('dis', 'Dissertant'), ('dbp', 'Distribution place'), ('djo', 'DJ'), ('dnr', 'Donor'), ('drm', 'Draftsman'), ('dbd', 'Dubbing director'), ('dub', 'Dubious author'), ('edt', 'Editor'), ('edc', 'Editor of compilation'), ('edm', 'Editor of moving image work'), ('edd', 'Editorial director'), ('elg', 'Electrician'), ('elt', 'Electrotyper'), ('enj', 'Enacting jurisdiction'), ('eng', 'Engineer'), ('egr', 'Engraver'), ('etr', 'Etcher'), ('evp', 'Event place'), ('exp', 'Expert'), ('fac', 'Facsimilist'), ('fld', 'Field director'), ('fmd', 'Film director'), ('fds', 'Film distributor'), ('flm', 'Film editor'), ('fmp', 'Film producer'), ('fmk', 'Filmmaker'), ('fpy', 'First party'), ('frg', 'Forger'), ('fmo', 'Former owner'), ('fon', 'Founder'), ('fnd', 'Funder'), ('gis', 'Geographic information specialist'), ('hnr', 'Honoree'), ('hst', 'Host'), ('his', 'Host institution'), ('ilu', 'Illuminator'), ('ill', 'Illustrator'), ('ins', 'Inscriber'), ('itr', 'Instrumentalist'), ('ive', 'Interviewee'), ('ivr', 'Interviewer'), ('inv', 'Inventor'), ('isb', 'Issuing body'), ('jud', 'Judge'), ('jug', 'Jurisdiction governed'), ('lbr', 'Laboratory'), ('ldr', 'Laboratory director'), ('lsa', 'Landscape architect'), ('led', 'Lead'), ('len', 'Lender'), ('lil', 'Libelant'), ('lit', 'Libelant-appellant'), ('lie', 'Libelant-appellee'), ('lel', 'Libelee'), ('let', 'Libelee-appellant'), ('lee', 'Libelee-appellee'), ('lbt', 'Librettist'), ('lse', 'Licensee'), ('lso', 'Licensor'), ('lgd', 'Lighting designer'), ('ltg', 'Lithographer'), ('lyr', 'Lyricist'), ('mka', 'Makeup artist'), ('mfp', 'Manufacture place'), ('mfr', 'Manufacturer'), ('mrb', 'Marbler'), ('mrk', 'Markup editor'), ('med', 'Medium'), ('mdc', 'Metadata contact'), ('mte', 'Metal-engraver'), ('mtk', 'Minute taker'), ('mxe', 'Mixing engineer'), ('mod', 'Moderator'), ('mon', 'Monitor'), ('mcp', 'Music copyist'), ('mup', 'Music programmer'), ('msd', 'Musical director'), ('mus', 'Musician'), ('nrt', 'Narrator'), ('nan', 'News anchor'), ('onp', 'Onscreen participant'), ('osp', 'Onscreen presenter'), ('opn', 'Opponent'), ('orm', 'Organizer'), ('org', 'Originator'), ('oth', 'Other'), ('own', 'Owner'), ('pan', 'Panelist'), ('ppm', 'Papermaker'), ('pta', 'Patent applicant'), ('pth', 'Patent holder'), ('pat', 'Patron'), ('prf', 'Performer'), ('pma', 'Permitting agency'), ('pht', 'Photographer'), ('pad', 'Place of address'), ('ptf', 'Plaintiff'), ('ptt', 'Plaintiff-appellant'), ('pte', 'Plaintiff-appellee'), ('plt', 'Platemaker'), ('pra', 'Praeses'), ('pre', 'Presenter'), ('prt', 'Printer'), ('pop', 'Printer of plates'), ('prm', 'Printmaker'), ('prc', 'Process contact'), ('pro', 'Producer'), ('prn', 'Production company'), ('prs', 'Production designer'), ('pmn', 'Production manager'), ('prd', 'Production personnel'), ('prp', 'Production place'), ('prg', 'Programmer'), ('pdr', 'Project director'), ('pfr', 'Proofreader'), ('prv', 'Provider'), ('pup', 'Publication place'), ('pbl', 'Publisher'), ('pbd', 'Publishing director'), ('ppt', 'Puppeteer'), ('rdd', 'Radio director'), ('rpc', 'Radio producer'), ('rap', 'Rapporteur'), ('rce', 'Recording engineer'), ('rcd', 'Recordist'), ('red', 'Redaktor'), ('rxa', 'Remix artist'), ('ren', 'Renderer'), ('rpt', 'Reporter'), ('rps', 'Repository'), ('rth', 'Research team head'), ('rtm', 'Research team member'), ('res', 'Researcher'), ('rsp', 'Respondent'), ('rst', 'Respondent-appellant'), ('rse', 'Respondent-appellee'), ('rpy', 'Responsible party'), ('rsg', 'Restager'), ('rsr', 'Restorationist'), ('rev', 'Reviewer'), ('rbr', 'Rubricator'), ('sce', 'Scenarist'), ('sad', 'Scientific advisor'), ('aus', 'Screenwriter'), ('scr', 'Scribe'), ('scl', 'Sculptor'), ('spy', 'Second party'), ('sec', 'Secretary'), ('sll', 'Seller'), ('std', 'Set designer'), ('stg', 'Setting'), ('sgn', 'Signer'), ('sng', 'Singer'), ('swd', 'Software developer'), ('sds', 'Sound designer'), ('sde', 'Sound engineer'), ('spk', 'Speaker'), ('sfx', 'Special effects provider'), ('spn', 'Sponsor'), ('sgd', 'Stage director'), ('stm', 'Stage manager'), ('stn', 'Standards body'), ('str', 'Stereotyper'), ('stl', 'Storyteller'), ('sht', 'Supporting host'), ('srv', 'Surveyor'), ('tch', 'Teacher'), ('tcd', 'Technical director'), ('tld', 'Television director'), ('tlg', 'Television guest'), ('tlh', 'Television host'), ('tlp', 'Television producer'), ('tau', 'Television writer'), ('ths', 'Thesis advisor'), ('trc', 'Transcriber'), ('trl', 'Translator'), ('tyd', 'Type designer'), ('tyg', 'Typographer'), ('uvp', 'University place'), ('vdg', 'Videographer'), ('vfx', 'Visual effects provider'), ('vac', 'Voice actor'), ('wit', 'Witness'), ('wde', 'Wood engraver'), ('wdc', 'Woodcutter'), ('wam', 'Writer of accompanying material'), ('wac', 'Writer of added commentary'), ('wal', 'Writer of added lyrics'), ('wat', 'Writer of added text'), ('win', 'Writer of introduction'), ('wpr', 'Writer of preface'), ('wst', 'Writer of supplementary textual content')]), default=list, size=None, verbose_name='responsibilities')), + ('created', models.DateTimeField(auto_now_add=True)), + ('modified', models.DateTimeField(auto_now=True)), + ('content_type', models.ForeignKey(limit_choices_to={'model__in': ['Resource', 'Edition', 'Occurrence']}, on_delete=django.db.models.deletion.CASCADE, related_name='organization_responsibility_statements', to='contenttypes.contenttype')), + ('organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='responsibility_statements', to='intertidal.organization')), + ], + options={ + 'db_table': 'intertidal_organization_responsibility_statement', + 'indexes': [models.Index(fields=['content_type', 'object_id'], name='intertidal__content_4be160_idx')], + }, + ), + migrations.CreateModel( + name='PersonResponsibilityStatement', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('object_id', models.BigIntegerField()), + ('marc_relators', intertidal.models.ArrayField(base_field=models.CharField(choices=[('abr', 'Abridger'), ('act', 'Actor'), ('adp', 'Adapter'), ('rcp', 'Addressee'), ('anl', 'Analyst'), ('anm', 'Animator'), ('ann', 'Annotator'), ('anc', 'Announcer'), ('apl', 'Appellant'), ('ape', 'Appellee'), ('app', 'Applicant'), ('arc', 'Architect'), ('arr', 'Arranger'), ('acp', 'Art copyist'), ('adi', 'Art director'), ('art', 'Artist'), ('ard', 'Artistic director'), ('asg', 'Assignee'), ('asn', 'Associated name'), ('att', 'Attributed name'), ('auc', 'Auctioneer'), ('aue', 'Audio engineer'), ('aup', 'Audio producer'), ('aut', 'Author'), ('aqt', 'Author in quotations or text abstracts'), ('aft', 'Author of afterword, colophon, etc.'), ('aud', 'Author of dialog'), ('aui', 'Author of introduction, etc.'), ('ato', 'Autographer'), ('ant', 'Bibliographic antecedent'), ('bnd', 'Binder'), ('bdd', 'Binding designer'), ('blw', 'Blurb writer'), ('bka', 'Book artist'), ('bkd', 'Book designer'), ('bkp', 'Book producer'), ('bjd', 'Bookjacket designer'), ('bpd', 'Bookplate designer'), ('bsl', 'Bookseller'), ('brl', 'Braille embosser'), ('brd', 'Broadcaster'), ('cll', 'Calligrapher'), ('cop', 'Camera operator'), ('ctg', 'Cartographer'), ('cas', 'Caster'), ('cad', 'Casting director'), ('cns', 'Censor'), ('chr', 'Choreographer'), ('cng', 'Cinematographer'), ('cli', 'Client'), ('cor', 'Collection registrar'), ('col', 'Collector'), ('clt', 'Collotyper'), ('clr', 'Colorist'), ('cmm', 'Commentator'), ('cwt', 'Commentator for written text'), ('com', 'Compiler'), ('cpl', 'Complainant'), ('cpt', 'Complainant-appellant'), ('cpe', 'Complainant-appellee'), ('cmp', 'Composer'), ('cmt', 'Compositor'), ('ccp', 'Conceptor'), ('cnd', 'Conductor'), ('con', 'Conservator'), ('csl', 'Consultant'), ('csp', 'Consultant to a project'), ('cos', 'Contestant'), ('cot', 'Contestant-appellant'), ('coe', 'Contestant-appellee'), ('cts', 'Contestee'), ('ctt', 'Contestee-appellant'), ('cte', 'Contestee-appellee'), ('ctr', 'Contractor'), ('ctb', 'Contributor'), ('cpc', 'Copyright claimant'), ('cph', 'Copyright holder'), ('crr', 'Corrector'), ('crp', 'Correspondent'), ('cst', 'Costume designer'), ('cou', 'Court governed'), ('crt', 'Court reporter'), ('cov', 'Cover designer'), ('cre', 'Creator'), ('cur', 'Curator'), ('dnc', 'Dancer'), ('dtc', 'Data contributor'), ('dtm', 'Data manager'), ('dte', 'Dedicatee'), ('dto', 'Dedicator'), ('dfd', 'Defendant'), ('dft', 'Defendant-appellant'), ('dfe', 'Defendant-appellee'), ('dgc', 'Degree committee member'), ('dgg', 'Degree granting institution'), ('dgs', 'Degree supervisor'), ('dln', 'Delineator'), ('dpc', 'Depicted'), ('dpt', 'Depositor'), ('dsr', 'Designer'), ('drt', 'Director'), ('dis', 'Dissertant'), ('dbp', 'Distribution place'), ('djo', 'DJ'), ('dnr', 'Donor'), ('drm', 'Draftsman'), ('dbd', 'Dubbing director'), ('dub', 'Dubious author'), ('edt', 'Editor'), ('edc', 'Editor of compilation'), ('edm', 'Editor of moving image work'), ('edd', 'Editorial director'), ('elg', 'Electrician'), ('elt', 'Electrotyper'), ('enj', 'Enacting jurisdiction'), ('eng', 'Engineer'), ('egr', 'Engraver'), ('etr', 'Etcher'), ('evp', 'Event place'), ('exp', 'Expert'), ('fac', 'Facsimilist'), ('fld', 'Field director'), ('fmd', 'Film director'), ('fds', 'Film distributor'), ('flm', 'Film editor'), ('fmp', 'Film producer'), ('fmk', 'Filmmaker'), ('fpy', 'First party'), ('frg', 'Forger'), ('fmo', 'Former owner'), ('fon', 'Founder'), ('fnd', 'Funder'), ('gis', 'Geographic information specialist'), ('hnr', 'Honoree'), ('hst', 'Host'), ('his', 'Host institution'), ('ilu', 'Illuminator'), ('ill', 'Illustrator'), ('ins', 'Inscriber'), ('itr', 'Instrumentalist'), ('ive', 'Interviewee'), ('ivr', 'Interviewer'), ('inv', 'Inventor'), ('isb', 'Issuing body'), ('jud', 'Judge'), ('jug', 'Jurisdiction governed'), ('lbr', 'Laboratory'), ('ldr', 'Laboratory director'), ('lsa', 'Landscape architect'), ('led', 'Lead'), ('len', 'Lender'), ('lil', 'Libelant'), ('lit', 'Libelant-appellant'), ('lie', 'Libelant-appellee'), ('lel', 'Libelee'), ('let', 'Libelee-appellant'), ('lee', 'Libelee-appellee'), ('lbt', 'Librettist'), ('lse', 'Licensee'), ('lso', 'Licensor'), ('lgd', 'Lighting designer'), ('ltg', 'Lithographer'), ('lyr', 'Lyricist'), ('mka', 'Makeup artist'), ('mfp', 'Manufacture place'), ('mfr', 'Manufacturer'), ('mrb', 'Marbler'), ('mrk', 'Markup editor'), ('med', 'Medium'), ('mdc', 'Metadata contact'), ('mte', 'Metal-engraver'), ('mtk', 'Minute taker'), ('mxe', 'Mixing engineer'), ('mod', 'Moderator'), ('mon', 'Monitor'), ('mcp', 'Music copyist'), ('mup', 'Music programmer'), ('msd', 'Musical director'), ('mus', 'Musician'), ('nrt', 'Narrator'), ('nan', 'News anchor'), ('onp', 'Onscreen participant'), ('osp', 'Onscreen presenter'), ('opn', 'Opponent'), ('orm', 'Organizer'), ('org', 'Originator'), ('oth', 'Other'), ('own', 'Owner'), ('pan', 'Panelist'), ('ppm', 'Papermaker'), ('pta', 'Patent applicant'), ('pth', 'Patent holder'), ('pat', 'Patron'), ('prf', 'Performer'), ('pma', 'Permitting agency'), ('pht', 'Photographer'), ('pad', 'Place of address'), ('ptf', 'Plaintiff'), ('ptt', 'Plaintiff-appellant'), ('pte', 'Plaintiff-appellee'), ('plt', 'Platemaker'), ('pra', 'Praeses'), ('pre', 'Presenter'), ('prt', 'Printer'), ('pop', 'Printer of plates'), ('prm', 'Printmaker'), ('prc', 'Process contact'), ('pro', 'Producer'), ('prn', 'Production company'), ('prs', 'Production designer'), ('pmn', 'Production manager'), ('prd', 'Production personnel'), ('prp', 'Production place'), ('prg', 'Programmer'), ('pdr', 'Project director'), ('pfr', 'Proofreader'), ('prv', 'Provider'), ('pup', 'Publication place'), ('pbl', 'Publisher'), ('pbd', 'Publishing director'), ('ppt', 'Puppeteer'), ('rdd', 'Radio director'), ('rpc', 'Radio producer'), ('rap', 'Rapporteur'), ('rce', 'Recording engineer'), ('rcd', 'Recordist'), ('red', 'Redaktor'), ('rxa', 'Remix artist'), ('ren', 'Renderer'), ('rpt', 'Reporter'), ('rps', 'Repository'), ('rth', 'Research team head'), ('rtm', 'Research team member'), ('res', 'Researcher'), ('rsp', 'Respondent'), ('rst', 'Respondent-appellant'), ('rse', 'Respondent-appellee'), ('rpy', 'Responsible party'), ('rsg', 'Restager'), ('rsr', 'Restorationist'), ('rev', 'Reviewer'), ('rbr', 'Rubricator'), ('sce', 'Scenarist'), ('sad', 'Scientific advisor'), ('aus', 'Screenwriter'), ('scr', 'Scribe'), ('scl', 'Sculptor'), ('spy', 'Second party'), ('sec', 'Secretary'), ('sll', 'Seller'), ('std', 'Set designer'), ('stg', 'Setting'), ('sgn', 'Signer'), ('sng', 'Singer'), ('swd', 'Software developer'), ('sds', 'Sound designer'), ('sde', 'Sound engineer'), ('spk', 'Speaker'), ('sfx', 'Special effects provider'), ('spn', 'Sponsor'), ('sgd', 'Stage director'), ('stm', 'Stage manager'), ('stn', 'Standards body'), ('str', 'Stereotyper'), ('stl', 'Storyteller'), ('sht', 'Supporting host'), ('srv', 'Surveyor'), ('tch', 'Teacher'), ('tcd', 'Technical director'), ('tld', 'Television director'), ('tlg', 'Television guest'), ('tlh', 'Television host'), ('tlp', 'Television producer'), ('tau', 'Television writer'), ('ths', 'Thesis advisor'), ('trc', 'Transcriber'), ('trl', 'Translator'), ('tyd', 'Type designer'), ('tyg', 'Typographer'), ('uvp', 'University place'), ('vdg', 'Videographer'), ('vfx', 'Visual effects provider'), ('vac', 'Voice actor'), ('wit', 'Witness'), ('wde', 'Wood engraver'), ('wdc', 'Woodcutter'), ('wam', 'Writer of accompanying material'), ('wac', 'Writer of added commentary'), ('wal', 'Writer of added lyrics'), ('wat', 'Writer of added text'), ('win', 'Writer of introduction'), ('wpr', 'Writer of preface'), ('wst', 'Writer of supplementary textual content')]), default=list, size=None, verbose_name='responsibilities')), + ('created', models.DateTimeField(auto_now_add=True)), + ('modified', models.DateTimeField(auto_now=True)), + ('content_type', models.ForeignKey(limit_choices_to={'model__in': ['Resource', 'Edition', 'Occurrence']}, on_delete=django.db.models.deletion.CASCADE, related_name='person_responsibility_statements', to='contenttypes.contenttype')), + ('person', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='responsibility_statements', to='intertidal.person')), + ], + options={ + 'db_table': 'intertidal_person_responsibility_statement', + 'indexes': [models.Index(fields=['content_type', 'object_id'], name='intertidal__content_2c79cb_idx')], + }, + ), + ] diff --git a/intertidal/migrations/__init__.py b/intertidal/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/intertidal/models.py b/intertidal/models.py new file mode 100644 index 0000000..74fc720 --- /dev/null +++ b/intertidal/models.py @@ -0,0 +1,284 @@ +from django.db import models +from django.contrib.postgres.fields import ArrayField as DjangoArrayField +from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation +from django.contrib.contenttypes.models import ContentType +from django.contrib.postgres.forms import SimpleArrayField, SplitArrayField +from partial_date import PartialDateField +from django.conf.global_settings import LANGUAGES +from intertidal.marc_relators import MarcRelator +from intertidal.cls_types import ClsTypes +from django.utils.safestring import mark_safe + + +class SimpleArrayFieldSelect2Fix(SimpleArrayField): + def prepare_value(self, value): + if isinstance(value, list): + return '|'.join( + str(self.base_field.prepare_value(v)) for v in value + ) + return value + + +class ArrayField(DjangoArrayField): + def formfield(self, **kwargs): + defaults = { + 'form_class': SimpleArrayFieldSelect2Fix, + } + defaults.update(kwargs) + return super().formfield(**defaults) + + +class Person(models.Model): + # fields + name = models.CharField(db_index=True) + alternative_names = ArrayField(models.CharField(), default=list, blank=True) + links = ArrayField(models.CharField(), default=list, blank=True) + emails = ArrayField(models.CharField(), default=list, blank=True) + + # relationships + # one-to-many responsibility_statements via PersonResponsibilityStatement Model + + # write tracking fields + created = models.DateTimeField(auto_now_add=True) + modified = models.DateTimeField(auto_now=True) + + class Meta: + verbose_name_plural = 'people' + + def get_alternative_names_short(self): + alternative_names = ', '.join(self.alternative_names) if len(self.alternative_names) else None + return alternative_names[:75] + '...' if alternative_names and len(alternative_names) >= 75 else alternative_names + + def __str__(self): + return f"{self.name} ({self.get_alternative_names_short()})" if self.get_alternative_names_short() else self.name + +class Organization(models.Model): + # fields + name = models.CharField(db_index=True) + alternative_names = ArrayField(models.CharField(), default=list, blank=True) + address = models.CharField(blank=True) + links = ArrayField(models.CharField(), default=list, blank=True) + emails = ArrayField(models.CharField(), default=list, blank=True) + + # relationships + # one-to-many responsibility_statements via OrganizationResponsibilityStatement Model + + # write tracking fields + created = models.DateTimeField(auto_now_add=True) + modified = models.DateTimeField(auto_now=True) + + def get_alternative_names_short(self): + alternative_names = ', '.join(self.alternative_names) if len(self.alternative_names) else None + return alternative_names[:75] + '...' if alternative_names and len(alternative_names) >= 75 else alternative_names + + def __str__(self): + return f"{self.name} ({self.get_alternative_names_short()})" if self.get_alternative_names_short() else self.name + +class PersonResponsibilityStatement(models.Model): + content_type = models.ForeignKey( + ContentType, + limit_choices_to={'model__in': ['Resource', 'Edition', 'Occurrence']}, + on_delete=models.CASCADE, + related_name='person_responsibility_statements', + ) + object_id = models.BigIntegerField() + content_object = GenericForeignKey() + person = models.ForeignKey( + Person, + related_name='responsibility_statements', + on_delete=models.CASCADE + ) + marc_relators = ArrayField(models.CharField(choices=MarcRelator.choices), default=list, verbose_name='responsibilities') + + # one-to-many resources via Resource Model + # one-to-many editions via Edition Model + # one-to-many occurrences via Occurrence Model + + # write tracking fields + created = models.DateTimeField(auto_now_add=True) + modified = models.DateTimeField(auto_now=True) + + class Meta: + db_table = 'intertidal_person_responsibility_statement' + indexes = [ + models.Index(fields=['content_type', 'object_id']), + ] + +class OrganizationResponsibilityStatement(models.Model): + content_type = models.ForeignKey( + ContentType, + limit_choices_to={'model__in': ['Resource', 'Edition', 'Occurrence']}, + on_delete=models.CASCADE, + related_name='organization_responsibility_statements', + ) + object_id = models.BigIntegerField() + content_object = GenericForeignKey() + organization = models.ForeignKey( + Organization, + related_name='responsibility_statements', + on_delete=models.CASCADE + ) + marc_relators = ArrayField(models.CharField(choices=MarcRelator.choices), default=list, verbose_name='responsibilities') + + # one-to-many resources via Resource Model + # one-to-many editions via Edition Model + # one-to-many occurrences via Occurrence Model + + # write tracking fields + created = models.DateTimeField(auto_now_add=True) + modified = models.DateTimeField(auto_now=True) + + class Meta: + db_table = 'intertidal_organization_responsibility_statement' + indexes = [ + models.Index(fields=['content_type', 'object_id']), + ] + +class Resource(models.Model): + class LocaleTypes(models.TextChoices): + VANCOUVER = "VANCOUVER", "Vancouver" + SINGAPORE = "SINGAPORE", "Singapore" + HONG_KONG = "HONG_KONG", "Hong Kong" + + class DisplayTypes(models.TextChoices): + LITERARY_WORK = "LITERARY_WORK", "Literary Work" + ART_PERFORMANCE = "ART_PERFORMANCE", "Art/Performance" + STATE_ARCHITECTURE_MISC = "STATE_ARCHITECTURE_MISC", "State/Architecture/Misc." + NEWS_DOCUMENTARY = "NEWS_DOCUMENTARY", "News/Documentary" + ACADEMIC_RESEARCH = "ACADEMIC_RESEARCH", "Academic Research" + SOCIAL_MEDIA = "SOCIAL_MEDIA", "Social Media" + + # fields + locale = models.CharField( + choices=LocaleTypes.choices, + default=LocaleTypes.VANCOUVER, + db_index=True, + ) + display_category = models.CharField( + choices=DisplayTypes.choices, + default=DisplayTypes.LITERARY_WORK, + db_index=True, + ) + name = models.CharField(db_index=True, verbose_name='Name/Title') + alternative_names = ArrayField(models.CharField(), default=list, blank=True, verbose_name='Alternative Names/Titles') + forms = ArrayField( + models.CharField(choices=ClsTypes.choices), default=list, blank=True, verbose_name='Physical/Digital Forms', + help_text=mark_safe('See the list of Citation Style Language Types for descriptions of each form') + ) + genres = ArrayField(models.CharField(), default=list, blank=True) + keywords = ArrayField(models.CharField(), default=list, blank=True) + date = PartialDateField(null=True, blank=True, db_index=True) + date_end = PartialDateField(null=True, blank=True) + date_current = models.BooleanField(default=False) + description = models.TextField(blank=True) + notes = models.TextField(blank=True, help_text='Notes are private (not publicly visible)') + links = ArrayField(models.CharField(), default=list, blank=True) + + # relationships + person_responsibility_statements = GenericRelation( + PersonResponsibilityStatement, + related_query_name='resource', + related_name='resources', + ) + organization_responsibility_statements = GenericRelation( + OrganizationResponsibilityStatement, + related_query_name='resource', + related_name='resources', + ) + + # write tracking fields + created = models.DateTimeField(auto_now_add=True) + modified = models.DateTimeField(auto_now=True) + + def date_str(self): + if not self.date: + return None + elif self.date_current: + return f"{self.date} - now" + elif self.date_end: + return f"{self.date} - {self.date_end}" + return self.date + + def get_alternative_names_short(self): + alternative_names = ', '.join(self.alternative_names) if len(self.alternative_names) else None + return alternative_names[:75] + '...' if alternative_names and len(alternative_names) >= 75 else alternative_names + + def __str__(self): + return f"{self.name} ({self.get_alternative_names_short()})" if self.get_alternative_names_short() else self.name + +class Edition(models.Model): + # fields + name = models.CharField(verbose_name='Name/Title', blank=True) + translation = models.BooleanField(default=False) + translation_language = models.CharField( + choices=LANGUAGES, + default='en', + blank=True, + verbose_name='language', + ) + date = PartialDateField(null=True, blank=True, db_index=True) + + # relationships + resource = models.ForeignKey( + Resource, + related_name='editions', + on_delete=models.CASCADE + ) + person_responsibility_statements = GenericRelation( + PersonResponsibilityStatement, + related_query_name='edition', + related_name='editions', + ) + organization_responsibility_statements = GenericRelation( + OrganizationResponsibilityStatement, + related_query_name='edition', + related_name='editions', + ) + + # write tracking fields + created = models.DateTimeField(auto_now_add=True) + modified = models.DateTimeField(auto_now=True) + + def __str__(self): + return f"{self.name} ({self.date})" if self.date else self.name + +class Occurrence(models.Model): + # fields + location = models.CharField(blank=True, verbose_name='Location/Venue') + address = models.CharField(blank=True) + date = PartialDateField(null=True, blank=True, db_index=True) + date_end = PartialDateField(null=True, blank=True) + date_current = models.BooleanField(default=False) + + # relationships + resource = models.ForeignKey( + Resource, + related_name='occurrences', + on_delete=models.CASCADE + ) + person_responsibility_statements = GenericRelation( + PersonResponsibilityStatement, + related_query_name='occurrence', + related_name='occurrences', + ) + organization_responsibility_statements = GenericRelation( + OrganizationResponsibilityStatement, + related_query_name='occurrence', + related_name='occurrences', + ) + + # write tracking fields + created = models.DateTimeField(auto_now_add=True) + modified = models.DateTimeField(auto_now=True) + + def date_str(self): + if not self.date: + return None + elif self.date_current: + return f"{self.date} - now" + elif self.date_end: + return f"{self.date} - {self.date_end}" + return self.date + + def __str__(self): + return f"{self.location} ({self.date_str()})" if self.date_str() else self.location \ No newline at end of file diff --git a/intertidal/static/css/base.css b/intertidal/static/css/base.css new file mode 100644 index 0000000..e69de29 diff --git a/intertidal/static/django_select2/django_select2.js b/intertidal/static/django_select2/django_select2.js new file mode 100644 index 0000000..0e8f33f --- /dev/null +++ b/intertidal/static/django_select2/django_select2.js @@ -0,0 +1,101 @@ +// TODO: TEMP FIX (see: https://github.com/codingjoe/django-select2/pull/300) + +/* global define, jQuery */ +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(['jquery'], factory) + } else if (typeof module === 'object' && module.exports) { + module.exports = factory(require('jquery')) + } else { + // Browser globals + factory(jQuery || window.django.jQuery) + } + }(function ($) { + 'use strict' + var init = function ($element, options) { + $element.select2(options) + } + + var initHeavy = function ($element, options) { + var settings = $.extend({ + ajax: { + data: function (params) { + var result = { + term: params.term, + page: params.page, + field_id: $element.data('field_id') + } + + var dependentFields = $element.data('select2-dependent-fields') + if (dependentFields) { + dependentFields = dependentFields.trim().split(/\s+/) + $.each(dependentFields, function (i, dependentField) { + result[dependentField] = $('[name=' + dependentField + ']', $element.closest('form')).val() + }) + } + + return result + }, + processResults: function (data, page) { + return { + results: data.results, + pagination: { + more: data.more + } + } + } + } + }, options) + + $element.select2(settings) + } + + $.fn.djangoSelect2 = function (options) { + var settings = $.extend({}, options) + $.each(this, function (i, element) { + var $element = $(element) + if ($element.hasClass('django-select2-heavy')) { + initHeavy($element, settings) + } else { + init($element, settings) + } + $element.on('select2:select', function (e) { + var name = $(e.currentTarget).attr('name') + $('[data-select2-dependent-fields~=' + name + ']').each(function () { + $(this).val('').trigger('change') + }) + }) + }) + return this + } + + $(function () { + $('.django-select2').djangoSelect2() + + // This part fixes new inlines not having the correct select2 widgets + function handleFormsetAdded (row, formsetName) { + // Converting to the "normal jQuery" + const jqRow = $(row) + + // Because select2 was already instantiated on the empty form, we need to remove it, destroy the instance, + // and re-instantiate it. + jqRow.find('.django-select2').parent().find('.select2-container').remove() + jqRow.find('.django-select2').djangoSelect2('destroy') + jqRow.find('.django-select2').djangoSelect2() + } + + // See: https://docs.djangoproject.com/en/dev/ref/contrib/admin/javascript/#supporting-versions-of-django-older-than-4-1 + $(document).on('formset:added', function (event, $row, formsetName) { + if (event.detail && event.detail.formsetName) { + // Django >= 4.1 + handleFormsetAdded(event.target, event.detail.formsetName) + } else { + // Django < 4.1, use $row and formsetName + handleFormsetAdded($row.get(0), formsetName) + } + }) + // End of fix + }) + + return $.fn.djangoSelect2 + })) diff --git a/intertidal/static/images/favicon.ico b/intertidal/static/images/favicon.ico new file mode 100644 index 0000000..cffa478 Binary files /dev/null and b/intertidal/static/images/favicon.ico differ diff --git a/intertidal/templates/base.html b/intertidal/templates/base.html new file mode 100644 index 0000000..1db5ece --- /dev/null +++ b/intertidal/templates/base.html @@ -0,0 +1,23 @@ +{% load static %} + + +
+ + + + +