├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── README.md ├── gramps_webapp ├── __init__.py ├── __main__.py ├── api.py ├── auth.py ├── db.py ├── gramps.py ├── image.py ├── js │ ├── 0e10338e.js │ ├── 19fdb0d6.js │ ├── 43e752ee.js │ ├── 4582424e.js │ ├── 4f794f05.js │ ├── 5522257c.js │ ├── 580fa394.js │ ├── 5e5495ec.js │ ├── 7d06e270.js │ ├── 7de3acdd.js │ ├── 80126bfe.js │ ├── 85f25d14.js │ ├── 911a2ccc.js │ ├── a088e339.js │ ├── adb2bcf5.js │ ├── b31c52e2.js │ ├── b64e72b8.js │ ├── c90d6c40.js │ ├── cb2c5929.js │ ├── cba53e11.js │ ├── d880604a.js │ ├── e6ec0820.js │ ├── e9e76f9e.js │ ├── eb930581.js │ ├── fe5344c8.js │ ├── fec21ef7.js │ ├── images │ │ ├── favicon.ico │ │ ├── icon-144x144.png │ │ ├── icon-192x192.png │ │ ├── icon-48x48.png │ │ ├── icon-512x512.png │ │ ├── icon-72x72.png │ │ ├── icon-96x96.png │ │ ├── layers-2x.png │ │ ├── layers.png │ │ ├── logo.svg │ │ ├── manifest │ │ │ ├── icon-144x144.png │ │ │ ├── icon-192x192.png │ │ │ ├── icon-48x48.png │ │ │ ├── icon-512x512.png │ │ │ ├── icon-72x72.png │ │ │ └── icon-96x96.png │ │ ├── marker-icon-2x.png │ │ ├── marker-icon.png │ │ └── marker-shadow.png │ ├── index.html │ ├── leaflet.css │ ├── login.html │ ├── sw.js │ ├── sw.js.map │ ├── workbox-a1d34bd3.js │ └── workbox-a1d34bd3.js.map ├── media.py ├── s3.py ├── sql_guid.py ├── test_auth.py └── wsgi.py ├── screenshot.png └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | jwt_secret_key 2 | appcache 3 | .directory 4 | 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | 63 | # Flask stuff: 64 | instance/ 65 | .webassets-cache 66 | 67 | # Scrapy stuff: 68 | .scrapy 69 | 70 | # Sphinx documentation 71 | docs/_build/ 72 | 73 | # PyBuilder 74 | target/ 75 | 76 | # Jupyter Notebook 77 | .ipynb_checkpoints 78 | 79 | # pyenv 80 | .python-version 81 | 82 | # celery beat schedule file 83 | celerybeat-schedule 84 | 85 | # SageMath parsed files 86 | *.sage.py 87 | 88 | # Environments 89 | .env 90 | .venv 91 | env/ 92 | venv/ 93 | ENV/ 94 | env.bak/ 95 | venv.bak/ 96 | 97 | # Spyder project settings 98 | .spyderproject 99 | .spyproject 100 | 101 | # Rope project settings 102 | .ropeproject 103 | 104 | # mkdocs documentation 105 | /site 106 | 107 | # mypy 108 | .mypy_cache/ 109 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.5" 4 | - "3.6" 5 | - "3.7" 6 | - "3.8" 7 | matrix: 8 | allow_failures: 9 | - python: "3.5" 10 | - python: "3.8" 11 | 12 | notifications: 13 | email: false 14 | 15 | install: 16 | - pip install -e . 17 | - pip install nose coveralls 18 | 19 | script: nosetests --with-coverage --cover-package=gramps_webapp 20 | 21 | after_success: coveralls 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include LICENSE 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gramps Web App 2 | 3 | ⚠ **Note: this project has been superseded by the [Gramps Web API](https://github.com/gramps-project/gramps-webapi) in combination with the [Gramps.js](https://github.com/DavidMStraub/Gramps.js) frontend.** 4 | 5 | This package provides a web frontend to the genealogical database of 6 | the [Gramps](https://gramps-project.org) software. It allows to share 7 | your genalogical research with family members or provides an alternative 8 | way of browsing your records on your local computer. 9 | 10 | ![](screenshot.png) 11 | 12 | ## Demo 13 | 14 | There is a [demo instance](https://mysterious-cove-02458.herokuapp.com) using Gramps's example family tree database. Use `test` for username and password. 15 | 16 | (NB: initial startup of the demo can take up to a minute since it is hosted on a free Heroku dyno that sleeps after 30 mins of inactivity.) 17 | 18 | ## Disclaimers 19 | 20 | **:warning: This is experimental software. Back up your data before using this on your own database! :warning:** 21 | 22 | The project is still in an early stage. Please use the issue system to report problems or suggest enhancements. 23 | 24 | **This package is not an official part of the Gramps project.** 25 | 26 | ## Technologies used 27 | 28 | - REST API to the Gramps database based on [Flask](https://palletsprojects.com/p/flask/) and [Flask-RESTful](https://flask-restful.readthedocs.io/) and directly using the Gramps Python package 29 | - Authentication system using JSON web tokens (using [Flask-JWT-Extended](https://flask-jwt-extended.readthedocs.io/)) 30 | - Progressive web app frontend based on [PWA Starter Kit](https://pwa-starter-kit.polymer-project.org/) and [Open WC](https://open-wc.org/) 31 | 32 | ## Features 33 | 34 | - Sortable and filtrable people, family, event, source, and place list views 35 | - Map view (based on [Leaflet](https://leafletjs.com/)) 36 | - Ancestor tree view 37 | - Galleries with full-size previews and linked person tags, embedded preview for PDFs in Chrome and Firefox 38 | - Most of the family tree data are cached in the browser, making the app fast after the initial loading 39 | - Fully internationalized UI (directly using Gramps's translation strings) 40 | - Optionally host media objects on Amazon S3 cloud storage ([docs](https://github.com/DavidMStraub/gramps-webapp/wiki/Storing-media-objects-on-S3)) 41 | - Optionally manage users in a separate SQL database ([docs](https://github.com/DavidMStraub/gramps-webapp/wiki/Managing-users)) 42 | 43 | ## Installation 44 | 45 | At present, the simplest method to install the latest version of the package directly from the repository is 46 | 47 | ``` 48 | python3 -m pip install --user git+https://github.com/DavidMStraub/gramps-webapp.git --upgrade 49 | ``` 50 | 51 | ## Running locally 52 | 53 | You can try out the web app locally with an existing Gramps database. 54 | 55 | **:warning: This is experimental software. Back up your data before trying this! :warning:** 56 | 57 | It will only work with SQLite databases (not with BSDDB). 58 | After installation, run 59 | 60 | ``` 61 | python3 -m gramps_webapp app -O 'My family tree' --no-auth run 62 | ``` 63 | 64 | The `--no-auth` option disables the user system (the login form will still be shown but can be left empty). You can find the names of the existing databases and their backends with `gramps -L`. 65 | 66 | 67 | ## Deploying to the web 68 | 69 | See the [Wiki](https://github.com/DavidMStraub/gramps-webapp/wiki). 70 | 71 | ## Configuration 72 | 73 | This is a list of environment variables that affect the web app. 74 | 75 | | Variable | Description| 76 | |---|---| 77 | | `TREE` | Family tree to open (can also be set by the `-O` tag on the command line, see above | 78 | | `JWT_SECRET_KEY` | Secret key for the tokens. If not set, a secure key will be generated, stored in the app's root directory, and reused for the next startup. Note that changing the token will require users to log in again. | 79 | | `GRAMPS_EXCLUDE_PRIVATE` | Exclude private records from being shown. Defaults to false. | 80 | | `GRAMPS_EXCLUDE_LIVING` | Do only show names, but no details, of living people. Note that the media objects and events will still be accessible (but not linked to the person). Defaults to false. | 81 | | `GRAMPS_AUTH_PROVIDER` | Authentication method to use. Possible values: `password` or `sql` (default). | 82 | | `PASSWORD` | User password in case of using password authentication. Empty by default (!) | 83 | | `GRAMPS_USER_DB_URI` | SQLAlchemy compatible URI for the user database when using SQL authentication. | 84 | | `GRAMPS_S3_BUCKET_NAME` | S3 bucket name when using AWS cloud storage for media files. See [the documentation](https://github.com/DavidMStraub/gramps-webapp/wiki/Storing-media-objects-on-S3) for details. 85 | 86 | 87 | 88 | 89 | ## Current limitations 90 | 91 | - ~~No user management~~ 92 | - ~~Tokens have infinite lifetime (refresh tokens would be more secure)~~ 93 | - ~~No display of sources, repositories, and notes~~ 94 | - ~~Private records not respected~~ 95 | - Read-only (no family tree editing) 96 | - ... 97 | 98 | Please use the [issue system](https://github.com/DavidMStraub/gramps-webapp/issues) to report bugs or make feature requests. 99 | -------------------------------------------------------------------------------- /gramps_webapp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidMStraub/gramps-webapp/1dcb57b7ce38a4aaa67fcd2896409f1111b8f557/gramps_webapp/__init__.py -------------------------------------------------------------------------------- /gramps_webapp/__main__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import click 4 | 5 | from .api import create_app, get_db 6 | 7 | 8 | @click.group("cli") 9 | def cli(): 10 | pass 11 | 12 | 13 | @cli.group() 14 | @click.option("-O", "--open", help="Family tree to use") 15 | @click.option("--no-auth", help="Disable authentication", is_flag=True) 16 | @click.pass_context 17 | def app(ctx, open, no_auth): 18 | if open: 19 | os.environ["TREE"] = open 20 | if no_auth: 21 | os.environ["GRAMPS_AUTH_PROVIDER"] = 'none' 22 | ctx.obj = create_app() 23 | 24 | 25 | @app.command("run") 26 | @click.option("-p", "--port", help="Port to use (default: 5000)", default=5000) 27 | @click.pass_context 28 | def run(ctx, port): 29 | """Custom CLI command.""" 30 | app = ctx.obj 31 | app.run(port=port, threaded=False) 32 | 33 | 34 | @app.group() 35 | @click.option("--bucket", help="S3 bucket name", required=True) 36 | @click.pass_context 37 | def s3(ctx, bucket): 38 | ctx.obj = {"bucket": bucket, "app": ctx.obj} 39 | 40 | 41 | @s3.command("upload") 42 | @click.pass_context 43 | def s3_upload(ctx): 44 | """Upload media objects to AWS S3 cloud storage.""" 45 | app = ctx.obj["app"] 46 | bucket = ctx.obj["bucket"] 47 | from .s3 import MediaBucketUploader 48 | 49 | with app.app_context(): 50 | if not get_db().dbstate.is_open(): 51 | get_db().open() 52 | 53 | uploader = MediaBucketUploader( 54 | get_db().db, bucket, create=True, logger=app.logger 55 | ) 56 | uploader.upload_missing() 57 | get_db().close() 58 | 59 | 60 | @cli.group("user") 61 | @click.argument("db") 62 | @click.pass_context 63 | def user(ctx, db): 64 | from .auth import SQLAuth 65 | ctx.obj = SQLAuth(db_uri=db, logging=False) 66 | 67 | 68 | @user.command("add") 69 | @click.argument("name") 70 | @click.argument("password" ) 71 | @click.option("--fullname", help="Full name", default="") 72 | @click.option("--email", help="E-mail address", default=None) 73 | @click.pass_context 74 | def user_add(ctx, name, password, fullname, email): 75 | auth = ctx.obj 76 | auth.add_user(name, password, fullname, email) 77 | 78 | 79 | @user.command("delete") 80 | @click.argument("name") 81 | @click.pass_context 82 | def user_del(ctx, name): 83 | auth = ctx.obj 84 | auth.delete_user(name) 85 | 86 | 87 | if __name__ == "__main__": 88 | cli() 89 | -------------------------------------------------------------------------------- /gramps_webapp/api.py: -------------------------------------------------------------------------------- 1 | """Flask web app providing a REST API to a gramps family tree.""" 2 | 3 | 4 | import datetime 5 | import json 6 | import logging 7 | import os 8 | import secrets 9 | from functools import wraps 10 | 11 | import click 12 | from flask import ( 13 | Flask, 14 | Response, 15 | current_app, 16 | g, 17 | jsonify, 18 | request, 19 | send_file, 20 | send_from_directory, 21 | ) 22 | from flask.cli import AppGroup, FlaskGroup 23 | from flask_caching import Cache 24 | from flask_compress import Compress 25 | from flask_cors import CORS 26 | from flask_jwt_extended import ( 27 | JWTManager, 28 | create_access_token, 29 | create_refresh_token, 30 | get_jwt_identity, 31 | jwt_refresh_token_required, 32 | jwt_required, 33 | verify_jwt_in_request, 34 | verify_jwt_refresh_token_in_request, 35 | ) 36 | from flask_restful import Api, Resource, reqparse 37 | 38 | from flask_limiter import Limiter 39 | from flask_limiter.util import get_remote_address 40 | 41 | from .auth import SingleUser, SQLAuth 42 | from .db import Db 43 | from .gramps import ( 44 | get_citations, 45 | get_db_info, 46 | get_events, 47 | get_families, 48 | get_languages, 49 | get_media, 50 | get_media_info, 51 | get_note, 52 | get_people, 53 | get_places, 54 | get_repositories, 55 | get_sources, 56 | get_translation, 57 | ) 58 | from .image import get_thumbnail, get_thumbnail_cropped 59 | from .media import FileHandler, S3Handler 60 | 61 | 62 | def Boolean(v): 63 | """Coerce value `v` to boolean.""" 64 | if isinstance(v, str): 65 | if v.lower() in ["yes", "y", "true"]: 66 | return True 67 | elif v.lower() in ["no", "n", "false"]: 68 | return False 69 | raise ValueError("Cannot corce string {} to boolean".format(v)) 70 | else: 71 | return bool(v) 72 | 73 | 74 | def get_db(): 75 | """Get a new `Db` instance. Called before every request. Cached on first call.""" 76 | if "db" not in g: 77 | g.db = Db( 78 | current_app.config["TREE"], 79 | include_private=not current_app.config["GRAMPS_EXCLUDE_PRIVATE"], 80 | include_living=not current_app.config["GRAMPS_EXCLUDE_LIVING"], 81 | ) 82 | return g.db 83 | 84 | 85 | def close_db(e=None): 86 | """Close the Database. Called after every request.""" 87 | db = g.pop("db", None) 88 | if db is not None: 89 | db.close(False) 90 | 91 | 92 | def get_jwt_secret_key(store=True): 93 | """Return the JWT secret key. 94 | 95 | If there is no environment variable 'JWT_SECRET_KEY', 96 | a key will be reat from the file 'jwt_secret_key'. 97 | If this does not exist, a safe token will be randomly 98 | generated. If `store` is True, this generated token 99 | will be safed to a file.""" 100 | jwt_secret_key = os.getenv("JWT_SECRET_KEY") 101 | if not jwt_secret_key: 102 | if os.path.exists("jwt_secret_key"): 103 | with open("jwt_secret_key", "r") as f: 104 | jwt_secret_key = f.read() 105 | else: 106 | jwt_secret_key = secrets.token_urlsafe(64) 107 | if store: 108 | with open("jwt_secret_key", "w") as f: 109 | f.write(jwt_secret_key) 110 | return jwt_secret_key 111 | 112 | 113 | def create_app(): 114 | """Flask application factory.""" 115 | app = Flask(__name__, static_folder="js") 116 | app.config["PROPAGATE_EXCEPTIONS"] = True 117 | app.config["TREE"] = os.getenv("TREE") 118 | app.config["GRAMPS_EXCLUDE_PRIVATE"] = Boolean(os.getenv("GRAMPS_EXCLUDE_PRIVATE")) 119 | app.config["GRAMPS_EXCLUDE_LIVING"] = Boolean(os.getenv("GRAMPS_EXCLUDE_LIVING")) 120 | app.config["TREE"] = os.getenv("TREE") 121 | if app.config["TREE"] is None or app.config["TREE"] == "": 122 | raise ValueError("You have to set the `TREE` environment variable.") 123 | app.config["GRAMPS_S3_BUCKET_NAME"] = os.getenv("GRAMPS_S3_BUCKET_NAME") 124 | app.config["PASSWORD"] = os.getenv("PASSWORD", "") 125 | app.config["GRAMPS_USER_DB_URI"] = os.getenv("GRAMPS_USER_DB_URI", "") 126 | app.config["GRAMPS_AUTH_PROVIDER"] = os.getenv("GRAMPS_AUTH_PROVIDER", "") 127 | 128 | if app.config["GRAMPS_AUTH_PROVIDER"] == "password": 129 | auth_provider = SingleUser(password=app.config["PASSWORD"]) 130 | elif app.config["GRAMPS_AUTH_PROVIDER"] == "none": 131 | auth_provider = None 132 | else: 133 | auth_provider = SQLAuth(db_uri=app.config["GRAMPS_USER_DB_URI"]) 134 | 135 | app.logger.setLevel(logging.INFO) 136 | app.logger.info("Opening family tree '{}'".format(app.config["TREE"])) 137 | 138 | limiter = Limiter(app, key_func=get_remote_address) 139 | 140 | # called once here in case Db's constructor raises 141 | Db(app.config["TREE"]) 142 | 143 | CORS(app) 144 | Compress(app) 145 | api = Api(app) 146 | cache = Cache(app, config={"CACHE_TYPE": "filesystem", "CACHE_DIR": "appcache"}) 147 | 148 | app.config["JWT_TOKEN_LOCATION"] = ["headers", "query_string"] 149 | app.config["JWT_ACCESS_TOKEN_EXPIRES"] = datetime.timedelta(minutes=15) 150 | app.config["JWT_REFRESH_TOKEN_EXPIRES"] = datetime.timedelta(days=30) 151 | app.config["JWT_SECRET_KEY"] = get_jwt_secret_key() 152 | 153 | jwt = JWTManager(app) 154 | 155 | @app.route("/", methods=["GET", "POST"]) 156 | def send_js_index(): 157 | return send_from_directory(app.static_folder, "index.html") 158 | 159 | @app.route("/", methods=["GET", "POST"]) 160 | def send_js(path): 161 | if path and os.path.exists(os.path.join(app.static_folder, path)): 162 | return send_from_directory(app.static_folder, path) 163 | else: 164 | return send_from_directory(app.static_folder, "index.html") 165 | 166 | @app.route("/api/login", methods=["POST"]) 167 | @limiter.limit("1/second") 168 | def login(): 169 | if app.config["GRAMPS_AUTH_PROVIDER"] == "none": 170 | ret = {"access_token": "1", "refresh_token": "1"} 171 | return jsonify(ret), 200 172 | if not request.is_json: 173 | return jsonify({"msg": "Missing JSON in request"}), 400 174 | username = request.json.get("username", None) 175 | password = request.json.get("password", None) 176 | from .auth import User 177 | 178 | if not auth_provider.authorized(username, password): 179 | return jsonify({"msg": "Wrong username or password"}), 401 180 | ret = { 181 | "access_token": create_access_token(identity=username), 182 | "refresh_token": create_refresh_token(identity=username), 183 | } 184 | return jsonify(ret), 200 185 | 186 | def jwt_required_ifauth(fn): 187 | """Check JWT unless authentication is disabled""" 188 | 189 | @wraps(fn) 190 | def wrapper(*args, **kwargs): 191 | if app.config["GRAMPS_AUTH_PROVIDER"] != "none": 192 | verify_jwt_in_request() 193 | return fn(*args, **kwargs) 194 | 195 | return wrapper 196 | 197 | def jwt_refresh_token_required_ifauth(fn): 198 | """Check JWT unless authentication is disabled""" 199 | 200 | @wraps(fn) 201 | def wrapper(*args, **kwargs): 202 | if app.config["GRAMPS_AUTH_PROVIDER"] != "none": 203 | verify_jwt_refresh_token_in_request() 204 | return fn(*args, **kwargs) 205 | 206 | return wrapper 207 | 208 | @app.route("/api/refresh", methods=["POST"]) 209 | @jwt_refresh_token_required_ifauth 210 | def refresh(): 211 | if app.config["GRAMPS_AUTH_PROVIDER"] == "none": 212 | ret = {"access_token": "1"} 213 | return jsonify(ret), 200 214 | current_user = get_jwt_identity() 215 | ret = {"access_token": create_access_token(identity=current_user)} 216 | return jsonify(ret), 200 217 | 218 | parser = reqparse.RequestParser() 219 | parser.add_argument("strings", type=str) 220 | parser.add_argument("fmt", type=str) 221 | 222 | @app.before_request 223 | def before_request(): 224 | if not get_db().dbstate.is_open(): 225 | get_db().open() 226 | 227 | app.teardown_appcontext(close_db) 228 | 229 | class ProtectedResource(Resource): 230 | method_decorators = [jwt_required_ifauth] 231 | 232 | class People(ProtectedResource): 233 | @cache.cached() 234 | def get(self): 235 | return get_people(get_db()) 236 | 237 | class Families(ProtectedResource): 238 | @cache.cached() 239 | def get(self): 240 | return get_families(get_db()) 241 | 242 | class Events(ProtectedResource): 243 | @cache.cached() 244 | def get(self): 245 | return get_events(get_db()) 246 | 247 | class Places(ProtectedResource): 248 | @cache.cached() 249 | def get(self): 250 | return get_places(get_db()) 251 | 252 | class Citations(ProtectedResource): 253 | @cache.cached() 254 | def get(self): 255 | return get_citations(get_db()) 256 | 257 | class Sources(ProtectedResource): 258 | @cache.cached() 259 | def get(self): 260 | return get_sources(get_db()) 261 | 262 | class Repositories(ProtectedResource): 263 | @cache.cached() 264 | def get(self): 265 | return get_repositories(get_db()) 266 | 267 | class MediaObjects(ProtectedResource): 268 | @cache.cached() 269 | def get(self): 270 | return get_media(get_db()) 271 | 272 | class DbInfo(ProtectedResource): 273 | @cache.cached() 274 | def get(self): 275 | return get_db_info(get_db()) 276 | 277 | class FullTree(ProtectedResource): 278 | @cache.cached() 279 | def get(self): 280 | return { 281 | "people": get_people(get_db()), 282 | "families": get_families(get_db()), 283 | "events": get_events(get_db()), 284 | "places": get_places(get_db()), 285 | "citations": get_citations(get_db()), 286 | "sources": get_sources(get_db()), 287 | "repositories": get_repositories(get_db()), 288 | "media": get_media(get_db()), 289 | "dbinfo": get_db_info(get_db()), 290 | } 291 | 292 | class Translate(Resource): 293 | @cache.cached() 294 | def get(self): 295 | args = parser.parse_args() 296 | try: 297 | strings = json.loads(args["strings"]) 298 | lang = args.get("lang") 299 | except (json.decoder.JSONDecodeError, TypeError, ValueError) as e: 300 | return {"error": str(e)} 301 | return {"data": get_translation(strings, lang=lang)} 302 | 303 | class Languages(Resource): 304 | @cache.cached() 305 | def get(self): 306 | return {"data": get_languages()} 307 | 308 | class Note(ProtectedResource): 309 | @cache.cached(query_string=True) 310 | def get(self, gramps_id): 311 | args = parser.parse_args() 312 | fmt = args.get("fmt") or "html" 313 | return get_note(get_db(), gramps_id, fmt=fmt) 314 | 315 | api.add_resource(People, "/api/people") 316 | api.add_resource(Families, "/api/families") 317 | api.add_resource(Events, "/api/events") 318 | api.add_resource(Places, "/api/places") 319 | api.add_resource(Citations, "/api/citations") 320 | api.add_resource(Sources, "/api/sources") 321 | api.add_resource(MediaObjects, "/api/mediaobjects") 322 | api.add_resource(Repositories, "/api/repositories") 323 | api.add_resource(Translate, "/api/translate") 324 | api.add_resource(Languages, "/api/languages") 325 | api.add_resource(DbInfo, "/api/dbinfo") 326 | api.add_resource(FullTree, "/api/tree") 327 | api.add_resource(Note, "/api/note/") 328 | 329 | def get_media_handler(handle): 330 | info = get_media_info(get_db(), handle) 331 | if app.config["GRAMPS_S3_BUCKET_NAME"]: 332 | return S3Handler( 333 | handle, info, bucket_name=app.config["GRAMPS_S3_BUCKET_NAME"] 334 | ) 335 | else: 336 | return FileHandler(handle, info) 337 | 338 | @app.route("/api/media/") 339 | @jwt_required_ifauth 340 | def show_image(handle): 341 | handler = get_media_handler(handle) 342 | return handler.send_file() 343 | 344 | @app.route("/api/thumbnail//") 345 | @jwt_required_ifauth 346 | @cache.cached() 347 | def show_thumbnail_square(handle, size): 348 | handler = get_media_handler(handle) 349 | return handler.send_thumbnail_square(size) 350 | 351 | @app.route( 352 | "/api/thumbnail//////" 353 | ) 354 | @jwt_required_ifauth 355 | @cache.cached() 356 | def show_thumbnail_square_cropped(handle, size, x1, y1, x2, y2): 357 | handler = get_media_handler(handle) 358 | return handler.send_thumbnail_square_cropped(size, x1, y1, x2, y2) 359 | 360 | return app 361 | -------------------------------------------------------------------------------- /gramps_webapp/auth.py: -------------------------------------------------------------------------------- 1 | """Define methods of providing authentication for users.""" 2 | 3 | import hashlib 4 | import os 5 | import uuid 6 | 7 | import sqlalchemy as sa 8 | from sqlalchemy.ext.declarative import declarative_base 9 | from sqlalchemy.orm import sessionmaker 10 | 11 | from .sql_guid import GUID 12 | 13 | 14 | class AuthProvider: 15 | """Base class. not meant to be used directly.""" 16 | 17 | def __init__(self): 18 | pass 19 | 20 | def authorized(self, username, password): 21 | """Return true if the username is authorized. Must be implemented 22 | by child classes.""" 23 | return False 24 | 25 | 26 | class SingleUser(AuthProvider): 27 | """Single user with hard coded password. Username is ignored.""" 28 | 29 | def __init__(self, password): 30 | super().__init__() 31 | self.password = password 32 | 33 | def authorized(self, username, password): 34 | """Return true if the username is authorized.""" 35 | if password == self.password: 36 | return True 37 | return False 38 | 39 | 40 | Base = declarative_base() 41 | 42 | 43 | class SQLAuth(AuthProvider): 44 | """SQL Alchemy user database.""" 45 | 46 | def __init__(self, db_uri, logging=False): 47 | super().__init__() 48 | self.db_uri = db_uri 49 | self.engine = sa.create_engine(db_uri, echo=logging) 50 | Session = sessionmaker(bind=self.engine) 51 | self.session = Session() 52 | 53 | def add_user(self, name, password, fullname="", email=None, role=None, commit=True): 54 | """Add a user.""" 55 | Base.metadata.create_all(bind=self.engine) # create table if not exists 56 | if password == "": 57 | raise ValueError("Password must not be empty") 58 | if name == "": 59 | raise ValueError("Username must not be empty") 60 | pwhash = self.hash_password(password) 61 | user = User( 62 | id=uuid.uuid4(), 63 | name=name, 64 | fullname=fullname, 65 | email=email, 66 | pwhash=pwhash, 67 | role=role, 68 | ) 69 | if self.session.query(sa.exists().where(User.name == name)).scalar(): 70 | raise ValueError("Username {} already exists".format(name)) 71 | if ( 72 | email 73 | and self.session.query(sa.exists().where(User.email == email)).scalar() 74 | ): 75 | raise ValueError("A user with this e-mail address already exists") 76 | self.session.add(user) 77 | if commit: 78 | self.session.commit() 79 | 80 | def get_guid(self, name): 81 | """Get the GUID of an existing user by username.""" 82 | user = self.session.query(User).filter_by(name=name).scalar() 83 | if user is None: 84 | raise ValueError("User {} not found".format(name)) 85 | return user.id 86 | 87 | def delete_user(self, name, commit=True): 88 | """Delete an existing user.""" 89 | user = self.session.query(User).filter_by(name=name).scalar() 90 | if user is None: 91 | raise ValueError("User {} not found".format(name)) 92 | self.session.delete(user) 93 | if commit: 94 | self.session.commit() 95 | 96 | def modify_user( 97 | self, 98 | name, 99 | name_new=None, 100 | password=None, 101 | fullname=None, 102 | email=None, 103 | role=None, 104 | commit=True, 105 | ): 106 | """Modify an existing user.""" 107 | user = self.session.query(User).filter_by(name=name).scalar() 108 | if user is None: 109 | raise ValueError("User {} not found".format(name)) 110 | if name_new is not None: 111 | user.name = name_new 112 | if password is not None: 113 | user.pwhash = self.hash_password(password) 114 | if fullname is not None: 115 | user.fullname = fullname 116 | user.email = email # also for None since nullable 117 | if role is not None: 118 | user.role = role 119 | if commit: 120 | self.session.commit() 121 | 122 | @staticmethod 123 | def _salt(): 124 | """Return a random salt.""" 125 | return hashlib.sha256(os.urandom(60)).hexdigest().encode("ascii") 126 | 127 | @staticmethod 128 | def _hash(password, salt): 129 | """Compute a password hash.""" 130 | return hashlib.pbkdf2_hmac("sha512", password.encode("utf-8"), salt, 100000) 131 | 132 | def hash_password(self, password): 133 | """Compute salted password hash.""" 134 | salt = self._salt() 135 | pw_hash = self._hash(password, salt) 136 | return salt.decode("ascii") + pw_hash.hex() 137 | 138 | def verify_password(self, password, salt_hash): 139 | """Verify a stored password against one provided by user""" 140 | salt = salt_hash[:64].encode("ascii") 141 | stored_pw_hash = salt_hash[64:] 142 | pw_hash = self._hash(password, salt) 143 | pw_hash = pw_hash.hex() 144 | return pw_hash == stored_pw_hash 145 | 146 | def authorized(self, username, password): 147 | """Return true if the username is authorized.""" 148 | user = self.session.query(User).filter_by(name=username).scalar() 149 | if user is None: 150 | return False 151 | return self.verify_password(password, user.pwhash) 152 | 153 | 154 | class User(Base): 155 | """User table class for sqlalchemy.""" 156 | 157 | __tablename__ = "users" 158 | 159 | id = sa.Column(GUID, primary_key=True) 160 | name = sa.Column(sa.String, unique=True) 161 | email = sa.Column(sa.String, unique=True, nullable=True) 162 | fullname = sa.Column(sa.String) 163 | pwhash = sa.Column(sa.String) 164 | role = sa.Column(sa.Integer, default=0) 165 | 166 | def __repr__(self): 167 | return "" % (self.name, self.fullname) 168 | -------------------------------------------------------------------------------- /gramps_webapp/db.py: -------------------------------------------------------------------------------- 1 | """Functions for database access.""" 2 | 3 | 4 | import os 5 | 6 | from gramps.cli.clidbman import CLIDbManager 7 | from gramps.cli.grampscli import CLIManager 8 | from gramps.cli.user import User 9 | from gramps.gen.db.utils import get_dbid_from_path 10 | from gramps.gen.dbstate import DbState 11 | from gramps.gen.proxy import LivingProxyDb, PrivateProxyDb 12 | 13 | 14 | ALLOWED_DB_BACKENDS = [ 15 | 'sqlite', 16 | ] 17 | 18 | 19 | class Db(): 20 | """Class for database handling.""" 21 | 22 | def __init__(self, name, 23 | include_private=True, include_living=True): 24 | """Initialize the database object for family tree `name`. 25 | 26 | This will raise if the database backend is not `sqlite`. 27 | The constructor does not open/lock the database yet. 28 | 29 | Parameters: 30 | 31 | - `include_private`: include records marked as private. Default True 32 | - `include_living`: include living people. Default True 33 | """ 34 | self.name = name 35 | self.include_private = include_private 36 | self.include_living = include_living 37 | self.dbstate = DbState() 38 | self.dbman = CLIDbManager(self.dbstate) 39 | self.user = User() 40 | self.smgr = CLIManager(self.dbstate, True, self.user) 41 | self.path = self.dbman.get_family_tree_path(name) 42 | if not self.path: 43 | raise ValueError("Family tree {} not found. Known trees: {}" 44 | .format(name, self.dbman.family_tree_list())) 45 | self.db_backend = self.get_dbid() 46 | if self.db_backend not in ALLOWED_DB_BACKENDS: 47 | raise ValueError("Database backend '{}' of tree '{}' not supported." 48 | .format(self.db_backend, name)) 49 | 50 | 51 | @property 52 | def db(self): 53 | """Return the database or a proxy database.""" 54 | _db = self.dbstate.db 55 | if not self.include_private: 56 | _db = PrivateProxyDb(_db) 57 | if not self.include_living: 58 | _db = LivingProxyDb(_db, 59 | LivingProxyDb.MODE_INCLUDE_FULL_NAME_ONLY) 60 | return _db 61 | 62 | def get_dbid(self): 63 | """Get the database backend.""" 64 | return get_dbid_from_path(self.path) 65 | 66 | def is_locked(self): 67 | """Returns a boolean whether the database is locked.""" 68 | return os.path.isfile(os.path.join(self.path, "lock")) 69 | 70 | def open(self, force=False): 71 | """Open the database. 72 | 73 | If `force` is `True`, will break an existing lock (use with care!). 74 | """ 75 | if force: 76 | self.dbman.break_lock(self.path) 77 | return self.smgr.open_activate(self.path) 78 | 79 | def close(self, *args, **kwargs): 80 | """Close the database (if it is open).""" 81 | if self.dbstate.is_open(): 82 | return self.dbstate.db.close(*args, **kwargs) 83 | -------------------------------------------------------------------------------- /gramps_webapp/gramps.py: -------------------------------------------------------------------------------- 1 | """Functions using the Gramps Python package to access the family tree.""" 2 | 3 | 4 | import os 5 | 6 | import bleach 7 | from gramps.gen.const import GRAMPS_LOCALE 8 | from gramps.gen.display.name import NameDisplay 9 | from gramps.gen.display.place import displayer as place_displayer 10 | from gramps.gen.lib import NoteType 11 | from gramps.gen.utils.db import get_marriage_or_fallback 12 | from gramps.gen.utils.file import expand_media_path 13 | from gramps.gen.utils.grampslocale import _LOCALE_NAMES, GrampsLocale 14 | from gramps.gen.utils.location import get_main_location 15 | from gramps.gen.utils.place import conv_lat_lon 16 | from gramps.plugins.lib.libhtml import Html 17 | from gramps.plugins.lib.libhtmlbackend import HtmlBackend, process_spaces 18 | 19 | nd = NameDisplay() 20 | dd = GRAMPS_LOCALE.date_displayer 21 | 22 | 23 | ALLOWED_TAGS = [ 24 | "a", 25 | "abbr", 26 | "acronym", 27 | "b", 28 | "blockquote", 29 | "code", 30 | "em", 31 | "i", 32 | "li", 33 | "ol", 34 | "strong", 35 | "ul", 36 | "span", 37 | "p", 38 | "br", 39 | "div", 40 | ] 41 | ALLOWED_ATTRIBUTES = { 42 | "a": ["href", "title", "style"], 43 | "abbr": ["title", "style"], 44 | "acronym": ["title", "style"], 45 | "p": ["style"], 46 | "div": ["style"], 47 | "span": ["style"], 48 | } 49 | ALLOWED_STYLES = [ 50 | "color", 51 | "background-color", 52 | "font-family", 53 | "font-weight", 54 | "font-size", 55 | "font-style", 56 | "text-decoration", 57 | ] 58 | 59 | 60 | def sanitize(s): 61 | """Sanitize an HTML string by keeping only a couple of allowed 62 | tags/attributes.""" 63 | if isinstance(s, str): 64 | return bleach.clean( 65 | s, 66 | tags=ALLOWED_TAGS, 67 | attributes=ALLOWED_ATTRIBUTES, 68 | styles=ALLOWED_STYLES, 69 | strip=True, 70 | ) 71 | return s 72 | 73 | 74 | def strip_tags(s): 75 | """Strip all HTML tags from a string.""" 76 | if isinstance(s, str): 77 | return bleach.clean(s, tags=[], attributes=[], strip=True) 78 | return s 79 | 80 | 81 | def get_birthplace(db, p): 82 | """Return the name of the birth place.""" 83 | birth_ref = p.get_birth_ref() 84 | if not birth_ref: 85 | return '' 86 | return get_event_place_from_handle(db, birth_ref.ref) 87 | 88 | 89 | def get_birthdate(db, p): 90 | """Return the formatted birth date.""" 91 | birth_ref = p.get_birth_ref() 92 | if not birth_ref: 93 | return '' 94 | return get_event_date_from_handle(db, birth_ref.ref) 95 | 96 | 97 | def get_deathdate(db, p): 98 | """Return the formatted death date.""" 99 | death_ref = p.get_death_ref() 100 | if not death_ref: 101 | return '' 102 | return get_event_date_from_handle(db, death_ref.ref) 103 | 104 | 105 | def get_deathplace(db, p): 106 | """Return the name of the death place.""" 107 | death_ref = p.get_death_ref() 108 | if not death_ref: 109 | return '' 110 | return get_event_place_from_handle(db, death_ref.ref) 111 | 112 | 113 | def display_date(date): 114 | """Format the date object.""" 115 | if not date: 116 | return '' 117 | return dd.display(date) 118 | 119 | 120 | def get_event_date_from_handle(db, handle): 121 | """Return a formatted date for the event.""" 122 | ev = db.get_event_from_handle(handle) 123 | if not ev: 124 | return '' 125 | date = ev.get_date_object() 126 | return display_date(date) 127 | 128 | 129 | def display_place(db, place): 130 | """Return the formatted place name.""" 131 | if not place: 132 | return '' 133 | return place_displayer.display(db, place) 134 | 135 | 136 | def get_event_place_from_handle(db, handle): 137 | """Get the event's place.""" 138 | ev = db.get_event_from_handle(handle) 139 | if not ev: 140 | return '' 141 | return get_event_place(db, ev) 142 | 143 | 144 | def get_event_place(db, ev): 145 | """Get the event's place.""" 146 | if not ev or not ev.place: 147 | return '' 148 | place = db.get_place_from_handle(ev.place) 149 | return place.gramps_id 150 | 151 | 152 | def get_marriageplace(db, f): 153 | """Get the marriage event's place.""" 154 | ev = get_marriage_or_fallback(db, f) 155 | if not ev: 156 | return '' 157 | return get_event_place_from_handle(db, ev.handle) 158 | 159 | 160 | def get_marriagedate(db, f): 161 | """Get the marriage event's date.""" 162 | ev = get_marriage_or_fallback(db, f) 163 | if not ev: 164 | return '' 165 | return get_event_date_from_handle(db, ev.handle) 166 | 167 | 168 | def get_father_id(db, f): 169 | """Get the Gramps ID of the family's father.""" 170 | handle = f.father_handle 171 | if not handle: 172 | return '' 173 | return db.get_person_from_handle(handle).gramps_id 174 | 175 | 176 | def get_name_from_handle(db, handle): 177 | """Get the name of the person.""" 178 | p = db.get_person_from_handle(handle) 179 | if not p: 180 | return '' 181 | sn = p.primary_name.get_surname() 182 | gn = nd.display_given(p) 183 | return '{}, {}'.format(sn, gn) 184 | 185 | 186 | def get_father_name(db, f): 187 | """Get the name of the family's father.""" 188 | handle = f.father_handle 189 | if not handle: 190 | return '' 191 | return get_name_from_handle(db, handle) 192 | 193 | 194 | def get_mother_id(db, f): 195 | """Get the Gramps ID of the family's mother.""" 196 | handle = f.mother_handle 197 | if not handle: 198 | return '' 199 | return db.get_person_from_handle(handle).gramps_id 200 | 201 | 202 | def get_mother_name(db, f): 203 | """Get the name of the family's mother.""" 204 | handle = f.mother_handle 205 | if not handle: 206 | return '' 207 | return get_name_from_handle(db, handle) 208 | 209 | 210 | def get_children_id(db, f): 211 | """Get the Gramps IDs of all the family's children.""" 212 | refs = f.child_ref_list 213 | if not refs: 214 | return [] 215 | return [db.get_person_from_handle(r.ref).gramps_id for r in refs] 216 | 217 | 218 | def get_parents_id(db, p): 219 | """Get the Gramps IDs of the family's parents.""" 220 | ref = p.get_main_parents_family_handle() 221 | if not ref: 222 | return '' 223 | return db.get_family_from_handle(ref).gramps_id 224 | 225 | 226 | def get_families_id(db, p): 227 | """Get the Gramps IDs of all the person's families.""" 228 | refs = p.get_family_handle_list() 229 | if not refs: 230 | return [] 231 | return [db.get_family_from_handle(r).gramps_id for r in refs] 232 | 233 | 234 | def get_event_participants(db, handle): 235 | """Get a list of dictionaries with the roles and Gramps IDs of all of the 236 | event's participants, and whether they are a family or person.""" 237 | participant = {} 238 | try: 239 | result_list = list(db.find_backlink_handles(handle, 240 | include_classes=['Person', 'Family'])) 241 | except: 242 | return {} 243 | 244 | people = set([x[1] for x in result_list if x[0] == 'Person']) 245 | for personhandle in people: 246 | person = db.get_person_from_handle(personhandle) 247 | if not person: 248 | continue 249 | for event_ref in person.get_event_ref_list(): 250 | if handle == event_ref.ref: 251 | role = event_ref.get_role().string 252 | if role not in participant: 253 | participant[role] = [] 254 | participant[role].append({'type': 'Person', 'gramps_id': person.gramps_id}) 255 | 256 | families = set([x[1] for x in result_list if x[0] == 'Family']) 257 | for familyhandle in families: 258 | family = db.get_family_from_handle(familyhandle) 259 | if not family: 260 | continue 261 | for event_ref in family.get_event_ref_list(): 262 | if handle == event_ref.ref: 263 | role = event_ref.get_role().string 264 | if role not in participant: 265 | participant[role] = [] 266 | participant[role].append({'type': 'Family', 'gramps_id': family.gramps_id}) 267 | 268 | return participant 269 | 270 | 271 | def geolocation(p): 272 | """Get the latitude and longitude of a place.""" 273 | lat, lon = p.get_latitude(), p.get_longitude() 274 | if lat is None or lon is None: 275 | return None 276 | return conv_lat_lon(lat, lon) 277 | 278 | 279 | def get_citation_ids(db, x): 280 | """Get the Gramps IDs of direct citations of object x 281 | (e.g. person, event, family, place)""" 282 | return [db.get_citation_from_handle(h).gramps_id for h in x.get_citation_list()] 283 | 284 | 285 | def get_note_ids(db, x): 286 | """Get the Gramps IDs of direct notes of object x 287 | (e.g. person, event, family, place)""" 288 | return [db.get_note_from_handle(h).gramps_id for h in x.get_note_list()] 289 | 290 | 291 | def get_event_ids(db, x): 292 | """Get the Gramps IDs of events of object x 293 | (e.g. person, family, place)""" 294 | return [db.get_event_from_handle(r.ref).gramps_id for r in x.get_event_ref_list()] 295 | 296 | 297 | def get_event_ids_roles(db, x): 298 | """Get the Gramps ID and role of events of object x""" 299 | return [{'gramps_id': db.get_event_from_handle(r.ref).gramps_id, 300 | 'role': r.get_role().string} 301 | for r in x.get_event_ref_list()] 302 | 303 | 304 | def family_to_dict(db, f): 305 | """Return a dictionary with information about the family.""" 306 | return { 307 | 'gramps_id': f.gramps_id, 308 | 'marriagedate': get_marriagedate(db, f), 309 | 'marriageplace': get_marriageplace(db, f), 310 | 'father_id': get_father_id(db, f), 311 | 'mother_id': get_mother_id(db, f), 312 | 'father_name': get_father_name(db, f), 313 | 'mother_name': get_mother_name(db, f), 314 | 'children': get_children_id(db, f), 315 | 'events': get_event_ids(db, f), 316 | 'media': [r.ref for r in f.get_media_list()], 317 | 'citations': get_citation_ids(db, f), 318 | 'notes': get_note_ids(db, f), 319 | } 320 | 321 | 322 | def person_to_dict(db, p): 323 | """Return a dictionary with information about the person.""" 324 | return { 325 | 'gramps_id': p.gramps_id, 326 | 'name_given': nd.display_given(p), 327 | 'name_surname': p.primary_name.get_surname(), 328 | 'gender': p.gender, 329 | 'birthdate': get_birthdate(db, p), 330 | 'deathdate': get_deathdate(db, p), 331 | 'birthplace': get_birthplace(db, p), 332 | 'deathplace': get_deathplace(db, p), 333 | 'parents': get_parents_id(db, p), 334 | 'families': get_families_id(db, p), 335 | 'events': get_event_ids_roles(db, p), 336 | 'media': [{'ref': r.ref, 'rect': r.rect} for r in p.get_media_list()], 337 | 'citations': get_citation_ids(db, p), 338 | 'notes': get_note_ids(db, p), 339 | } 340 | 341 | 342 | def place_to_dict(db, p): 343 | """Return a dictionary with information about the place.""" 344 | return { 345 | 'handle': p.handle, 346 | 'name': p.name.value, 347 | 'geolocation': geolocation(p), 348 | 'gramps_id': p.gramps_id, 349 | 'type_string': p.place_type.string, 350 | 'type_value': p.place_type.value, 351 | 'media': [{'ref': r.ref, 'rect': r.rect} for r in p.get_media_list()], 352 | 'hierarchy': get_main_location(db, p), 353 | 'citations': get_citation_ids(db, p), 354 | 'notes': get_note_ids(db, p), 355 | } 356 | 357 | 358 | def event_to_dict(db, e): 359 | """Return a dictionary with information about the event.""" 360 | return { 361 | 'handle': e.handle, 362 | 'gramps_id': e.gramps_id, 363 | 'type': e.get_type().string, 364 | 'place': get_event_place(db, e), 365 | 'date': display_date(e.date), 366 | 'date_sortval': e.date.get_sort_value(), 367 | 'description': e.get_description(), 368 | 'media': [{'ref': r.ref, 'rect': r.rect} for r in e.get_media_list()], 369 | 'participants': get_event_participants(db, e.handle), 370 | 'citations': get_citation_ids(db, e), 371 | 'notes': get_note_ids(db, e), 372 | } 373 | 374 | 375 | def citation_to_dict(db, c): 376 | """Return a dictionary with information about the citation.""" 377 | return { 378 | 'gramps_id': c.gramps_id, 379 | 'source': db.get_source_from_handle(c.get_reference_handle()).gramps_id, 380 | 'media': [{'ref': r.ref, 'rect': r.rect} for r in c.get_media_list()], 381 | 'date': dd.display(c.date), 382 | 'page': c.page, 383 | 'notes': get_note_ids(db, c), 384 | } 385 | 386 | 387 | def source_to_dict(db, s): 388 | """Return a dictionary with information about the source.""" 389 | return { 390 | 'gramps_id': s.gramps_id, 391 | 'title': s.get_title(), 392 | 'media': [{'ref': r.ref, 'rect': r.rect} for r in s.get_media_list()], 393 | 'repositories': [db.get_repository_from_handle(r.ref).gramps_id for r in s.get_reporef_list()], 394 | 'author': s.author, 395 | 'pubinfo': s.pubinfo, 396 | 'notes': get_note_ids(db, s), 397 | } 398 | 399 | 400 | def repository_to_dict(db, r): 401 | """Return a dictionary with information about the repository.""" 402 | return { 403 | 'gramps_id': r.gramps_id, 404 | 'title': r.name, 405 | 'type': r.get_type().string, 406 | 'notes': get_note_ids(db, r), 407 | } 408 | 409 | 410 | def media_to_dict(db, m): 411 | """Return a dictionary with information about the media object.""" 412 | return { 413 | 'gramps_id': m.gramps_id, 414 | 'desc': m.get_description(), 415 | 'notes': get_note_ids(db, m), 416 | 'citations': get_citation_ids(db, m), 417 | 'mime': m.get_mime_type(), 418 | 'date': dd.display(m.date), 419 | } 420 | 421 | 422 | def get_default_person_gramps_id(db): 423 | p = db.get_default_person() 424 | if p is None: 425 | return '' 426 | return p.gramps_id 427 | 428 | 429 | def get_db_info(tree): 430 | """Return a dictionary with information about the database.""" 431 | db = tree.db 432 | full_db = tree.dbstate.db 433 | return { 434 | 'name': full_db.get_dbname(), 435 | 'default_person': get_default_person_gramps_id(db), 436 | 'researcher': db.get_researcher().get_name(), 437 | 'number_people': db.get_number_of_people(), 438 | 'number_events': db.get_number_of_events(), 439 | 'number_families': db.get_number_of_families(), 440 | 'number_places': db.get_number_of_places(), 441 | } 442 | 443 | 444 | def get_people(tree): 445 | """Return a nested dictionary with information about all the people.""" 446 | db = tree.db 447 | return {p.gramps_id: person_to_dict(db, p) for p in db.iter_people()} 448 | 449 | 450 | def get_families(tree): 451 | """Return a nested dictionary with information about all the families.""" 452 | db = tree.db 453 | return {f.gramps_id: family_to_dict(db, f) for f in db.iter_families()} 454 | 455 | 456 | def get_events(tree): 457 | """Return a nested dictionary with information about all the events.""" 458 | db = tree.db 459 | return {e.gramps_id: event_to_dict(db, e) for e in db.iter_events()} 460 | 461 | def get_places(tree): 462 | """Return a nested dictionary with information about all the places.""" 463 | db = tree.db 464 | return {p.gramps_id: place_to_dict(db, p) for p in db.iter_places()} 465 | 466 | 467 | def get_citations(tree): 468 | """Return a nested dictionary with information about all the citations.""" 469 | db = tree.db 470 | return {c.gramps_id: citation_to_dict(db, c) for c in db.iter_citations()} 471 | 472 | 473 | def get_sources(tree): 474 | """Return a nested dictionary with information about all the sources.""" 475 | db = tree.db 476 | return {s.gramps_id: source_to_dict(db, s) for s in db.iter_sources()} 477 | 478 | 479 | def get_repositories(tree): 480 | """Return a nested dictionary with information about all the repositories.""" 481 | db = tree.db 482 | return {r.gramps_id: repository_to_dict(db, r) for r in db.iter_repositories()} 483 | 484 | def get_media(tree): 485 | """Return a nested dictionary with information about all the media objects.""" 486 | db = tree.db 487 | return {m.handle: media_to_dict(db, m) for m in db.iter_media()} 488 | 489 | 490 | def get_translation(strings, lang=None): 491 | """Return the translation of all the given strings for the current locale.""" 492 | if lang is not None: 493 | gramps_locale = GrampsLocale(lang=lang) 494 | else: 495 | gramps_locale = GRAMPS_LOCALE 496 | return {s: gramps_locale.translation.sgettext(s) for s in strings} 497 | 498 | def get_languages(): 499 | return _LOCALE_NAMES 500 | 501 | def get_media_info(tree, handle): 502 | """Return a dictionary with information about the media object.""" 503 | db = tree.db 504 | m = db.get_media_from_handle(handle) 505 | base_path = expand_media_path(db.get_mediapath(), db) 506 | return { 507 | 'mime': m.mime, 508 | 'path': m.path, 509 | 'full_path': os.path.join(base_path, m.path), 510 | } 511 | 512 | 513 | def get_note(tree, gramps_id, fmt='html'): 514 | """Return the type and content of a note. 515 | 516 | By default, the format of the content is HTML. 517 | If `fmt` is 'text' or None, it is returned as pure text instead.""" 518 | db = tree.db 519 | note = db.get_note_from_gramps_id(gramps_id) 520 | note_type = note.type.string 521 | if fmt == 'text' or fmt is None: 522 | return strip_tags(str(note.text)) 523 | elif fmt == 'html': 524 | htmlnotetext = styled_note(note.get_styledtext(), 525 | note.get_format(), 526 | contains_html=(note.get_type() == NoteType.HTML_CODE)) 527 | return {'type': note_type, 'content': sanitize(htmlnotetext), 'gramps_id': gramps_id} 528 | raise ValueError("Format {} not recognized.".format(fmt)) 529 | 530 | 531 | def styled_note(styledtext, format, contains_html=False): 532 | """Return the note in HTML format. 533 | 534 | Adapted from DynamicWeb. 535 | """ 536 | _backend = HtmlBackend() 537 | 538 | text = str(styledtext) 539 | 540 | if (not text): return('') 541 | 542 | s_tags = styledtext.get_tags() 543 | htmllist = Html("div", class_="grampsstylednote") 544 | if contains_html: 545 | markuptext = _backend.add_markup_from_styled(text, 546 | s_tags, 547 | split='\n', 548 | escape=False) 549 | htmllist += markuptext 550 | else: 551 | markuptext = _backend.add_markup_from_styled(text, 552 | s_tags, 553 | split='\n') 554 | linelist = [] 555 | linenb = 1 556 | sigcount = 0 557 | for line in markuptext.split('\n'): 558 | [line, sigcount] = process_spaces(line, format) 559 | if sigcount == 0: 560 | # The rendering of an empty paragraph '

' 561 | # is undefined so we use a non-breaking space 562 | if linenb == 1: 563 | linelist.append(' ') 564 | htmllist.extend(Html('p') + linelist) 565 | linelist = [] 566 | linenb = 1 567 | else: 568 | if linenb > 1: 569 | linelist[-1] += '
' 570 | linelist.append(line) 571 | linenb += 1 572 | if linenb > 1: 573 | htmllist.extend(Html('p') + linelist) 574 | # if the last line was blank, then as well as outputting the previous para, 575 | # which we have just done, 576 | # we also output a new blank para 577 | if sigcount == 0: 578 | linelist = [" "] 579 | htmllist.extend(Html('p') + linelist) 580 | return '\n'.join(htmllist) 581 | -------------------------------------------------------------------------------- /gramps_webapp/image.py: -------------------------------------------------------------------------------- 1 | """Functions for manipulating images, e.g. generating thumbnails.""" 2 | 3 | 4 | import io 5 | 6 | from PIL import Image, ImageOps 7 | from pdf2image import convert_from_path 8 | 9 | 10 | def get_image(path, mime): 11 | if mime == 'application/pdf': 12 | ims = convert_from_path(path, 13 | single_file=True, 14 | use_cropbox=True, 15 | dpi=100) 16 | if not ims: 17 | return None 18 | return ims[0] 19 | return Image.open(path) 20 | 21 | 22 | def get_thumbnail(path, size, square=False, mime=None): 23 | """Return a thumbnail of `size` (longest side) for the image at `path`. 24 | 25 | If `square` is true, the image is cropped to a centered square.""" 26 | im = get_image(path, mime) 27 | if square: 28 | im = ImageOps.fit(im, (size, size), bleed=0.0, centering=(0.0, 0.5), method=Image.BICUBIC) 29 | else: 30 | im.thumbnail((size, size)) 31 | f = io.BytesIO() 32 | im.save(f, format='JPEG') 33 | f.seek(0) 34 | return f 35 | 36 | 37 | def get_thumbnail_cropped(path, size, x1, y1, x2, y2, square=False, mime=None): 38 | """Return a cropped thumbnail of `size` (longest side) of the image at `path`. 39 | 40 | The arguments `x1`, `y1`, `x2`, `y2` are the coordinates of the cropped region 41 | in terms of the original image's coordinate system. 42 | 43 | If `square` is true, the image is cropped to a centered square.""" 44 | im = get_image(path, mime) 45 | w, h = im.size 46 | im = im.crop((x1 * w / 100, y1 * h / 100, x2 * w / 100, y2 * h / 100)) 47 | im.thumbnail((size, size)) 48 | if square: 49 | im = ImageOps.fit(im, (size, size), bleed=0.0, centering=(0.0, 0.5), method=Image.BICUBIC) 50 | else: 51 | im.thumbnail((size, size)) 52 | f = io.BytesIO() 53 | im.save(f, format='JPEG') 54 | f.seek(0) 55 | return f 56 | -------------------------------------------------------------------------------- /gramps_webapp/js/0e10338e.js: -------------------------------------------------------------------------------- 1 | let e,a=e=>e;import{c as i,s as t,h as r,t as d,S as s}from"./80126bfe.js";import{P as n}from"./c90d6c40.js";import"./e6ec0820.js";import"./fe5344c8.js";import"./fec21ef7.js";class m extends(i(t)(n)){render(){return r(e||(e=a`
`),this._families,this._hidden,d("Father"),d("Mother"),this._hidden,d("Marriage Date"))}static get styles(){return[s]}static get properties(){return{_families:{type:Object},_hidden:{type:Boolean}}}stateChanged(e){this._families=Object.values(e.api.families),this._hidden=!e.app.wideLayout}firstUpdated(){}}window.customElements.define("gr-view-families",m); 2 | -------------------------------------------------------------------------------- /gramps_webapp/js/19fdb0d6.js: -------------------------------------------------------------------------------- 1 | let e,a=e=>e;import{c as t,s as i,h as d,t as r,S as s}from"./80126bfe.js";import{P as n}from"./c90d6c40.js";import"./e6ec0820.js";import"./fe5344c8.js";import"./fec21ef7.js";class l extends(t(i)(n)){render(){return d(e||(e=a`
`),this._places,this._hidden,r("Name"),r("Type"))}static get styles(){return[s]}constructor(){super(),this._hidden=!1}static get properties(){return{_places:{type:Object},_hidden:{type:Boolean}}}stateChanged(e){this._places=Object.values(e.api.places),this._hidden=!i.getState().app.wideLayout}firstUpdated(){}}window.customElements.define("gr-view-places",l); 2 | -------------------------------------------------------------------------------- /gramps_webapp/js/43e752ee.js: -------------------------------------------------------------------------------- 1 | let e,t,i,a,s,o,r,n,l,p=e=>e;import{c as h,s as c,h as m,t as d,S as g}from"./80126bfe.js";import{P as _}from"./c90d6c40.js";import"./cb2c5929.js";import"./4582424e.js";import"./cba53e11.js";import"./4f794f05.js";import"./e6ec0820.js";import"./fe5344c8.js";import"./580fa394.js";import"./d880604a.js";import"./adb2bcf5.js";var u={"-1":"Unknown",0:"Custom",1:"Country",2:"State",3:"County",4:"City",5:"Parish",6:"Locality",7:"Street",8:"Province",9:"Region",10:"Department",11:"Neighborhood",12:"District",13:"Borough",14:"Municipality",15:"Town",16:"Village",17:"Hamlet",18:"Farm",19:"Building",20:"Number"};class y extends(h(c)(_)){render(){return null==this._place?m(e||(e=p`

Loading ...

`)):m(t||(t=p`

${0}

${0}
${0} ${0} ${0} ${0} ${0}
`),this._place.name,Object.entries(this._place.hierarchy).sort((e,t)=>t[0]>e[0]?1:-1).map((function(e){return m(i||(i=p` ${0} ${0} `),d(u[e[0]]),e[1])})),this._media.length?m(a||(a=p`

${0}

`),d("Media")):"",this._media,this._token,this._place.geolocation&&this._place.geolocation[0]?m(s||(s=p`

${0}

`),d("Map"),this._place.geolocation[0],this._place.geolocation[1],this._place.geolocation[0],this._place.geolocation[1]):"",this._notes.length?m(o||(o=p`

${0}

`),d("Notes")):"",this._notes.map(e=>m(r||(r=p` `),e)),this._citations.length?m(n||(n=p`

${0}

`),d("Sources")):"",this._citations)}static get styles(){return[g]}constructor(){super(),this._selected=0,this._media=Array()}static get properties(){return{_place:{type:Object},_gramps_id:{type:String},_token:{type:String},_events:{type:Object},_media:{type:Object},_hierarchy:{type:Object},_selected:{type:Number}}}_handleSelected(e){this._selected=e.detail.selected,window.location.hash=this._selected}_onHashChange(e){this._selected=e.newURL.split("#")[1]}firstUpdated(){window.addEventListener("hashchange",this._onHashChange),null!=window.location.hash.split("#")[1]&&(this._selected=window.location.hash.split("#")[1])}_personLink(e,t){return null==e?"":m(l||(l=p` ${0} ${0}${0} `),e.gramps_id,e.name_given,e.name_surname,t?"":", ")}_addMimeType(e,t){return e.map((function(e){return e.mime=t.api.media[e.ref].mime,e}))}stateChanged(e){this._token=e.api.token,this._gramps_id=e.app.activePlace,this._place=e.api.places[this._gramps_id],null!=this._place&&(this._media=this._addMimeType(this._place.media,e),this._hierarchy=this._place._hierarchy,this._citations=this._place.citations,this._notes=this._place.notes)}}window.customElements.define("gr-view-place",y); 2 | -------------------------------------------------------------------------------- /gramps_webapp/js/4582424e.js: -------------------------------------------------------------------------------- 1 | let e,t,s,i,r,n=e=>e;import{L as p,h as a,S as d}from"./80126bfe.js";import"./cb2c5929.js";window.customElements.define("gr-pedigree-card",class extends p{render(){return this.person?a(t||(t=n` ${0} `),null==this.person||Object.keys(this.person).length?a(i||(i=n` `),1===this.person.gender?"male":"female",this.width,this._personSelected,"pedigree"===this.link?"tree":"person/"+this.person.gramps_id,this.person.media.length?a(r||(r=n` `),this.token,this.person.media[0].ref,this.person.media[0].rect):"",this.person.name_surname,this.person.name_given,this.person.birthdate?"*":"",this.person.birthdate,this.person.deathdate?"†":"",this.person.deathdate):a(s||(s=n`
NN
`),this.width)):a(e||(e=n``))}static get styles(){return[d]}constructor(){super(),this.width="164px",this.link="pedigree"}_personSelected(){this.dispatchEvent(new CustomEvent("person-selected",{detail:{gramps_id:this.person.gramps_id}}))}static get properties(){return{person:{type:Object},width:{type:String},link:{type:String},token:{type:String}}}}); 2 | -------------------------------------------------------------------------------- /gramps_webapp/js/5522257c.js: -------------------------------------------------------------------------------- 1 | export{e as defineProperty,t as objectSpread2};function e(e,r,t){return r in e?Object.defineProperty(e,r,{value:t,enumerable:!0,configurable:!0,writable:!0}):e[r]=t,e}function r(e,r){var t=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);r&&(n=n.filter((function(r){return Object.getOwnPropertyDescriptor(e,r).enumerable}))),t.push.apply(t,n)}return t}function t(t){for(var n=1;ne;import{b as $,L as x,h as w,t as j,S,l as z,m as B,r as D,c as E,s as R,u as T}from"./80126bfe.js";import"./4582424e.js";import"./e6ec0820.js";import"./fe5344c8.js";const A=$(e||(e=k` 2 | 43 | `));document.head.appendChild(A.content);const C=$(t||(t=k` 44 | 53 | 54 | 166 | 167 | 218 | `));document.head.appendChild(C.content);window.customElements.define("gr-children-element",class extends x{render(){return w(a||(a=k` `),this.items,j("Given name"),j("Birth Date"),j("Death Date"))}static get styles(){return[S]}static get properties(){return{items:{type:Array}}}firstUpdated(){this.shadowRoot.querySelector("vaadin-grid").heightByRows=!0}});window.customElements.define("gr-person-element",class extends x{render(){return w(i||(i=k` ${0}${0} ${0} ${0} ${0} ${0} ${0} `),this.person.gramps_id,this.person.name_surname,this.person.name_surname&&this.person.name_given?",":"",this.person.name_given,this.person.birthdate?w(r||(r=k`   ${0} ${0}`),z,this.person.birthdate):"",this.person.birthplace?w(o||(o=k` ${0} ${0}`),j("in"),this.person.birthplace):"",this.person.deathdate?w(s||(s=k`   ${0} ${0}`),B,this.person.deathdate):"",this.person.deathplace?w(n||(n=k` ${0} ${0}`),j("in"),this.person.deathplace):"")}static get styles(){return[S]}static get properties(){return{person:{type:Array}}}});const L=D(l||(l=k` 219 | vaadin-grid-cell-content { 220 | white-space: normal; 221 | vertical-align: text-top; 222 | } 223 | } 224 | `));window.customElements.define("gr-events-element",class extends x{render(){return w(c||(c=k` ${0} `),this.items,j("Date"),j("Type"),j("Description"),this.place?w(d||(d=k` `),j("Place")):"")}static get styles(){return[L,S]}constructor(){super(),this.items=[],this.place=!1}firstUpdated(){this.shadowRoot.querySelector("vaadin-grid").heightByRows=!0}static get properties(){return{items:{type:Array},place:{type:Boolean}}}});class M extends(E(R)(x)){render(){const e=R.getState();return"families"in e.api?(this._family=e.api.families[this.gramps_id],"undefined"!=this._family.marriageplace&&""!=this._family.marriageplace&&(this._marriageplace_name=e.api.places[this._family.marriageplace].name),this._father=e.api.people[this._family.father_id],this._mother=e.api.people[this._family.mother_id],this._children=this._family.children.map(t=>e.api.people[t]),this._events=this._family.events.map(t=>e.api.events[t]),this._events=this._events.map(t=>this._get_place_name(e,t)),this._media=this._addMimeType(this._family.media,e),this._citations=this._family.citations,this._notes=this._family.notes,w(h||(h=k`
${0} ${0}

${0} ${0} ${0} ${0} ${0} ${0} `),this._father,this._token,this._family.marriagedate?w(p||(p=k`${0} ${0}`),T,this._family.marriagedate):"",this._family.marriageplace?w(g||(g=k`
${0} ${0}`),j("in"),this._family.marriageplace,this._marriageplace_name):"",this._mother,this._token,this._family.children.length>0?w(u||(u=k`

${0}

`),this.siblings?j("Siblings"):j("Children"),this._children):"",this._family.events.length>0?w(f||(f=k`

${0}

`),j("Events"),this._events):"",this._media.length?w(b||(b=k`

${0}

`),j("Media")):"",this._media,this._token,this._notes.length?w(v||(v=k`

${0}

`),j("Notes")):"",this._notes.map(e=>w(y||(y=k` `),e)),this._citations.length?w(_||(_=k`

${0}

`),j("Sources")):"",this._citations)):w(m||(m=k`Loading...`))}static get styles(){return[S]}constructor(){super(),this._family={},this._father={},this._mother={}}static get properties(){return{gramps_id:{type:String},father:{type:Boolean},mother:{type:Boolean},siblings:{type:Boolean},_family:{type:Object},_father:{type:Object},_mother:{type:Object},_children:{type:Array},_token:{type:String}}}_get_place_name(e,t){return null!=t.place&&""!=t.place&&e.api&&e.api.places&&(t.place_name=e.api.places[t.place].name),t}_addMimeType(e,t){return e.map((function(e){let a={ref:e};return a.mime=t.api.media[a.ref].mime,a}))}stateChanged(e){this._token=e.api.token}}window.customElements.define("gr-family-element",M); 225 | -------------------------------------------------------------------------------- /gramps_webapp/js/5e5495ec.js: -------------------------------------------------------------------------------- 1 | let t,e,i,s,r,o,a,n,h,c=t=>t;import{c as m,s as p,h as d,t as _,S as u}from"./80126bfe.js";import{P as l}from"./c90d6c40.js";import"./cb2c5929.js";import"./cba53e11.js";import"./4f794f05.js";import"./d880604a.js";class g extends(m(p)(l)){render(){return null==this._source?d(t||(t=c`

Loading ...

`)):d(e||(e=c`

${0}

${0} ${0} ${0}
${0} ${0} ${0} ${0}
`),this._source.title,""==this._source.author?"":d(i||(i=c` ${0} ${0} `),_("Author"),this._source.author),""==this._source.pubinfo?"":d(s||(s=c` ${0} ${0} `),_("Publication Information"),this._source.pubinfo),""==this._source.repositories?"":d(r||(r=c` ${0} ${0} `),_("Repositories"),this._repositories.join(", ")),this._media.length?d(o||(o=c`

${0}

`),_("Media")):"",this._media,this._token,this._notes.length?d(a||(a=c`

${0}

`),_("Notes")):"",this._notes.map(t=>d(n||(n=c` `),t)),this._citations.length?d(h||(h=c`

${0}

`),_("Citations")):"",this._citations)}static get styles(){return[u]}constructor(){super(),this._media=Array()}static get properties(){return{_source:{type:Object},_gramps_id:{type:String},_token:{type:String},_media:{type:Object}}}firstUpdated(){}_addMimeType(t,e){return t.map((function(t){return t.mime=e.api.media[t.ref].mime,t}))}stateChanged(t){this._token=t.api.token,this._gramps_id=t.app.activeSource,this._source=t.api.sources[this._gramps_id],null!=this._source&&(this._media=this._addMimeType(this._source.media,t),this._notes=this._source.notes,this._citations=Object.values(t.api.citations).filter(t=>t.source==this._gramps_id).map(t=>t.gramps_id),this._repositories=this._source.repositories.map(e=>t.api.repositories[e].title))}}window.customElements.define("gr-view-source",g); 2 | -------------------------------------------------------------------------------- /gramps_webapp/js/7d06e270.js: -------------------------------------------------------------------------------- 1 | let e,t,i,s,a,p,n,r,o,h,l,d,c=e=>e;import{c as m,s as _,h as g,l as $,t as b,m as v,S as f}from"./80126bfe.js";import{P as y}from"./c90d6c40.js";import"./cb2c5929.js";import"./4582424e.js";import"./cba53e11.js";import"./4f794f05.js";import"./e6ec0820.js";import"./fe5344c8.js";import"./580fa394.js";import"./d880604a.js";class u extends(m(_)(y)){render(){return null==this._person?g(e||(e=c`

Loading ...

`)):g(t||(t=c`
${0}

${0} ${0}

${0} ${0}

${0} ${0}

${0} ${0} ${0}
${0}${0}
${0} ${0}
${0}
${0}
${0}
${0}
`),this._media&&this._media.length?g(i||(i=c`
`),this._token,this._media[0].ref,this._media[0].mime,this._media[0].rect):"",this._person.name_given,this._person.name_surname,this._person.birthdate?g(s||(s=c` ${0} ${0} `),$,this._person.birthdate):"",this._person.birthplace?g(a||(a=c` ${0} ${0} `),b("in"),this._person.birthplace,this._person.birthplace_name):"",this._person.deathdate?g(p||(p=c` ${0} ${0} `),v,this._person.deathdate):"",this._person.deathplace?g(n||(n=c` ${0} ${0} `),b("in"),this._person.deathplace,this._person.deathplace_name):"",this._selected,this._handleSelected,b("Events"),b("Parents"),b("Families"),b("Media"),this._media.length?g(r||(r=c` `),this._media.length):"",b("Notes"),b("Sources"),0!=this._selected,this._events?g(o||(o=c` `),this._events):"",1!=this._selected,this._parents?g(h||(h=c``),this._parents):"",2!=this._selected,this._person.families?this._person.families.map(e=>g(l||(l=c` `),e)):"",3!=this._selected,this._media,this._token,4!=this._selected,this._notes.map(e=>g(d||(d=c` `),e)),5!=this._selected,this._citations)}static get styles(){return[f]}constructor(){super(),this._selected=0}static get properties(){return{_gramps_id:{type:String},_person:{type:Object},_parents:{type:String},_token:{type:String},_events:{type:Object},_selected:{type:Number}}}_handleSelected(e){this._selected=e.detail.selected,window.location.hash=this._selected}_onHashChange(e){this._selected=e.newURL.split("#")[1]}firstUpdated(){window.addEventListener("hashchange",this._onHashChange),null!=window.location.hash.split("#")[1]&&(this._selected=window.location.hash.split("#")[1])}_get_place_name(e,t){return null!=t.place&&""!=t.place&&e.api&&e.api.places&&(t.place_name=e.api.places[t.place].name),t}_addMimeType(e,t){return e.map((function(e){return e.mime=t.api.media[e.ref].mime,e}))}stateChanged(e){this._token=e.api.token,this._gramps_id=e.app.activePerson,this._person=e.api.people[this._gramps_id],this._hidden=!_.getState().app.wideLayout,null!=this._person&&(this._parents=this._person.parents,this._events=this._person.events.map((function(t){let i=e.api.events[t.gramps_id];return t.role==b("Primary")?i.role="":i.role=t.role,i})),this._events=this._events.map(t=>this._get_place_name(e,t)),""!=this._person.birthplace&&(this._person.birthplace_name=e.api.places[this._person.birthplace].name),""!=this._person.deathplace&&(this._person.deathplace_name=e.api.places[this._person.deathplace].name),this._media=this._addMimeType(this._person.media,e),this._citations=this._person.citations,this._notes=this._person.notes)}}window.customElements.define("gr-view-person",u); 2 | -------------------------------------------------------------------------------- /gramps_webapp/js/7de3acdd.js: -------------------------------------------------------------------------------- 1 | let e,a=e=>e;import{c as t,s as i,h as r,t as n,S as s}from"./80126bfe.js";import{P as d}from"./c90d6c40.js";import"./e6ec0820.js";import"./fe5344c8.js";import"./fec21ef7.js";class p extends(t(i)(d)){render(){return r(e||(e=a`
`),this._events,this._hidden,n("Date"),n("Type"),n("Participants"))}static get styles(){return[s]}constructor(){super(),this._hidden=!1}static get properties(){return{_events:{type:Object},_hidden:{type:Boolean}}}_get_event_participants(e,a){if(null!=a.participants&&null!=a.participants[n("Primary")])var t=a.participants[n("Primary")].map((function(a,t){if("Person"==a.type)return e.api.people[a.gramps_id].name_given+" "+e.api.people[a.gramps_id].name_surname})).join(", ");else t="";if(null!=a.participants&&null!=a.participants[n("Family")])var i=a.participants[n("Family")].map((function(a,t){if("Family"==a.type)return e.api.families[a.gramps_id].father_name+", "+e.api.families[a.gramps_id].mother_name})).join(", ");else i="";return a.primary_participants=t+i,a}stateChanged(e){this._events=Object.values(e.api.events).map(a=>this._get_event_participants(e,a)),this._hidden=!i.getState().app.wideLayout}firstUpdated(){}}window.customElements.define("gr-view-events",p); 2 | -------------------------------------------------------------------------------- /gramps_webapp/js/85f25d14.js: -------------------------------------------------------------------------------- 1 | let t,e=t=>t;import{c as s,s as d,h as i,t as r,S as a}from"./80126bfe.js";import{P as n}from"./c90d6c40.js";class o extends(s(d)(n)){render(){return i(t||(t=e`

${0}

${0}

${0}: ${0}

${0} ${0}
${0} ${0}
${0} ${0}
${0} ${0}
`),r("Home Page"),r("Database overview"),r("Name"),this._dbinfo.name,r("Number of individuals"),this._dbinfo.number_people,r("Number of families"),this._dbinfo.number_families,r("Number of places"),this._dbinfo.number_places,r("Number of events"),this._dbinfo.number_events)}static get styles(){return[a]}static get properties(){return{_dbinfo:{type:Object}}}stateChanged(t){this._dbinfo=t.api.dbinfo}}window.customElements.define("gr-view-dashboard",o); 2 | -------------------------------------------------------------------------------- /gramps_webapp/js/911a2ccc.js: -------------------------------------------------------------------------------- 1 | let t,e,i,a,s,n,p,r,h,m,_,l,o,c=t=>t;import{c as d,s as g,h as $,t as y,S as v}from"./80126bfe.js";import{P as f}from"./c90d6c40.js";import"./cb2c5929.js";import"./4582424e.js";import"./e6ec0820.js";import"./fe5344c8.js";import"./580fa394.js";import"./d880604a.js";class u extends(d(g)(f)){render(){return null==this._event?$(t||(t=c``)):$(e||(e=c`

${0}: ${0} ${0}

${0} ${0} ${0}

${0}

${0} ${0} ${0} ${0}
`),this._event.type,this._participants[y("Primary")]?$(i||(i=c` ${0} `),this._participants[y("Primary")].map((t,e,i)=>this._participantLink(t,e==i.length-1))):"",this._participants[y("Family")]?$(a||(a=c` ${0} `),this._participants[y("Family")].map((t,e,i)=>this._participantLink(t,e==i.length-1))):"",this._event.date?$(s||(s=c` ${0} ${0} `),y("Date"),this._event.date):"",this._event.place?$(n||(n=c` ${0} ${0} `),y("Place"),this._event.place,this._event.place_name):"",Object.keys(this._participants).map(t=>{if(t!=y("Primary")&&t!=y("Family"))return $(p||(p=c` ${0} ${0} `),t,this._participants[t].map((t,e,i)=>this._participantLink(t,e==i.length-1)))}),this._event.description,this._media.length?$(r||(r=c`

${0}

`),y("Media")):"",this._media,this._token,this._notes.length?$(h||(h=c`

${0}

`),y("Notes")):"",this._notes.map(t=>$(m||(m=c` `),t)),this._citations.length?$(_||(_=c`

${0}

`),y("Sources")):"",this._citations)}static get styles(){return[v]}constructor(){super(),this._media=Array()}static get properties(){return{_event:{type:Object},_handle:{type:String},_token:{type:String},_media:{type:Object}}}firstUpdated(){}_participantLink(t,e){return null==t?"":"Person"==t.type?$(l||(l=c` ${0} ${0}${0} `),t.person.gramps_id,t.person.name_given,t.person.name_surname,e?"":", "):"Family"==t.type?$(o||(o=c` ${0} ${0} ${0}${0} `),t.family.father_id,t.family.father_name,y("and"),t.family.mother_id,t.family.mother_name,e?"":", "):void 0}_addMimeType(t,e){return t.map((function(t){return t.mime=e.api.media[t.ref].mime,t}))}stateChanged(t){this._token=t.api.token,this._gramps_id=t.app.activeEvent,this._event=t.api.events[this._gramps_id],null!=this._event&&(""!=this._event.place&&null!=t.api.places[this._event.place]&&(this._event.place_name=t.api.places[this._event.place].name),this._media=this._addMimeType(this._event.media,t),this._citations=this._event.citations,this._notes=this._event.notes,this._participants=Object.assign({},this._event.participants),Object.keys(this._participants).map(e=>{this._participants[e]=this._participants[e].map((function(e){return"Person"==e.type?{type:e.type,person:t.api.people[e.gramps_id]}:"Family"==e.type?{type:e.type,family:t.api.families[e.gramps_id]}:void 0}))}))}}window.customElements.define("gr-view-event",u); 2 | -------------------------------------------------------------------------------- /gramps_webapp/js/b31c52e2.js: -------------------------------------------------------------------------------- 1 | let e,a=e=>e;import{c as i,s as t,h as d,t as r,S as n}from"./80126bfe.js";import{P as s}from"./c90d6c40.js";import"./e6ec0820.js";import"./fe5344c8.js";import"./fec21ef7.js";class m extends(i(t)(s)){render(){return d(e||(e=a`
`),this._people,this._hidden,r("Given name"),r("Surname"),this._hidden,r("Birth Date"),this._hidden,r("Death Date"))}static get styles(){return[n]}constructor(){super(),this._hidden=!1}static get properties(){return{_people:{type:Object},_hidden:{type:Boolean}}}stateChanged(e){this._people=Object.values(e.api.people),this._hidden=!e.app.wideLayout}firstUpdated(){}}window.customElements.define("gr-view-people",m); 2 | -------------------------------------------------------------------------------- /gramps_webapp/js/b64e72b8.js: -------------------------------------------------------------------------------- 1 | let e,t=e=>e;import{c as a,s as i,h as r,t as s,R as d,S as n}from"./80126bfe.js";import{P as m}from"./c90d6c40.js";import"./e6ec0820.js";import"./fe5344c8.js";import"./fec21ef7.js";class o extends(a(i)(m)){render(){return r(e||(e=t`
`),this._sources,this._hidden,s("Name"),s("Author"),this._hidden,d)}static get styles(){return[n]}constructor(){super(),this._hidden=!1}static get properties(){return{_sources:{type:Object},_hidden:{type:Boolean}}}stateChanged(e){this._sources=Object.values(e.api.sources),this._sources=this._sources.map((function(e){return e.has_attachment=!1,(e.media.length>0||e.notes.length>0)&&(e.has_attachment=!0),e})),this._hidden=!i.getState().app.wideLayout}firstUpdated(){}}window.customElements.define("gr-view-sources",o); 2 | -------------------------------------------------------------------------------- /gramps_webapp/js/c90d6c40.js: -------------------------------------------------------------------------------- 1 | import{L as e}from"./80126bfe.js";class t extends e{shouldUpdate(){return this.active}static get properties(){return{active:{type:Boolean}}}}export{t as P}; 2 | -------------------------------------------------------------------------------- /gramps_webapp/js/cb2c5929.js: -------------------------------------------------------------------------------- 1 | let t,e,i,s,h,r=t=>t;import{L as n,h as a,f as l,g as $,S as o}from"./80126bfe.js";window.customElements.define("gr-img-element",class extends n{render(){if(this.mime.startsWith("image/")||""==this.mime||"application/pdf"==this.mime)if(null==this.rect)n=a(e||(e=r` `),"",this.handle,this.size,this.token,"",this.handle,1.5*this.size,this.token,"",this.handle,2*this.size,this.token,"",this.handle,2*this.size,this.token,this.circle?"50%":"0",this._errorHandler);else n=a(i||(i=r` ${0} `),"",this.handle,this.size,this.rect[0],this.rect[1],this.rect[2],this.rect[3],this.token,"",this.handle,1.5*this.size,this.rect[0],this.rect[1],this.rect[2],this.rect[3],this.token,"",this.handle,2*this.size,this.rect[0],this.rect[1],this.rect[2],this.rect[3],this.token,"",this.handle,2*this.size,this.rect[0],this.rect[1],this.rect[2],this.rect[3],this.token,this.circle?"50%":"0",this._errorHandler,this.link?a(s||(s=r``)):"");else var n=a(t||(t=r`
${0}
`),this.size,this.size,"application/pdf"==this.mime?l:$);return this.nolink?n:a(h||(h=r` ${0} `),this._lightbox_handle,n)}static get styles(){return[o]}constructor(){super(),this.nolink=!1,this.handles=Array(),this.mime=""}_errorHandler(t){this.dispatchEvent(new CustomEvent("media-load-error",{bubbles:!0,composed:!0}))}_lightbox_handle(){this.dispatchEvent(new CustomEvent("media-selected",{bubbles:!0,composed:!0,detail:{selected:this.handle,media:this.handles}})),this.dispatchEvent(new CustomEvent("lightbox-opened-changed",{bubbles:!0,composed:!0,detail:{opened:!0}}))}static get properties(){return{handle:{type:String},token:{type:String},handles:{type:Object},size:{type:Number},rect:{type:Array},circle:{type:Boolean},square:{type:Boolean},nolink:{type:Boolean},mime:{type:String}}}}); 2 | -------------------------------------------------------------------------------- /gramps_webapp/js/cba53e11.js: -------------------------------------------------------------------------------- 1 | import{i as e,j as s,k as i}from"./80126bfe.js";const r={observers:["_focusedChanged(receivedFocusFromKeyboard)"],_focusedChanged:function(e){e&&this.ensureRipple(),this.hasRipple()&&(this._ripple.holdDown=e)},_createRipple:function(){var s=e._createRipple();return s.id="ink",s.setAttribute("center",""),s.classList.add("circle"),s}},t=[s,i,e,r];export{t as P,r as a}; 2 | -------------------------------------------------------------------------------- /gramps_webapp/js/d880604a.js: -------------------------------------------------------------------------------- 1 | let e,t,i,s=e=>e;import{L as r,h as m,S as a}from"./80126bfe.js";import"./cb2c5929.js";window.customElements.define("gr-gallery-element",class extends r{render(){return this.images.length?m(t||(t=s` ${0}
`),this.images.map((e,t,r)=>m(i||(i=s`
`),e.ref,this.token,e.mime,this.images.map(e=>e.ref),e.rect,r[t+1]?r[t+1].ref:"",r[t-1]?r[t-1].ref:""))):m(e||(e=s``))}static get styles(){return[a]}static get properties(){return{images:{type:Object},token:{type:String}}}}); 2 | -------------------------------------------------------------------------------- /gramps_webapp/js/e9e76f9e.js: -------------------------------------------------------------------------------- 1 | let e,t,r,i,a,s,n,o,l,d=e=>e;import{c as p,s as c,L as h,h as g,S as u,a as m,P as b,b as f,I as v,d as _,e as y,t as k}from"./80126bfe.js";import{P as x}from"./c90d6c40.js";import"./cb2c5929.js";import"./4582424e.js";import{P as w,a as P}from"./cba53e11.js";class $ extends(p(c)(h)){render(){return g(e||(e=d`
${0} ${0}
`),2**(this.depth-1)*100,this._people.map((e,a)=>a>this.depth-1?"":g(t||(t=d` ${0} `),e.map((e,t)=>Object.keys(e).length?g(r||(r=d`
${0}
`),230*a,100*(2**(this.depth-a-1)*(t+.5)-.5),e,this._personSelected,this._token,0==a?"":g(i||(i=d`
`),1===e.gender?"male":"female",1===e.gender?45:100*-(2**(this.depth-a-2))+45,2**(this.depth-a-2)*100,1===e.gender?"male":"female",1===e.gender?45:100*-(2**(this.depth-a-2))+45,2**(this.depth-a-2)*100)):""))),this._children.map((e,t)=>Object.keys(e).length?g(a||(a=d` `),e.families.length?"font-weight:bold;":"font-weight:normal;",100*(2**(this.depth-0-1)*.5-.5+1)+20*t,()=>this._selectPerson(e.gramps_id),e.name_given):""))}static get styles(){return[u]}static get properties(){return{_people:{type:Array},_children:{type:Array},_token:{type:String},depth:{type:Number}}}stateChanged(e){this._token=e.api.token,this._people=this._getTree(e,e.app.activePerson,6),this._children=this._getChildren(e,e.app.activePerson)}_personSelected(e){c.dispatch(m(e.detail.gramps_id))}_selectPerson(e){c.dispatch(m(e))}_getTree(e,t,r){var i=[];if(i.push([this._getPerson(e,t)]),1==r)return i;if(i.push(this._getParents(e,t)),2==r)return i;for(var a=3;a<=r;a++)i.push(i.slice(-1)[0].map(t=>this._getParents(e,t.gramps_id)).flat());return i}_getPerson(e,t){return null==t?{}:e.api.people[t]}_getParents(e,t){if(null==t)return[{},{}];const r=e.api.people[t];if(""==r.parents)return[{},{}];const i=e.api.families[r.parents];if(i.father_id)var a=this._getPerson(e,i.father_id);else a={};if(i.mother_id)var s=this._getPerson(e,i.mother_id);else s={};return[a,s]}_getChildren(e,t){if(null==t)return[];const r=e.api.people[t];if(r.families==[])return[];var i=r.families.flatMap(t=>e.api.families[t].children);return i=i.map(t=>e.api.people[t])}}window.customElements.define("gr-pedigree-element",$);const X={properties:{value:{type:Number,value:0,notify:!0,reflectToAttribute:!0},min:{type:Number,value:0,notify:!0},max:{type:Number,value:100,notify:!0},step:{type:Number,value:1,notify:!0},ratio:{type:Number,value:0,readOnly:!0,notify:!0}},observers:["_update(value, min, max, step)"],_calcRatio:function(e){return(this._clampValue(e)-this.min)/(this.max-this.min)},_clampValue:function(e){return Math.min(this.max,Math.max(this.min,this._calcStep(e)))},_calcStep:function(e){if(e=parseFloat(e),!this.step)return e;var t=Math.round((e-this.min)/this.step);return this.step<1?t/(1/this.step)+this.min:t*this.step+this.min},_validateValue:function(){var e=this._clampValue(this.value);return this.value=this.oldValue=isNaN(e)?this.oldValue:e,this.value!==e},_update:function(){this._validateValue(),this._setRatio(100*this._calcRatio(this.value))}};b({_template:f(s||(s=d` 2 | 160 | 161 |
162 |
163 |
164 |
165 | `)),is:"paper-progress",behaviors:[X],properties:{secondaryProgress:{type:Number,value:0},secondaryRatio:{type:Number,value:0,readOnly:!0},indeterminate:{type:Boolean,value:!1,observer:"_toggleIndeterminate"},disabled:{type:Boolean,value:!1,reflectToAttribute:!0,observer:"_disabledChanged"}},observers:["_progressChanged(secondaryProgress, value, min, max, indeterminate)"],hostAttributes:{role:"progressbar"},_toggleIndeterminate:function(e){this.toggleClass("indeterminate",e,this.$.primaryProgress)},_transformProgress:function(e,t){var r="scaleX("+t/100+")";e.style.transform=e.style.webkitTransform=r},_mainRatioChanged:function(e){this._transformProgress(this.$.primaryProgress,e)},_progressChanged:function(e,t,r,i,a){e=this._clampValue(e),t=this._clampValue(t);var s=100*this._calcRatio(e),n=100*this._calcRatio(t);this._setSecondaryRatio(s),this._transformProgress(this.$.secondaryProgress,s),this._transformProgress(this.$.primaryProgress,n),this.secondaryProgress=e,a?this.removeAttribute("aria-valuenow"):this.setAttribute("aria-valuenow",t),this.setAttribute("aria-valuemin",r),this.setAttribute("aria-valuemax",i)},_disabledChanged:function(e){this.setAttribute("aria-disabled",e?"true":"false")},_hideSecondaryProgress:function(e){return 0===e}});const K=f(n||(n=d` 166 | 430 | 431 |
432 |
433 | 435 |
436 | 437 | 444 | 445 |
446 |
447 |
448 |
449 | 450 | 454 | `));K.setAttribute("strip-whitespace",""),b({_template:K,is:"paper-slider",behaviors:[v,_,w,X],properties:{value:{type:Number,value:0},snaps:{type:Boolean,value:!1,notify:!0},pin:{type:Boolean,value:!1,notify:!0},secondaryProgress:{type:Number,value:0,notify:!0,observer:"_secondaryProgressChanged"},editable:{type:Boolean,value:!1},immediateValue:{type:Number,value:0,readOnly:!0,notify:!0},maxMarkers:{type:Number,value:0,notify:!0},expand:{type:Boolean,value:!1,readOnly:!0},ignoreBarTouch:{type:Boolean,value:!1},dragging:{type:Boolean,value:!1,readOnly:!0,notify:!0},transiting:{type:Boolean,value:!1,readOnly:!0},markers:{type:Array,readOnly:!0,value:function(){return[]}}},observers:["_updateKnob(value, min, max, snaps, step)","_valueChanged(value)","_immediateValueChanged(immediateValue)","_updateMarkers(maxMarkers, min, max, snaps)"],hostAttributes:{role:"slider",tabindex:0},keyBindings:{left:"_leftKey",right:"_rightKey","down pagedown home":"_decrementKey","up pageup end":"_incrementKey"},ready:function(){this.ignoreBarTouch&&y(this.$.sliderBar,"auto")},increment:function(){this.value=this._clampValue(this.value+this.step)},decrement:function(){this.value=this._clampValue(this.value-this.step)},_updateKnob:function(e,t,r,i,a){this.setAttribute("aria-valuemin",t),this.setAttribute("aria-valuemax",r),this.setAttribute("aria-valuenow",e),this._positionKnob(100*this._calcRatio(e))},_valueChanged:function(){this.fire("value-change",{composed:!0})},_immediateValueChanged:function(){this.dragging?this.fire("immediate-value-change",{composed:!0}):this.value=this.immediateValue},_secondaryProgressChanged:function(){this.secondaryProgress=this._clampValue(this.secondaryProgress)},_expandKnob:function(){this._setExpand(!0)},_resetKnob:function(){this.cancelDebouncer("expandKnob"),this._setExpand(!1)},_positionKnob:function(e){this._setImmediateValue(this._calcStep(this._calcKnobPosition(e))),this._setRatio(100*this._calcRatio(this.immediateValue)),this.$.sliderKnob.style.left=this.ratio+"%",this.dragging&&(this._knobstartx=this.ratio*this._w/100,this.translate3d(0,0,0,this.$.sliderKnob))},_calcKnobPosition:function(e){return(this.max-this.min)*e/100+this.min},_onTrack:function(e){switch(e.stopPropagation(),e.detail.state){case"start":this._trackStart(e);break;case"track":this._trackX(e);break;case"end":this._trackEnd()}},_trackStart:function(e){this._setTransiting(!1),this._w=this.$.sliderBar.offsetWidth,this._x=this.ratio*this._w/100,this._startx=this._x,this._knobstartx=this._startx,this._minx=-this._startx,this._maxx=this._w-this._startx,this.$.sliderKnob.classList.add("dragging"),this._setDragging(!0)},_trackX:function(e){this.dragging||this._trackStart(e);var t=this._isRTL?-1:1,r=Math.min(this._maxx,Math.max(this._minx,e.detail.dx*t));this._x=this._startx+r;var i=this._calcStep(this._calcKnobPosition(this._x/this._w*100));this._setImmediateValue(i);var a=this._calcRatio(this.immediateValue)*this._w-this._knobstartx;this.translate3d(a+"px",0,0,this.$.sliderKnob)},_trackEnd:function(){var e=this.$.sliderKnob.style;this.$.sliderKnob.classList.remove("dragging"),this._setDragging(!1),this._resetKnob(),this.value=this.immediateValue,e.transform=e.webkitTransform="",this.fire("change",{composed:!0})},_knobdown:function(e){this._expandKnob(),e.preventDefault(),this.focus()},_bartrack:function(e){this._allowBarEvent(e)&&this._onTrack(e)},_barclick:function(e){this._w=this.$.sliderBar.offsetWidth;var t=this.$.sliderBar.getBoundingClientRect(),r=(e.detail.x-t.left)/this._w*100;this._isRTL&&(r=100-r);var i=this.ratio;this._setTransiting(!0),this._positionKnob(r),i===this.ratio&&this._setTransiting(!1),this.async((function(){this.fire("change",{composed:!0})})),e.preventDefault(),this.focus()},_bardown:function(e){this._allowBarEvent(e)&&(this.debounce("expandKnob",this._expandKnob,60),this._barclick(e))},_knobTransitionEnd:function(e){e.target===this.$.sliderKnob&&this._setTransiting(!1)},_updateMarkers:function(e,t,r,i){i||this._setMarkers([]);var a=Math.round((r-t)/this.step);a>e&&(a=e),(a<0||!isFinite(a))&&(a=0),this._setMarkers(new Array(a))},_mergeClasses:function(e){return Object.keys(e).filter((function(t){return e[t]})).join(" ")},_getClassNames:function(){return this._mergeClasses({disabled:this.disabled,pin:this.pin,snaps:this.snaps,ring:this.immediateValue<=this.min,expand:this.expand,dragging:this.dragging,transiting:this.transiting,editable:this.editable})},_allowBarEvent:function(e){return!this.ignoreBarTouch||e.detail.sourceEvent instanceof MouseEvent},get _isRTL(){return void 0===this.__isRTL&&(this.__isRTL="rtl"===window.getComputedStyle(this).direction),this.__isRTL},_leftKey:function(e){this._isRTL?this._incrementKey(e):this._decrementKey(e)},_rightKey:function(e){this._isRTL?this._decrementKey(e):this._incrementKey(e)},_incrementKey:function(e){this.disabled||("end"===e.detail.key?this.value=this.max:this.increment(),this.fire("change"),e.preventDefault())},_decrementKey:function(e){this.disabled||("home"===e.detail.key?this.value=this.min:this.decrement(),this.fire("change"),e.preventDefault())},_changeValue:function(e){this.value=e.target.value,this.fire("change",{composed:!0})},_inputKeyDown:function(e){e.stopPropagation()},_createRipple:function(){return this._rippleContainer=this.$.sliderKnob,P._createRipple.call(this)},_focusedChanged:function(e){e&&this.ensureRipple(),this.hasRipple()&&(this._ripple.style.display=e?"":"none",this._ripple.holdDown=e)}});class T extends(p(c)(x)){render(){return null==this._gramps_id?g(o||(o=d`

Loading ...

`)):g(l||(l=d`
${0}
`),k("Number of generations:"),this._depth,this._updateDepth,this._zoom,this._depth)}static get properties(){return{_gramps_id:{type:String},_depth:{type:Number},_zoom:{type:Number}}}_updateDepth(e){e.detail.value&&(this._depth=e.detail.value)}constructor(){super(),this._depth=4,this._zoom=1}static get styles(){return[u]}getZoom(){let e=(this.shadowRoot.getElementById("pedigree-section").offsetWidth-24)/(230*this._depth*this._zoom)*this._zoom;return e>1?1:e<.2?.2:e}setZoom(){this._zoom=this.getZoom()}_resizeHandler(e){clearTimeout(this._resizeTimer);var t=this;this._resizeTimer=setTimeout((function(){t.setZoom()}),250)}firstUpdated(){window.addEventListener("resize",this._resizeHandler.bind(this)),c.getState().app.wideLayout?this._depth=4:this._depth=3,this.setZoom()}stateChanged(e){this._gramps_id=e.app.activePerson}updated(e){e.has("_depth")&&this.setZoom()}}window.customElements.define("gr-view-tree",T); 455 | -------------------------------------------------------------------------------- /gramps_webapp/js/eb930581.js: -------------------------------------------------------------------------------- 1 | let e,s=e=>e;import{S as t,h as o}from"./80126bfe.js";import{P as r}from"./c90d6c40.js";window.customElements.define("gr-view404",class extends r{static get styles(){return[t]}render(){return o(e||(e=s`

Oops! You hit a 404

The page you're looking for doesn't seem to exist. Head back home and try again?

`))}}); 2 | -------------------------------------------------------------------------------- /gramps_webapp/js/fec21ef7.js: -------------------------------------------------------------------------------- 1 | let e,t,o=e=>e;import{b as a}from"./80126bfe.js";import"./e6ec0820.js";const r=document.createElement("template");r.innerHTML='\n \n',document.head.appendChild(r.content);const i=a(e||(e=o` 2 | 68 | `));document.head.appendChild(i.content);const n=a(t||(t=o` 69 | 211 | `));document.head.appendChild(n.content); 212 | -------------------------------------------------------------------------------- /gramps_webapp/js/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidMStraub/gramps-webapp/1dcb57b7ce38a4aaa67fcd2896409f1111b8f557/gramps_webapp/js/images/favicon.ico -------------------------------------------------------------------------------- /gramps_webapp/js/images/icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidMStraub/gramps-webapp/1dcb57b7ce38a4aaa67fcd2896409f1111b8f557/gramps_webapp/js/images/icon-144x144.png -------------------------------------------------------------------------------- /gramps_webapp/js/images/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidMStraub/gramps-webapp/1dcb57b7ce38a4aaa67fcd2896409f1111b8f557/gramps_webapp/js/images/icon-192x192.png -------------------------------------------------------------------------------- /gramps_webapp/js/images/icon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidMStraub/gramps-webapp/1dcb57b7ce38a4aaa67fcd2896409f1111b8f557/gramps_webapp/js/images/icon-48x48.png -------------------------------------------------------------------------------- /gramps_webapp/js/images/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidMStraub/gramps-webapp/1dcb57b7ce38a4aaa67fcd2896409f1111b8f557/gramps_webapp/js/images/icon-512x512.png -------------------------------------------------------------------------------- /gramps_webapp/js/images/icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidMStraub/gramps-webapp/1dcb57b7ce38a4aaa67fcd2896409f1111b8f557/gramps_webapp/js/images/icon-72x72.png -------------------------------------------------------------------------------- /gramps_webapp/js/images/icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidMStraub/gramps-webapp/1dcb57b7ce38a4aaa67fcd2896409f1111b8f557/gramps_webapp/js/images/icon-96x96.png -------------------------------------------------------------------------------- /gramps_webapp/js/images/layers-2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidMStraub/gramps-webapp/1dcb57b7ce38a4aaa67fcd2896409f1111b8f557/gramps_webapp/js/images/layers-2x.png -------------------------------------------------------------------------------- /gramps_webapp/js/images/layers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidMStraub/gramps-webapp/1dcb57b7ce38a4aaa67fcd2896409f1111b8f557/gramps_webapp/js/images/layers.png -------------------------------------------------------------------------------- /gramps_webapp/js/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | image/svg+xml -------------------------------------------------------------------------------- /gramps_webapp/js/images/manifest/icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidMStraub/gramps-webapp/1dcb57b7ce38a4aaa67fcd2896409f1111b8f557/gramps_webapp/js/images/manifest/icon-144x144.png -------------------------------------------------------------------------------- /gramps_webapp/js/images/manifest/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidMStraub/gramps-webapp/1dcb57b7ce38a4aaa67fcd2896409f1111b8f557/gramps_webapp/js/images/manifest/icon-192x192.png -------------------------------------------------------------------------------- /gramps_webapp/js/images/manifest/icon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidMStraub/gramps-webapp/1dcb57b7ce38a4aaa67fcd2896409f1111b8f557/gramps_webapp/js/images/manifest/icon-48x48.png -------------------------------------------------------------------------------- /gramps_webapp/js/images/manifest/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidMStraub/gramps-webapp/1dcb57b7ce38a4aaa67fcd2896409f1111b8f557/gramps_webapp/js/images/manifest/icon-512x512.png -------------------------------------------------------------------------------- /gramps_webapp/js/images/manifest/icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidMStraub/gramps-webapp/1dcb57b7ce38a4aaa67fcd2896409f1111b8f557/gramps_webapp/js/images/manifest/icon-72x72.png -------------------------------------------------------------------------------- /gramps_webapp/js/images/manifest/icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidMStraub/gramps-webapp/1dcb57b7ce38a4aaa67fcd2896409f1111b8f557/gramps_webapp/js/images/manifest/icon-96x96.png -------------------------------------------------------------------------------- /gramps_webapp/js/images/marker-icon-2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidMStraub/gramps-webapp/1dcb57b7ce38a4aaa67fcd2896409f1111b8f557/gramps_webapp/js/images/marker-icon-2x.png -------------------------------------------------------------------------------- /gramps_webapp/js/images/marker-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidMStraub/gramps-webapp/1dcb57b7ce38a4aaa67fcd2896409f1111b8f557/gramps_webapp/js/images/marker-icon.png -------------------------------------------------------------------------------- /gramps_webapp/js/images/marker-shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidMStraub/gramps-webapp/1dcb57b7ce38a4aaa67fcd2896409f1111b8f557/gramps_webapp/js/images/marker-shadow.png -------------------------------------------------------------------------------- /gramps_webapp/js/index.html: -------------------------------------------------------------------------------- 1 | Gramps app -------------------------------------------------------------------------------- /gramps_webapp/js/leaflet.css: -------------------------------------------------------------------------------- 1 | /* required styles */ 2 | 3 | .leaflet-pane, 4 | .leaflet-tile, 5 | .leaflet-marker-icon, 6 | .leaflet-marker-shadow, 7 | .leaflet-tile-container, 8 | .leaflet-pane > svg, 9 | .leaflet-pane > canvas, 10 | .leaflet-zoom-box, 11 | .leaflet-image-layer, 12 | .leaflet-layer { 13 | position: absolute; 14 | left: 0; 15 | top: 0; 16 | } 17 | .leaflet-container { 18 | overflow: hidden; 19 | } 20 | .leaflet-tile, 21 | .leaflet-marker-icon, 22 | .leaflet-marker-shadow { 23 | -webkit-user-select: none; 24 | -moz-user-select: none; 25 | user-select: none; 26 | -webkit-user-drag: none; 27 | } 28 | /* Prevents IE11 from highlighting tiles in blue */ 29 | .leaflet-tile::selection { 30 | background: transparent; 31 | } 32 | /* Safari renders non-retina tile on retina better with this, but Chrome is worse */ 33 | .leaflet-safari .leaflet-tile { 34 | image-rendering: -webkit-optimize-contrast; 35 | } 36 | /* hack that prevents hw layers "stretching" when loading new tiles */ 37 | .leaflet-safari .leaflet-tile-container { 38 | width: 1600px; 39 | height: 1600px; 40 | -webkit-transform-origin: 0 0; 41 | } 42 | .leaflet-marker-icon, 43 | .leaflet-marker-shadow { 44 | display: block; 45 | } 46 | /* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */ 47 | /* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */ 48 | .leaflet-container .leaflet-overlay-pane svg, 49 | .leaflet-container .leaflet-marker-pane img, 50 | .leaflet-container .leaflet-shadow-pane img, 51 | .leaflet-container .leaflet-tile-pane img, 52 | .leaflet-container img.leaflet-image-layer, 53 | .leaflet-container .leaflet-tile { 54 | max-width: none !important; 55 | max-height: none !important; 56 | } 57 | 58 | .leaflet-container.leaflet-touch-zoom { 59 | -ms-touch-action: pan-x pan-y; 60 | touch-action: pan-x pan-y; 61 | } 62 | .leaflet-container.leaflet-touch-drag { 63 | -ms-touch-action: pinch-zoom; 64 | /* Fallback for FF which doesn't support pinch-zoom */ 65 | touch-action: none; 66 | touch-action: pinch-zoom; 67 | } 68 | .leaflet-container.leaflet-touch-drag.leaflet-touch-zoom { 69 | -ms-touch-action: none; 70 | touch-action: none; 71 | } 72 | .leaflet-container { 73 | -webkit-tap-highlight-color: transparent; 74 | } 75 | .leaflet-container a { 76 | -webkit-tap-highlight-color: rgba(51, 181, 229, 0.4); 77 | } 78 | .leaflet-tile { 79 | filter: inherit; 80 | visibility: hidden; 81 | } 82 | .leaflet-tile-loaded { 83 | visibility: inherit; 84 | } 85 | .leaflet-zoom-box { 86 | width: 0; 87 | height: 0; 88 | -moz-box-sizing: border-box; 89 | box-sizing: border-box; 90 | z-index: 800; 91 | } 92 | /* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */ 93 | .leaflet-overlay-pane svg { 94 | -moz-user-select: none; 95 | } 96 | 97 | .leaflet-pane { z-index: 400; } 98 | 99 | .leaflet-tile-pane { z-index: 200; } 100 | .leaflet-overlay-pane { z-index: 400; } 101 | .leaflet-shadow-pane { z-index: 500; } 102 | .leaflet-marker-pane { z-index: 600; } 103 | .leaflet-tooltip-pane { z-index: 650; } 104 | .leaflet-popup-pane { z-index: 700; } 105 | 106 | .leaflet-map-pane canvas { z-index: 100; } 107 | .leaflet-map-pane svg { z-index: 200; } 108 | 109 | .leaflet-vml-shape { 110 | width: 1px; 111 | height: 1px; 112 | } 113 | .lvml { 114 | behavior: url(#default#VML); 115 | display: inline-block; 116 | position: absolute; 117 | } 118 | 119 | 120 | /* control positioning */ 121 | 122 | .leaflet-control { 123 | position: relative; 124 | z-index: 800; 125 | pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */ 126 | pointer-events: auto; 127 | } 128 | .leaflet-top, 129 | .leaflet-bottom { 130 | position: absolute; 131 | z-index: 1000; 132 | pointer-events: none; 133 | } 134 | .leaflet-top { 135 | top: 0; 136 | } 137 | .leaflet-right { 138 | right: 0; 139 | } 140 | .leaflet-bottom { 141 | bottom: 0; 142 | } 143 | .leaflet-left { 144 | left: 0; 145 | } 146 | .leaflet-control { 147 | float: left; 148 | clear: both; 149 | } 150 | .leaflet-right .leaflet-control { 151 | float: right; 152 | } 153 | .leaflet-top .leaflet-control { 154 | margin-top: 10px; 155 | } 156 | .leaflet-bottom .leaflet-control { 157 | margin-bottom: 10px; 158 | } 159 | .leaflet-left .leaflet-control { 160 | margin-left: 10px; 161 | } 162 | .leaflet-right .leaflet-control { 163 | margin-right: 10px; 164 | } 165 | 166 | 167 | /* zoom and fade animations */ 168 | 169 | .leaflet-fade-anim .leaflet-tile { 170 | will-change: opacity; 171 | } 172 | .leaflet-fade-anim .leaflet-popup { 173 | opacity: 0; 174 | -webkit-transition: opacity 0.2s linear; 175 | -moz-transition: opacity 0.2s linear; 176 | transition: opacity 0.2s linear; 177 | } 178 | .leaflet-fade-anim .leaflet-map-pane .leaflet-popup { 179 | opacity: 1; 180 | } 181 | .leaflet-zoom-animated { 182 | -webkit-transform-origin: 0 0; 183 | -ms-transform-origin: 0 0; 184 | transform-origin: 0 0; 185 | } 186 | .leaflet-zoom-anim .leaflet-zoom-animated { 187 | will-change: transform; 188 | } 189 | .leaflet-zoom-anim .leaflet-zoom-animated { 190 | -webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1); 191 | -moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1); 192 | transition: transform 0.25s cubic-bezier(0,0,0.25,1); 193 | } 194 | .leaflet-zoom-anim .leaflet-tile, 195 | .leaflet-pan-anim .leaflet-tile { 196 | -webkit-transition: none; 197 | -moz-transition: none; 198 | transition: none; 199 | } 200 | 201 | .leaflet-zoom-anim .leaflet-zoom-hide { 202 | visibility: hidden; 203 | } 204 | 205 | 206 | /* cursors */ 207 | 208 | .leaflet-interactive { 209 | cursor: pointer; 210 | } 211 | .leaflet-grab { 212 | cursor: -webkit-grab; 213 | cursor: -moz-grab; 214 | cursor: grab; 215 | } 216 | .leaflet-crosshair, 217 | .leaflet-crosshair .leaflet-interactive { 218 | cursor: crosshair; 219 | } 220 | .leaflet-popup-pane, 221 | .leaflet-control { 222 | cursor: auto; 223 | } 224 | .leaflet-dragging .leaflet-grab, 225 | .leaflet-dragging .leaflet-grab .leaflet-interactive, 226 | .leaflet-dragging .leaflet-marker-draggable { 227 | cursor: move; 228 | cursor: -webkit-grabbing; 229 | cursor: -moz-grabbing; 230 | cursor: grabbing; 231 | } 232 | 233 | /* marker & overlays interactivity */ 234 | .leaflet-marker-icon, 235 | .leaflet-marker-shadow, 236 | .leaflet-image-layer, 237 | .leaflet-pane > svg path, 238 | .leaflet-tile-container { 239 | pointer-events: none; 240 | } 241 | 242 | .leaflet-marker-icon.leaflet-interactive, 243 | .leaflet-image-layer.leaflet-interactive, 244 | .leaflet-pane > svg path.leaflet-interactive, 245 | svg.leaflet-image-layer.leaflet-interactive path { 246 | pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */ 247 | pointer-events: auto; 248 | } 249 | 250 | /* visual tweaks */ 251 | 252 | .leaflet-container { 253 | background: #ddd; 254 | outline: 0; 255 | } 256 | .leaflet-container a { 257 | color: #0078A8; 258 | } 259 | .leaflet-container a.leaflet-active { 260 | outline: 2px solid orange; 261 | } 262 | .leaflet-zoom-box { 263 | border: 2px dotted #38f; 264 | background: rgba(255,255,255,0.5); 265 | } 266 | 267 | 268 | /* general typography */ 269 | .leaflet-container { 270 | font: 12px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif; 271 | } 272 | 273 | 274 | /* general toolbar styles */ 275 | 276 | .leaflet-bar { 277 | box-shadow: 0 1px 5px rgba(0,0,0,0.65); 278 | border-radius: 4px; 279 | } 280 | .leaflet-bar a, 281 | .leaflet-bar a:hover { 282 | background-color: #fff; 283 | border-bottom: 1px solid #ccc; 284 | width: 26px; 285 | height: 26px; 286 | line-height: 26px; 287 | display: block; 288 | text-align: center; 289 | text-decoration: none; 290 | color: black; 291 | } 292 | .leaflet-bar a, 293 | .leaflet-control-layers-toggle { 294 | background-position: 50% 50%; 295 | background-repeat: no-repeat; 296 | display: block; 297 | } 298 | .leaflet-bar a:hover { 299 | background-color: #f4f4f4; 300 | } 301 | .leaflet-bar a:first-child { 302 | border-top-left-radius: 4px; 303 | border-top-right-radius: 4px; 304 | } 305 | .leaflet-bar a:last-child { 306 | border-bottom-left-radius: 4px; 307 | border-bottom-right-radius: 4px; 308 | border-bottom: none; 309 | } 310 | .leaflet-bar a.leaflet-disabled { 311 | cursor: default; 312 | background-color: #f4f4f4; 313 | color: #bbb; 314 | } 315 | 316 | .leaflet-touch .leaflet-bar a { 317 | width: 30px; 318 | height: 30px; 319 | line-height: 30px; 320 | } 321 | .leaflet-touch .leaflet-bar a:first-child { 322 | border-top-left-radius: 2px; 323 | border-top-right-radius: 2px; 324 | } 325 | .leaflet-touch .leaflet-bar a:last-child { 326 | border-bottom-left-radius: 2px; 327 | border-bottom-right-radius: 2px; 328 | } 329 | 330 | /* zoom control */ 331 | 332 | .leaflet-control-zoom-in, 333 | .leaflet-control-zoom-out { 334 | font: bold 18px 'Lucida Console', Monaco, monospace; 335 | text-indent: 1px; 336 | } 337 | 338 | .leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out { 339 | font-size: 22px; 340 | } 341 | 342 | 343 | /* layers control */ 344 | 345 | .leaflet-control-layers { 346 | box-shadow: 0 1px 5px rgba(0,0,0,0.4); 347 | background: #fff; 348 | border-radius: 5px; 349 | } 350 | .leaflet-control-layers-toggle { 351 | background-image: url(images/layers.png); 352 | width: 36px; 353 | height: 36px; 354 | } 355 | .leaflet-retina .leaflet-control-layers-toggle { 356 | background-image: url(images/layers-2x.png); 357 | background-size: 26px 26px; 358 | } 359 | .leaflet-touch .leaflet-control-layers-toggle { 360 | width: 44px; 361 | height: 44px; 362 | } 363 | .leaflet-control-layers .leaflet-control-layers-list, 364 | .leaflet-control-layers-expanded .leaflet-control-layers-toggle { 365 | display: none; 366 | } 367 | .leaflet-control-layers-expanded .leaflet-control-layers-list { 368 | display: block; 369 | position: relative; 370 | } 371 | .leaflet-control-layers-expanded { 372 | padding: 6px 10px 6px 6px; 373 | color: #333; 374 | background: #fff; 375 | } 376 | .leaflet-control-layers-scrollbar { 377 | overflow-y: scroll; 378 | overflow-x: hidden; 379 | padding-right: 5px; 380 | } 381 | .leaflet-control-layers-selector { 382 | margin-top: 2px; 383 | position: relative; 384 | top: 1px; 385 | } 386 | .leaflet-control-layers label { 387 | display: block; 388 | } 389 | .leaflet-control-layers-separator { 390 | height: 0; 391 | border-top: 1px solid #ddd; 392 | margin: 5px -10px 5px -6px; 393 | } 394 | 395 | /* Default icon URLs */ 396 | .leaflet-default-icon-path { 397 | background-image: url(images/marker-icon.png); 398 | } 399 | 400 | 401 | /* attribution and scale controls */ 402 | 403 | .leaflet-container .leaflet-control-attribution { 404 | background: #fff; 405 | background: rgba(255, 255, 255, 0.7); 406 | margin: 0; 407 | } 408 | .leaflet-control-attribution, 409 | .leaflet-control-scale-line { 410 | padding: 0 5px; 411 | color: #333; 412 | } 413 | .leaflet-control-attribution a { 414 | text-decoration: none; 415 | } 416 | .leaflet-control-attribution a:hover { 417 | text-decoration: underline; 418 | } 419 | .leaflet-container .leaflet-control-attribution, 420 | .leaflet-container .leaflet-control-scale { 421 | font-size: 11px; 422 | } 423 | .leaflet-left .leaflet-control-scale { 424 | margin-left: 5px; 425 | } 426 | .leaflet-bottom .leaflet-control-scale { 427 | margin-bottom: 5px; 428 | } 429 | .leaflet-control-scale-line { 430 | border: 2px solid #777; 431 | border-top: none; 432 | line-height: 1.1; 433 | padding: 2px 5px 1px; 434 | font-size: 11px; 435 | white-space: nowrap; 436 | overflow: hidden; 437 | -moz-box-sizing: border-box; 438 | box-sizing: border-box; 439 | 440 | background: #fff; 441 | background: rgba(255, 255, 255, 0.5); 442 | } 443 | .leaflet-control-scale-line:not(:first-child) { 444 | border-top: 2px solid #777; 445 | border-bottom: none; 446 | margin-top: -2px; 447 | } 448 | .leaflet-control-scale-line:not(:first-child):not(:last-child) { 449 | border-bottom: 2px solid #777; 450 | } 451 | 452 | .leaflet-touch .leaflet-control-attribution, 453 | .leaflet-touch .leaflet-control-layers, 454 | .leaflet-touch .leaflet-bar { 455 | box-shadow: none; 456 | } 457 | .leaflet-touch .leaflet-control-layers, 458 | .leaflet-touch .leaflet-bar { 459 | border: 2px solid rgba(0,0,0,0.2); 460 | background-clip: padding-box; 461 | } 462 | 463 | 464 | /* popup */ 465 | 466 | .leaflet-popup { 467 | position: absolute; 468 | text-align: center; 469 | margin-bottom: 20px; 470 | } 471 | .leaflet-popup-content-wrapper { 472 | padding: 1px; 473 | text-align: left; 474 | border-radius: 12px; 475 | } 476 | .leaflet-popup-content { 477 | margin: 13px 19px; 478 | line-height: 1.4; 479 | } 480 | .leaflet-popup-content p { 481 | margin: 18px 0; 482 | } 483 | .leaflet-popup-tip-container { 484 | width: 40px; 485 | height: 20px; 486 | position: absolute; 487 | left: 50%; 488 | margin-left: -20px; 489 | overflow: hidden; 490 | pointer-events: none; 491 | } 492 | .leaflet-popup-tip { 493 | width: 17px; 494 | height: 17px; 495 | padding: 1px; 496 | 497 | margin: -10px auto 0; 498 | 499 | -webkit-transform: rotate(45deg); 500 | -moz-transform: rotate(45deg); 501 | -ms-transform: rotate(45deg); 502 | transform: rotate(45deg); 503 | } 504 | .leaflet-popup-content-wrapper, 505 | .leaflet-popup-tip { 506 | background: white; 507 | color: #333; 508 | box-shadow: 0 3px 14px rgba(0,0,0,0.4); 509 | } 510 | .leaflet-container a.leaflet-popup-close-button { 511 | position: absolute; 512 | top: 0; 513 | right: 0; 514 | padding: 4px 4px 0 0; 515 | border: none; 516 | text-align: center; 517 | width: 18px; 518 | height: 14px; 519 | font: 16px/14px Tahoma, Verdana, sans-serif; 520 | color: #c3c3c3; 521 | text-decoration: none; 522 | font-weight: bold; 523 | background: transparent; 524 | } 525 | .leaflet-container a.leaflet-popup-close-button:hover { 526 | color: #999; 527 | } 528 | .leaflet-popup-scrolled { 529 | overflow: auto; 530 | border-bottom: 1px solid #ddd; 531 | border-top: 1px solid #ddd; 532 | } 533 | 534 | .leaflet-oldie .leaflet-popup-content-wrapper { 535 | zoom: 1; 536 | } 537 | .leaflet-oldie .leaflet-popup-tip { 538 | width: 24px; 539 | margin: 0 auto; 540 | 541 | -ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)"; 542 | filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678); 543 | } 544 | .leaflet-oldie .leaflet-popup-tip-container { 545 | margin-top: -1px; 546 | } 547 | 548 | .leaflet-oldie .leaflet-control-zoom, 549 | .leaflet-oldie .leaflet-control-layers, 550 | .leaflet-oldie .leaflet-popup-content-wrapper, 551 | .leaflet-oldie .leaflet-popup-tip { 552 | border: 1px solid #999; 553 | } 554 | 555 | 556 | /* div icon */ 557 | 558 | .leaflet-div-icon { 559 | background: #fff; 560 | border: 1px solid #666; 561 | } 562 | 563 | 564 | /* Tooltip */ 565 | /* Base styles for the element that has a tooltip */ 566 | .leaflet-tooltip { 567 | position: absolute; 568 | padding: 6px; 569 | background-color: #fff; 570 | border: 1px solid #fff; 571 | border-radius: 3px; 572 | color: #222; 573 | white-space: nowrap; 574 | -webkit-user-select: none; 575 | -moz-user-select: none; 576 | -ms-user-select: none; 577 | user-select: none; 578 | pointer-events: none; 579 | box-shadow: 0 1px 3px rgba(0,0,0,0.4); 580 | } 581 | .leaflet-tooltip.leaflet-clickable { 582 | cursor: pointer; 583 | pointer-events: auto; 584 | } 585 | .leaflet-tooltip-top:before, 586 | .leaflet-tooltip-bottom:before, 587 | .leaflet-tooltip-left:before, 588 | .leaflet-tooltip-right:before { 589 | position: absolute; 590 | pointer-events: none; 591 | border: 6px solid transparent; 592 | background: transparent; 593 | content: ""; 594 | } 595 | 596 | /* Directions */ 597 | 598 | .leaflet-tooltip-bottom { 599 | margin-top: 6px; 600 | } 601 | .leaflet-tooltip-top { 602 | margin-top: -6px; 603 | } 604 | .leaflet-tooltip-bottom:before, 605 | .leaflet-tooltip-top:before { 606 | left: 50%; 607 | margin-left: -6px; 608 | } 609 | .leaflet-tooltip-top:before { 610 | bottom: 0; 611 | margin-bottom: -12px; 612 | border-top-color: #fff; 613 | } 614 | .leaflet-tooltip-bottom:before { 615 | top: 0; 616 | margin-top: -12px; 617 | margin-left: -6px; 618 | border-bottom-color: #fff; 619 | } 620 | .leaflet-tooltip-left { 621 | margin-left: -6px; 622 | } 623 | .leaflet-tooltip-right { 624 | margin-left: 6px; 625 | } 626 | .leaflet-tooltip-left:before, 627 | .leaflet-tooltip-right:before { 628 | top: 50%; 629 | margin-top: -6px; 630 | } 631 | .leaflet-tooltip-left:before { 632 | right: 0; 633 | margin-right: -12px; 634 | border-left-color: #fff; 635 | } 636 | .leaflet-tooltip-right:before { 637 | left: 0; 638 | margin-left: -12px; 639 | border-right-color: #fff; 640 | } 641 | -------------------------------------------------------------------------------- /gramps_webapp/js/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Login 12 | 28 | 29 | 30 | 31 | 32 |
33 | 34 |
35 |
36 | 37 | 38 |
39 |
40 |
41 |
42 | 43 | 44 |
45 |
46 |
47 |
48 | 49 |
50 |
51 |
52 | 53 | 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /gramps_webapp/js/sw.js: -------------------------------------------------------------------------------- 1 | if(!self.define){const e=e=>{"require"!==e&&(e+=".js");let r=Promise.resolve();return c[e]||(r=new Promise(async r=>{if("document"in self){const c=document.createElement("script");c.src=e,document.head.appendChild(c),c.onload=r}else importScripts(e),r()})),r.then(()=>{if(!c[e])throw new Error(`Module ${e} didn’t register its module`);return c[e]})},r=(r,c)=>{Promise.all(r.map(e)).then(e=>c(1===e.length?e[0]:e))},c={require:Promise.resolve(r)};self.define=(r,s,i)=>{c[r]||(c[r]=Promise.resolve().then(()=>{let c={};const d={uri:location.origin+r.slice(1)};return Promise.all(s.map(r=>{switch(r){case"exports":return c;case"module":return d;default:return e(r)}})).then(e=>{const r=i(...e);return c.default||(c.default=r),c})}))}}define("./sw.js",["./workbox-a1d34bd3"],(function(e){"use strict";e.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"0e10338e.js",revision:"2366aef5b69d74a45782cbbdcda68371"},{url:"19fdb0d6.js",revision:"91219d9ce0a1eeacf8a1f5a560f84ad0"},{url:"43e752ee.js",revision:"d0305fe11694af2647f8a23b689fa2bf"},{url:"4582424e.js",revision:"81405dc94579ebb0999b0920387a43b1"},{url:"4f794f05.js",revision:"2407deefec6d03f848e1d9c92ea153f3"},{url:"5522257c.js",revision:"e637f1a8299f9f7cc5b25fb411990df3"},{url:"580fa394.js",revision:"1f09ea08bec88847d9509d38dafec874"},{url:"5e5495ec.js",revision:"0ec0eb5cc41239478ea7147f7668cc20"},{url:"7d06e270.js",revision:"885131b1ea2c217bee63c3894e4c72a7"},{url:"7de3acdd.js",revision:"ad3a32579fbda8121ac8d6a1aa82473d"},{url:"80126bfe.js",revision:"786c007bc00b96a72cefe708a427f0c3"},{url:"85f25d14.js",revision:"a7c50004c138d5504a1bc03486e4cd51"},{url:"911a2ccc.js",revision:"55b0e951b5a0c59464a7c2a9c04726c1"},{url:"a088e339.js",revision:"e9620688b42fafa8442c4f8ed041d2b8"},{url:"adb2bcf5.js",revision:"d42543d22696c21f151ee05bdb6d19c5"},{url:"b31c52e2.js",revision:"94544b655d7823793659e03755f5943f"},{url:"b64e72b8.js",revision:"8dd7f64613c9a622cb2c0cf090754b67"},{url:"c90d6c40.js",revision:"94ac7969fffc663fd8087987587d0343"},{url:"cb2c5929.js",revision:"aa5a8e355d99ead62179213c26517918"},{url:"cba53e11.js",revision:"18ff2426f075f06add9262e17e373424"},{url:"d880604a.js",revision:"9e4cb0e1d6f416e44ceba92f64bf24e5"},{url:"e6ec0820.js",revision:"40b15fce6d3f8a373d459e0f552192d9"},{url:"e9e76f9e.js",revision:"0d767eed5e394d16f9bdca6b51a123ce"},{url:"eb930581.js",revision:"9eb404e634bf258e8651da88e71d69c0"},{url:"fe5344c8.js",revision:"0e9d5e9c82ea4440e04ce4859ae738b9"},{url:"fec21ef7.js",revision:"5197eae81715c40fad0eabeb66014e84"},{url:"index.html",revision:"fcdceeafd7e32aedd9eddfe7a7d533f0"},{url:"leaflet.css",revision:"6b7939304e1bc55fac601aabffcc528d"},{url:"login.html",revision:"e9bd78ca777ffc8a2a3d4b6c814efa14"}],{}),e.registerRoute(new e.NavigationRoute(e.createHandlerBoundToURL("/index.html"))),e.registerRoute("polyfills/*.js",new e.CacheFirst,"GET")})); 2 | //# sourceMappingURL=sw.js.map 3 | -------------------------------------------------------------------------------- /gramps_webapp/js/sw.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"sw.js","sources":["../../../../../../../tmp/38261f48b54154a1644f8955adaa60d8/sw.js"],"sourcesContent":["import {registerRoute as workbox_routing_registerRoute} from '/home/david/Dokumente/Code/gramps/frontend/gramps-webapp-frontend/node_modules/workbox-routing/registerRoute.mjs';\nimport {CacheFirst as workbox_strategies_CacheFirst} from '/home/david/Dokumente/Code/gramps/frontend/gramps-webapp-frontend/node_modules/workbox-strategies/CacheFirst.mjs';\nimport {skipWaiting as workbox_core_skipWaiting} from '/home/david/Dokumente/Code/gramps/frontend/gramps-webapp-frontend/node_modules/workbox-core/skipWaiting.mjs';\nimport {clientsClaim as workbox_core_clientsClaim} from '/home/david/Dokumente/Code/gramps/frontend/gramps-webapp-frontend/node_modules/workbox-core/clientsClaim.mjs';\nimport {precacheAndRoute as workbox_precaching_precacheAndRoute} from '/home/david/Dokumente/Code/gramps/frontend/gramps-webapp-frontend/node_modules/workbox-precaching/precacheAndRoute.mjs';\nimport {NavigationRoute as workbox_routing_NavigationRoute} from '/home/david/Dokumente/Code/gramps/frontend/gramps-webapp-frontend/node_modules/workbox-routing/NavigationRoute.mjs';\nimport {createHandlerBoundToURL as workbox_precaching_createHandlerBoundToURL} from '/home/david/Dokumente/Code/gramps/frontend/gramps-webapp-frontend/node_modules/workbox-precaching/createHandlerBoundToURL.mjs';/**\n * Welcome to your Workbox-powered service worker!\n *\n * You'll need to register this file in your web app.\n * See https://goo.gl/nhQhGp\n *\n * The rest of the code is auto-generated. Please don't update this file\n * directly; instead, make changes to your Workbox build configuration\n * and re-run your build process.\n * See https://goo.gl/2aRDsh\n */\n\n\n\n\n\n\n\n\nworkbox_core_skipWaiting();\n\nworkbox_core_clientsClaim();\n\n\n/**\n * The precacheAndRoute() method efficiently caches and responds to\n * requests for URLs in the manifest.\n * See https://goo.gl/S9QRab\n */\nworkbox_precaching_precacheAndRoute([\n {\n \"url\": \"0e10338e.js\",\n \"revision\": \"2366aef5b69d74a45782cbbdcda68371\"\n },\n {\n \"url\": \"19fdb0d6.js\",\n \"revision\": \"91219d9ce0a1eeacf8a1f5a560f84ad0\"\n },\n {\n \"url\": \"43e752ee.js\",\n \"revision\": \"d0305fe11694af2647f8a23b689fa2bf\"\n },\n {\n \"url\": \"4582424e.js\",\n \"revision\": \"81405dc94579ebb0999b0920387a43b1\"\n },\n {\n \"url\": \"4f794f05.js\",\n \"revision\": \"2407deefec6d03f848e1d9c92ea153f3\"\n },\n {\n \"url\": \"5522257c.js\",\n \"revision\": \"e637f1a8299f9f7cc5b25fb411990df3\"\n },\n {\n \"url\": \"580fa394.js\",\n \"revision\": \"1f09ea08bec88847d9509d38dafec874\"\n },\n {\n \"url\": \"5e5495ec.js\",\n \"revision\": \"0ec0eb5cc41239478ea7147f7668cc20\"\n },\n {\n \"url\": \"7d06e270.js\",\n \"revision\": \"885131b1ea2c217bee63c3894e4c72a7\"\n },\n {\n \"url\": \"7de3acdd.js\",\n \"revision\": \"ad3a32579fbda8121ac8d6a1aa82473d\"\n },\n {\n \"url\": \"80126bfe.js\",\n \"revision\": \"786c007bc00b96a72cefe708a427f0c3\"\n },\n {\n \"url\": \"85f25d14.js\",\n \"revision\": \"a7c50004c138d5504a1bc03486e4cd51\"\n },\n {\n \"url\": \"911a2ccc.js\",\n \"revision\": \"55b0e951b5a0c59464a7c2a9c04726c1\"\n },\n {\n \"url\": \"a088e339.js\",\n \"revision\": \"e9620688b42fafa8442c4f8ed041d2b8\"\n },\n {\n \"url\": \"adb2bcf5.js\",\n \"revision\": \"d42543d22696c21f151ee05bdb6d19c5\"\n },\n {\n \"url\": \"b31c52e2.js\",\n \"revision\": \"94544b655d7823793659e03755f5943f\"\n },\n {\n \"url\": \"b64e72b8.js\",\n \"revision\": \"8dd7f64613c9a622cb2c0cf090754b67\"\n },\n {\n \"url\": \"c90d6c40.js\",\n \"revision\": \"94ac7969fffc663fd8087987587d0343\"\n },\n {\n \"url\": \"cb2c5929.js\",\n \"revision\": \"aa5a8e355d99ead62179213c26517918\"\n },\n {\n \"url\": \"cba53e11.js\",\n \"revision\": \"18ff2426f075f06add9262e17e373424\"\n },\n {\n \"url\": \"d880604a.js\",\n \"revision\": \"9e4cb0e1d6f416e44ceba92f64bf24e5\"\n },\n {\n \"url\": \"e6ec0820.js\",\n \"revision\": \"40b15fce6d3f8a373d459e0f552192d9\"\n },\n {\n \"url\": \"e9e76f9e.js\",\n \"revision\": \"0d767eed5e394d16f9bdca6b51a123ce\"\n },\n {\n \"url\": \"eb930581.js\",\n \"revision\": \"9eb404e634bf258e8651da88e71d69c0\"\n },\n {\n \"url\": \"fe5344c8.js\",\n \"revision\": \"0e9d5e9c82ea4440e04ce4859ae738b9\"\n },\n {\n \"url\": \"fec21ef7.js\",\n \"revision\": \"5197eae81715c40fad0eabeb66014e84\"\n },\n {\n \"url\": \"index.html\",\n \"revision\": \"fcdceeafd7e32aedd9eddfe7a7d533f0\"\n },\n {\n \"url\": \"leaflet.css\",\n \"revision\": \"6b7939304e1bc55fac601aabffcc528d\"\n },\n {\n \"url\": \"login.html\",\n \"revision\": \"e9bd78ca777ffc8a2a3d4b6c814efa14\"\n }\n], {});\n\nworkbox_routing_registerRoute(new workbox_routing_NavigationRoute(workbox_precaching_createHandlerBoundToURL(\"/index.html\")));\n\n\nworkbox_routing_registerRoute(\"polyfills/*.js\", new workbox_strategies_CacheFirst(), 'GET');\n\n\n\n\n"],"names":["workbox_routing_NavigationRoute","workbox_precaching_createHandlerBoundToURL","workbox_strategies_CacheFirst"],"mappings":"k1BAmCoC,CAClC,KACS,uBACK,oCAEd,KACS,uBACK,oCAEd,KACS,uBACK,oCAEd,KACS,uBACK,oCAEd,KACS,uBACK,oCAEd,KACS,uBACK,oCAEd,KACS,uBACK,oCAEd,KACS,uBACK,oCAEd,KACS,uBACK,oCAEd,KACS,uBACK,oCAEd,KACS,uBACK,oCAEd,KACS,uBACK,oCAEd,KACS,uBACK,oCAEd,KACS,uBACK,oCAEd,KACS,uBACK,oCAEd,KACS,uBACK,oCAEd,KACS,uBACK,oCAEd,KACS,uBACK,oCAEd,KACS,uBACK,oCAEd,KACS,uBACK,oCAEd,KACS,uBACK,oCAEd,KACS,uBACK,oCAEd,KACS,uBACK,oCAEd,KACS,uBACK,oCAEd,KACS,uBACK,oCAEd,KACS,uBACK,oCAEd,KACS,sBACK,oCAEd,KACS,uBACK,oCAEd,KACS,sBACK,qCAEb,oBAE2B,IAAIA,kBAAgCC,0BAA2C,iCAG/E,iBAAkB,IAAIC,aAAiC"} -------------------------------------------------------------------------------- /gramps_webapp/js/workbox-a1d34bd3.js: -------------------------------------------------------------------------------- 1 | define("./workbox-a1d34bd3.js",["exports"],(function(e){"use strict";try{self["workbox:core:5.1.3"]&&_()}catch(e){}const t=(e,...t)=>{let n=e;return t.length>0&&(n+=" :: "+JSON.stringify(t)),n};class n extends Error{constructor(e,n){super(t(e,n)),this.name=e,this.details=n}}try{self["workbox:routing:5.1.3"]&&_()}catch(e){}const s=e=>e&&"object"==typeof e?e:{handle:e};class r{constructor(e,t,n="GET"){this.handler=s(t),this.match=e,this.method=n}}class i extends r{constructor(e,t,n){super(({url:t})=>{const n=e.exec(t.href);if(n&&(t.origin===location.origin||0===n.index))return n.slice(1)},t,n)}}const o=e=>new URL(String(e),location.href).href.replace(new RegExp("^"+location.origin),"");class c{constructor(){this.t=new Map}get routes(){return this.t}addFetchListener(){self.addEventListener("fetch",e=>{const{request:t}=e,n=this.handleRequest({request:t,event:e});n&&e.respondWith(n)})}addCacheListener(){self.addEventListener("message",e=>{if(e.data&&"CACHE_URLS"===e.data.type){const{payload:t}=e.data,n=Promise.all(t.urlsToCache.map(e=>{"string"==typeof e&&(e=[e]);const t=new Request(...e);return this.handleRequest({request:t})}));e.waitUntil(n),e.ports&&e.ports[0]&&n.then(()=>e.ports[0].postMessage(!0))}})}handleRequest({request:e,event:t}){const n=new URL(e.url,location.href);if(!n.protocol.startsWith("http"))return;const{params:s,route:r}=this.findMatchingRoute({url:n,request:e,event:t});let i,o=r&&r.handler;if(!o&&this.s&&(o=this.s),o){try{i=o.handle({url:n,request:e,event:t,params:s})}catch(e){i=Promise.reject(e)}return i instanceof Promise&&this.i&&(i=i.catch(s=>this.i.handle({url:n,request:e,event:t}))),i}}findMatchingRoute({url:e,request:t,event:n}){const s=this.t.get(t.method)||[];for(const r of s){let s;const i=r.match({url:e,request:t,event:n});if(i)return s=i,(Array.isArray(i)&&0===i.length||i.constructor===Object&&0===Object.keys(i).length||"boolean"==typeof i)&&(s=void 0),{route:r,params:s}}return{}}setDefaultHandler(e){this.s=s(e)}setCatchHandler(e){this.i=s(e)}registerRoute(e){this.t.has(e.method)||this.t.set(e.method,[]),this.t.get(e.method).push(e)}unregisterRoute(e){if(!this.t.has(e.method))throw new n("unregister-route-but-not-found-with-method",{method:e.method});const t=this.t.get(e.method).indexOf(e);if(!(t>-1))throw new n("unregister-route-route-not-registered");this.t.get(e.method).splice(t,1)}}let a;const u=()=>(a||(a=new c,a.addFetchListener(),a.addCacheListener()),a);const h={googleAnalytics:"googleAnalytics",precache:"precache-v2",prefix:"workbox",runtime:"runtime",suffix:"undefined"!=typeof registration?registration.scope:""},l=e=>[h.prefix,e,h.suffix].filter(e=>e&&e.length>0).join("-"),f=e=>e||l(h.precache),w=e=>e||l(h.runtime),p=new Set;const d=(e,t)=>e.filter(e=>t in e),y=async({request:e,mode:t,plugins:n=[]})=>{const s=d(n,"cacheKeyWillBeUsed");let r=e;for(const e of s)r=await e.cacheKeyWillBeUsed.call(e,{mode:t,request:r}),"string"==typeof r&&(r=new Request(r));return r},g=async({cacheName:e,request:t,event:n,matchOptions:s,plugins:r=[]})=>{const i=await self.caches.open(e),o=await y({plugins:r,request:t,mode:"read"});let c=await i.match(o,s);for(const t of r)if("cachedResponseWillBeUsed"in t){const r=t.cachedResponseWillBeUsed;c=await r.call(t,{cacheName:e,event:n,matchOptions:s,cachedResponse:c,request:o})}return c},R=async({cacheName:e,request:t,response:s,event:r,plugins:i=[],matchOptions:c})=>{const a=await y({plugins:i,request:t,mode:"write"});if(!s)throw new n("cache-put-with-no-response",{url:o(a.url)});const u=await(async({request:e,response:t,event:n,plugins:s=[]})=>{let r=t,i=!1;for(const t of s)if("cacheWillUpdate"in t){i=!0;const s=t.cacheWillUpdate;if(r=await s.call(t,{request:e,response:r,event:n}),!r)break}return i||(r=r&&200===r.status?r:void 0),r||null})({event:r,plugins:i,response:s,request:a});if(!u)return;const h=await self.caches.open(e),l=d(i,"cacheDidUpdate"),f=l.length>0?await g({cacheName:e,matchOptions:c,request:a}):null;try{await h.put(a,u)}catch(e){throw"QuotaExceededError"===e.name&&await async function(){for(const e of p)await e()}(),e}for(const t of l)await t.cacheDidUpdate.call(t,{cacheName:e,event:r,oldResponse:f,newResponse:u,request:a})},m=g,q=async({request:e,fetchOptions:t,event:s,plugins:r=[]})=>{if("string"==typeof e&&(e=new Request(e)),s instanceof FetchEvent&&s.preloadResponse){const e=await s.preloadResponse;if(e)return e}const i=d(r,"fetchDidFail"),o=i.length>0?e.clone():null;try{for(const t of r)if("requestWillFetch"in t){const n=t.requestWillFetch,r=e.clone();e=await n.call(t,{request:r,event:s})}}catch(e){throw new n("plugin-error-request-will-fetch",{thrownError:e})}const c=e.clone();try{let n;n="navigate"===e.mode?await fetch(e):await fetch(e,t);for(const e of r)"fetchDidSucceed"in e&&(n=await e.fetchDidSucceed.call(e,{event:s,request:c,response:n}));return n}catch(e){for(const t of i)await t.fetchDidFail.call(t,{error:e,event:s,originalRequest:o.clone(),request:c.clone()});throw e}};try{self["workbox:strategies:5.1.3"]&&_()}catch(e){}let v;async function U(e,t){const n=e.clone(),s={headers:new Headers(n.headers),status:n.status,statusText:n.statusText},r=t?t(s):s,i=function(){if(void 0===v){const e=new Response("");if("body"in e)try{new Response(e.body),v=!0}catch(e){v=!1}v=!1}return v}()?n.body:await n.blob();return new Response(i,r)}try{self["workbox:precaching:5.1.3"]&&_()}catch(e){}function L(e){if(!e)throw new n("add-to-cache-list-unexpected-type",{entry:e});if("string"==typeof e){const t=new URL(e,location.href);return{cacheKey:t.href,url:t.href}}const{revision:t,url:s}=e;if(!s)throw new n("add-to-cache-list-unexpected-type",{entry:e});if(!t){const e=new URL(s,location.href);return{cacheKey:e.href,url:e.href}}const r=new URL(s,location.href),i=new URL(s,location.href);return r.searchParams.set("__WB_REVISION__",t),{cacheKey:r.href,url:i.href}}class x{constructor(e){this.o=f(e),this.u=new Map,this.h=new Map,this.l=new Map}addToCacheList(e){const t=[];for(const s of e){"string"==typeof s?t.push(s):s&&void 0===s.revision&&t.push(s.url);const{cacheKey:e,url:r}=L(s),i="string"!=typeof s&&s.revision?"reload":"default";if(this.u.has(r)&&this.u.get(r)!==e)throw new n("add-to-cache-list-conflicting-entries",{firstEntry:this.u.get(r),secondEntry:e});if("string"!=typeof s&&s.integrity){if(this.l.has(e)&&this.l.get(e)!==s.integrity)throw new n("add-to-cache-list-conflicting-integrities",{url:r});this.l.set(e,s.integrity)}if(this.u.set(r,e),this.h.set(r,i),t.length>0){const e=`Workbox is precaching URLs without revision info: ${t.join(", ")}\nThis is generally NOT safe. Learn more at https://bit.ly/wb-precache`;console.warn(e)}}}async install({event:e,plugins:t}={}){const n=[],s=[],r=await self.caches.open(this.o),i=await r.keys(),o=new Set(i.map(e=>e.url));for(const[e,t]of this.u)o.has(t)?s.push(e):n.push({cacheKey:t,url:e});const c=n.map(({cacheKey:n,url:s})=>{const r=this.l.get(n),i=this.h.get(s);return this.p({cacheKey:n,cacheMode:i,event:e,integrity:r,plugins:t,url:s})});await Promise.all(c);return{updatedURLs:n.map(e=>e.url),notUpdatedURLs:s}}async activate(){const e=await self.caches.open(this.o),t=await e.keys(),n=new Set(this.u.values()),s=[];for(const r of t)n.has(r.url)||(await e.delete(r),s.push(r.url));return{deletedURLs:s}}async p({cacheKey:e,url:t,cacheMode:s,event:r,plugins:i,integrity:o}){const c=new Request(t,{integrity:o,cache:s,credentials:"same-origin"});let a,u=await q({event:r,plugins:i,request:c});for(const e of i||[])"cacheWillUpdate"in e&&(a=e);if(!(a?await a.cacheWillUpdate({event:r,request:c,response:u}):u.status<400))throw new n("bad-precaching-response",{url:t,status:u.status});u.redirected&&(u=await U(u)),await R({event:r,plugins:i,response:u,request:e===t?c:new Request(e),cacheName:this.o,matchOptions:{ignoreSearch:!0}})}getURLsToCacheKeys(){return this.u}getCachedURLs(){return[...this.u.keys()]}getCacheKeyForURL(e){const t=new URL(e,location.href);return this.u.get(t.href)}async matchPrecache(e){const t=e instanceof Request?e.url:e,n=this.getCacheKeyForURL(t);if(n){return(await self.caches.open(this.o)).match(n)}}createHandler(e=!0){return async({request:t})=>{try{const e=await this.matchPrecache(t);if(e)return e;throw new n("missing-precache-entry",{cacheName:this.o,url:t instanceof Request?t.url:t})}catch(n){if(e)return fetch(t);throw n}}}createHandlerBoundToURL(e,t=!0){if(!this.getCacheKeyForURL(e))throw new n("non-precached-url",{url:e});const s=this.createHandler(t),r=new Request(e);return()=>s({request:r})}}let N;const b=()=>(N||(N=new x),N);const M=(e,t)=>{const n=b().getURLsToCacheKeys();for(const s of function*(e,{ignoreURLParametersMatching:t,directoryIndex:n,cleanURLs:s,urlManipulation:r}={}){const i=new URL(e,location.href);i.hash="",yield i.href;const o=function(e,t=[]){for(const n of[...e.searchParams.keys()])t.some(e=>e.test(n))&&e.searchParams.delete(n);return e}(i,t);if(yield o.href,n&&o.pathname.endsWith("/")){const e=new URL(o.href);e.pathname+=n,yield e.href}if(s){const e=new URL(o.href);e.pathname+=".html",yield e.href}if(r){const e=r({url:i});for(const t of e)yield t.href}}(e,t)){const e=n.get(s);if(e)return e}};let O=!1;function E(e){O||((({ignoreURLParametersMatching:e=[/^utm_/],directoryIndex:t="index.html",cleanURLs:n=!0,urlManipulation:s}={})=>{const r=f();self.addEventListener("fetch",i=>{const o=M(i.request.url,{cleanURLs:n,directoryIndex:t,ignoreURLParametersMatching:e,urlManipulation:s});if(!o)return;let c=self.caches.open(r).then(e=>e.match(o)).then(e=>e||fetch(o));i.respondWith(c)})})(e),O=!0)}const K=[],C={get:()=>K,add(e){K.push(...e)}},S=e=>{const t=b(),n=C.get();e.waitUntil(t.install({event:e,plugins:n}).catch(e=>{throw e}))},P=e=>{const t=b();e.waitUntil(t.activate())};e.CacheFirst=class{constructor(e={}){this.o=w(e.cacheName),this.g=e.plugins||[],this.R=e.fetchOptions,this.m=e.matchOptions}async handle({event:e,request:t}){"string"==typeof t&&(t=new Request(t));let s,r=await m({cacheName:this.o,request:t,event:e,matchOptions:this.m,plugins:this.g});if(!r)try{r=await this.q(t,e)}catch(e){s=e}if(!r)throw new n("no-response",{url:t.url,error:s});return r}async q(e,t){const n=await q({request:e,event:t,fetchOptions:this.R,plugins:this.g}),s=n.clone(),r=R({cacheName:this.o,request:e,response:s,event:t,plugins:this.g});if(t)try{t.waitUntil(r)}catch(e){}return n}},e.NavigationRoute=class extends r{constructor(e,{allowlist:t=[/./],denylist:n=[]}={}){super(e=>this.v(e),e),this.U=t,this.L=n}v({url:e,request:t}){if(t&&"navigate"!==t.mode)return!1;const n=e.pathname+e.search;for(const e of this.L)if(e.test(n))return!1;return!!this.U.some(e=>e.test(n))}},e.clientsClaim=function(){self.addEventListener("activate",()=>self.clients.claim())},e.createHandlerBoundToURL=function(e){return b().createHandlerBoundToURL(e)},e.precacheAndRoute=function(e,t){!function(e){b().addToCacheList(e),e.length>0&&(self.addEventListener("install",S),self.addEventListener("activate",P))}(e),E(t)},e.registerRoute=function(e,t,s){let o;if("string"==typeof e){const n=new URL(e,location.href);o=new r(({url:e})=>e.href===n.href,t,s)}else if(e instanceof RegExp)o=new i(e,t,s);else if("function"==typeof e)o=new r(e,t,s);else{if(!(e instanceof r))throw new n("unsupported-route-type",{moduleName:"workbox-routing",funcName:"registerRoute",paramName:"capture"});o=e}return u().registerRoute(o),o},e.skipWaiting=function(){self.addEventListener("install",()=>self.skipWaiting())}})); 2 | //# sourceMappingURL=workbox-a1d34bd3.js.map 3 | -------------------------------------------------------------------------------- /gramps_webapp/media.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | import boto3 5 | from botocore.exceptions import ClientError 6 | from flask import redirect, send_file 7 | 8 | from .image import get_thumbnail, get_thumbnail_cropped 9 | 10 | 11 | class MediaHandler: 12 | def __init__(self, handle, media_info): 13 | self.handle = handle 14 | self.media_info = media_info 15 | self.mime = media_info["mime"] 16 | 17 | 18 | class FileHandler(MediaHandler): 19 | def __init__(self, handle, media_info, base_path=None): 20 | super().__init__(handle, media_info) 21 | if base_path is None: 22 | self.path = media_info["full_path"] 23 | else: 24 | self.path = os.path.join(base_path, media_info["path"]) 25 | 26 | def send_file(self): 27 | return send_file(self.path) 28 | 29 | def send_thumbnail_square(self, size): 30 | f = get_thumbnail(self.path, size, square=True, mime=self.mime) 31 | return send_file(f, self.mime) 32 | 33 | def send_thumbnail_square_cropped(self, size, x1, y1, x2, y2): 34 | f = get_thumbnail_cropped( 35 | self.path, size, x1, y1, x2, y2, square=True, mime=self.mime 36 | ) 37 | return send_file(f, self.mime) 38 | 39 | 40 | class S3Handler(MediaHandler): 41 | def __init__(self, handle, media_info, bucket_name, base_path=None): 42 | super().__init__(handle, media_info) 43 | self.bucket_name = bucket_name 44 | self.client = boto3.client( 45 | "s3", 46 | config=boto3.session.Config( 47 | s3={"addressing_style": "path"}, signature_version="s3v4" 48 | ), 49 | ) 50 | self.object_name = handle 51 | self.url_lifetime = 3600 52 | if base_path is None: 53 | self.path = media_info["full_path"] 54 | else: 55 | self.path = os.path.join(base_path, media_info["path"]) 56 | 57 | def get_presigned_url(self, expires_in): 58 | try: 59 | response = self.client.generate_presigned_url( 60 | "get_object", 61 | Params={ 62 | "Bucket": self.bucket_name, 63 | "Key": self.object_name, 64 | "ResponseContentType": self.mime, 65 | }, 66 | ExpiresIn=expires_in, 67 | ) 68 | except ClientError as e: 69 | logging.error(e) 70 | return None 71 | return response 72 | 73 | def upload_file(self): 74 | try: 75 | self.client.upload_file( 76 | self.path, 77 | self.bucket_name, 78 | self.object_name, 79 | ExtraArgs={"ContentType": self.mime}, 80 | ) 81 | except ClientError as e: 82 | logging.error(e) 83 | return False 84 | return True 85 | 86 | def download_fileobj(self): 87 | try: 88 | response = self.client.get_object( 89 | Bucket=self.bucket_name, Key=self.object_name 90 | ) 91 | except ClientError as e: 92 | logging.error(e) 93 | return None 94 | return response["Body"] 95 | 96 | def send_file(self): 97 | url = self.get_presigned_url(expires_in=self.url_lifetime) 98 | return redirect(url, 307) 99 | 100 | def get_thumbnail_square(self, size): 101 | f = self.download_fileobj() 102 | return get_thumbnail(f, size, square=True, mime=self.mime) 103 | 104 | def get_thumbnail_square_cropped(self, size, x1, y1, x2, y2): 105 | f = self.download_fileobj() 106 | return get_thumbnail_cropped( 107 | f, size, x1, y1, x2, y2, square=True, mime=self.mime 108 | ) 109 | 110 | def send_thumbnail_square(self, size): 111 | f = self.get_thumbnail_square(size) 112 | return send_file(f, self.mime) 113 | 114 | def send_thumbnail_square_cropped(self, size, x1, y1, x2, y2): 115 | f = self.get_thumbnail_square_cropped(size, x1, y1, x2, y2) 116 | return send_file(f, self.mime) 117 | -------------------------------------------------------------------------------- /gramps_webapp/s3.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | import boto3 5 | from botocore.exceptions import ClientError 6 | from gramps.gen.utils.file import expand_media_path 7 | 8 | 9 | logging.basicConfig(level=logging.INFO) 10 | 11 | 12 | class MediaBucketUploader: 13 | """Class to upload media objects to an S3 bucket.""" 14 | 15 | def __init__(self, db, bucket_name, create=False, logger=None): 16 | """Initialize the class. 17 | 18 | `db` is an instance of an appropriate subclass of `gramps.gen.db.base.DbReadBase`. 19 | `bucket_name` is the S3 bucket name. 20 | If `create` is True, the bucket will be created if it doesn't exist. 21 | """ 22 | self.db = db 23 | self.bucket_name = bucket_name 24 | self.s3 = boto3.resource("s3") 25 | self.client = boto3.client("s3") 26 | self.logger = logger or logging.getLogger() 27 | if create: 28 | if self.bucket_exists: 29 | self.logger.info("Bucket {} already exists".format(bucket_name)) 30 | else: 31 | self.logger.info("Creating bucket {}".format(bucket_name)) 32 | region_name = boto3.session.Session().region_name 33 | bucket_config = {} 34 | if region_name: 35 | bucket_config = {"LocationConstraint": region_name} 36 | self.client.create_bucket( 37 | Bucket=bucket_name, CreateBucketConfiguration=bucket_config 38 | ) 39 | self.bucket = self.s3.Bucket(self.bucket_name) 40 | self.base_path = expand_media_path(self.db.get_mediapath(), self.db) 41 | 42 | @property 43 | def bucket_exists(self): 44 | try: 45 | self.client.head_bucket(Bucket=self.bucket_name) 46 | except ClientError as e: 47 | error_code = int(e.response["Error"]["Code"]) 48 | if error_code == 404: # bucket does not exist 49 | return False 50 | return True 51 | 52 | def get_remote_handles(self): 53 | """Get a set of all names of objects (media handles) in the bucket.""" 54 | return set([obj.key for obj in self.bucket.objects.all()]) 55 | 56 | def get_local_handles(self): 57 | """Get a dictionary of handle and mime types of all media objects in 58 | the database.""" 59 | return {m.handle: m.get_mime_type() for m in self.db.iter_media()} 60 | 61 | def get_full_path(self, handle): 62 | """Get the full local path to a media object by handle.""" 63 | m = self.db.get_media_from_handle(handle) 64 | return os.path.join(self.base_path, m.path) 65 | 66 | def upload(self, handle, mime): 67 | """Upload a media object with given handle and MIME type.""" 68 | path = self.get_full_path(handle) 69 | try: 70 | self.client.upload_file( 71 | path, self.bucket_name, handle, ExtraArgs={"ContentType": mime} 72 | ) 73 | except ClientError as e: 74 | logging.error(e) 75 | return False 76 | return True 77 | 78 | def upload_all(self): 79 | """Upload all media objects (overwriting existing ones).""" 80 | local_handles = self.get_local_handles() 81 | for handle, mime in local_handles.items(): 82 | self.upload(handle, mime) 83 | 84 | def upload_missing(self): 85 | """Upload the media objects that are not yet in the bucket.""" 86 | local_handles_dict = self.get_local_handles() 87 | local_handles = set(local_handles_dict.keys()) 88 | remote_handles = self.get_remote_handles() 89 | missing = local_handles - remote_handles 90 | N = len(missing) 91 | self.logger.info("Found {} objects to upload.".format(N)) 92 | for i, handle in enumerate(missing): 93 | self.logger.info( 94 | "Uploading file {} of {} ({}%)".format(i + 1, N, round(100 * i / N)) 95 | ) 96 | mime = local_handles_dict[handle] 97 | self.upload(handle, mime) 98 | -------------------------------------------------------------------------------- /gramps_webapp/sql_guid.py: -------------------------------------------------------------------------------- 1 | """Backend-agnostic GUID Type for SQLAlchemy 2 | 3 | https://docs.sqlalchemy.org/en/13/core/custom_types.html#backend-agnostic-guid-type 4 | """ 5 | 6 | from sqlalchemy.types import TypeDecorator, CHAR 7 | from sqlalchemy.dialects.postgresql import UUID 8 | import uuid 9 | 10 | 11 | class GUID(TypeDecorator): 12 | """Platform-independent GUID type. 13 | 14 | Uses PostgreSQL's UUID type, otherwise uses 15 | CHAR(32), storing as stringified hex values. 16 | """ 17 | 18 | impl = CHAR 19 | 20 | def load_dialect_impl(self, dialect): 21 | if dialect.name == "postgresql": 22 | return dialect.type_descriptor(UUID()) 23 | else: 24 | return dialect.type_descriptor(CHAR(32)) 25 | 26 | def process_bind_param(self, value, dialect): 27 | if value is None: 28 | return value 29 | elif dialect.name == "postgresql": 30 | return str(value) 31 | else: 32 | if not isinstance(value, uuid.UUID): 33 | return "%.32x" % uuid.UUID(value).int 34 | else: 35 | # hexstring 36 | return "%.32x" % value.int 37 | 38 | def process_result_value(self, value, dialect): 39 | if value is None: 40 | return value 41 | else: 42 | if not isinstance(value, uuid.UUID): 43 | value = uuid.UUID(value) 44 | return value 45 | -------------------------------------------------------------------------------- /gramps_webapp/test_auth.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from . import auth 3 | 4 | 5 | class TestSQLAuth(unittest.TestCase): 6 | def test_pwhash(self): 7 | sqlauth = auth.SQLAuth("sqlite://", logging=False) 8 | pwhash = sqlauth.hash_password("Xälz") 9 | test_correct = sqlauth.verify_password("Xälz", pwhash) 10 | test_wrong = sqlauth.verify_password("Marmelade", pwhash) 11 | self.assertTrue(test_correct) 12 | self.assertFalse(test_wrong) 13 | 14 | def test_add_user(self): 15 | sqlauth = auth.SQLAuth("sqlite://", logging=False) 16 | with self.assertRaises(ValueError): 17 | sqlauth.add_user("", "123") # empty username 18 | with self.assertRaises(ValueError): 19 | sqlauth.add_user("test_user", "") # empty pw 20 | sqlauth.add_user("test_user", "123", fullname="Test User") 21 | with self.assertRaisesRegex(ValueError, r".* already exists"): 22 | # adding again should fail 23 | sqlauth.add_user("test_user", "123", fullname="Test User") 24 | user = sqlauth.session.query(auth.User).filter_by(name="test_user").scalar() 25 | self.assertEqual(user.name, "test_user") 26 | self.assertEqual(user.fullname, "Test User") 27 | 28 | def test_authorized(self): 29 | sqlauth = auth.SQLAuth("sqlite://", logging=False) 30 | sqlauth.add_user("test_user", "123", fullname="Test User") 31 | self.assertTrue(sqlauth.authorized("test_user", "123")) 32 | self.assertFalse(sqlauth.authorized("test_user", "1234")) 33 | self.assertFalse(sqlauth.authorized("not_exist", "123")) 34 | 35 | def test_delete_user(self): 36 | sqlauth = auth.SQLAuth("sqlite://", logging=False) 37 | sqlauth.add_user("test_user", "123", fullname="Test User") 38 | user = sqlauth.session.query(auth.User).filter_by(name="test_user").scalar() 39 | self.assertIsNotNone(user) 40 | sqlauth.delete_user("test_user") 41 | user = sqlauth.session.query(auth.User).filter_by(name="test_user").scalar() 42 | self.assertIsNone(user) 43 | with self.assertRaisesRegex(ValueError, r".* not found"): 44 | # deleting again should fail 45 | sqlauth.delete_user("test_user") 46 | 47 | 48 | def test_change_names(self): 49 | sqlauth = auth.SQLAuth("sqlite://", logging=False) 50 | sqlauth.add_user("test_user", "123", fullname="Test User") 51 | guid = sqlauth.get_guid("test_user") 52 | sqlauth.modify_user("test_user", name_new="test_2", fullname="Test 2") 53 | user = sqlauth.session.query(auth.User).filter_by(id=guid).scalar() 54 | self.assertEqual(user.name, "test_2") 55 | self.assertEqual(user.fullname, "Test 2") 56 | 57 | def test_change_pw(self): 58 | sqlauth = auth.SQLAuth("sqlite://", logging=False) 59 | sqlauth.add_user("test_user", "123", fullname="Test User") 60 | sqlauth.modify_user("test_user", password="1234") 61 | self.assertFalse(sqlauth.authorized("test_user", "123")) 62 | self.assertTrue(sqlauth.authorized("test_user", "1234")) 63 | self.assertFalse(sqlauth.authorized("not_exist", "1234")) 64 | -------------------------------------------------------------------------------- /gramps_webapp/wsgi.py: -------------------------------------------------------------------------------- 1 | from .api import create_app 2 | 3 | app = create_app() -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidMStraub/gramps-webapp/1dcb57b7ce38a4aaa67fcd2896409f1111b8f557/screenshot.png -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | import glob 3 | import os 4 | 5 | 6 | with open('README.md') as f: 7 | LONG_DESCRIPTION = f.read() 8 | 9 | 10 | PACKAGE_DATA = [p[14:] for p in glob.glob('gramps_webapp/js/**/*', recursive=True)] 11 | PACKAGE_DATA = [f for f in PACKAGE_DATA if os.path.isfile(os.path.join('gramps_webapp', f))] 12 | 13 | 14 | setup(name='gramps-webapp', 15 | version='0.1', 16 | author='David M. Straub ', 17 | author_email='david.straub@tum.de', 18 | url='https://github.com/DavidMStraub/gramps-webapp', 19 | long_description=LONG_DESCRIPTION, 20 | long_description_content_type='text/markdown', 21 | license='GPLv3', 22 | packages=find_packages(), 23 | package_data={'gramps_webapp': PACKAGE_DATA}, 24 | install_requires=['flask', 25 | 'flask-restful', 26 | 'flask-caching', 27 | 'flask-jwt-extended', 28 | 'flask-cors', 29 | 'flask-compress', 30 | 'flask-limiter', 31 | 'Pillow', 32 | 'pdf2image', 33 | 'boto3', 34 | 'bleach', 35 | 'sqlalchemy'], 36 | ) 37 | --------------------------------------------------------------------------------