├── .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 | 
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` `)):m(t||(t=p`
${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 |
3 |
42 |
43 | `));document.head.appendChild(A.content);const C=$(t||(t=k`
44 |
45 |
52 |
53 |
54 |
55 |
165 |
166 |
167 |
168 |
217 |
218 | `));document.head.appendChild(C.content);window.customElements.define("gr-children-element",class extends x{render(){return w(a||(a=k` [[item.name_given]]
`),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` [[item.type]]
([[item.role]]) ${0} `),this.items,j("Date"),j("Type"),j("Description"),this.place?w(d||(d=k` [[item.place_name]]
`),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` `)):d(e||(e=c` `),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` `)):g(t||(t=c` ${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` [[item.gramps_id]] [[item.date]]
[[item.type]] [[item.primary_participants]] `),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}
`),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} `),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 |
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 |
436 |
437 |
438 |
443 |
444 |
445 |
448 |
449 |
450 |
451 |
452 |
453 |
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` `)):g(l||(l=d` `),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 |
3 |
67 |
68 | `));document.head.appendChild(i.content);const n=a(t||(t=o`
69 |
70 |
210 |
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 Please enable JavaScript to view this website.
--------------------------------------------------------------------------------
/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 |
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 |
--------------------------------------------------------------------------------