diff --git a/examples/datetime-timezone/README.rst b/examples/datetime-timezone/README.rst new file mode 100644 index 000000000..c570419fd --- /dev/null +++ b/examples/datetime-timezone/README.rst @@ -0,0 +1,25 @@ +This example shows how to make Flask-Admin display all datetime fields in client's +timezone. +Timezone conversion is handled by the frontend in /static/js/timezone.js, but an +automatic post request to /set_timezone is done so that flask session can store the +client's timezone and save datetime inputs in the correct timezone. + +To run this example: + +1. Clone the repository:: + + git clone https://github.com/pallets-eco/flask-admin.git + cd flask-admin + +2. Create and activate a virtual environment:: + + virtualenv env + source env/bin/activate + +3. Install requirements:: + + pip install -r 'examples/datetime-timezone/requirements.txt' + +4. Run the application:: + + python examples/datetime-timezone/app.py diff --git a/examples/datetime-timezone/__init__.py b/examples/datetime-timezone/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/examples/datetime-timezone/app.py b/examples/datetime-timezone/app.py new file mode 100644 index 000000000..0d113ab73 --- /dev/null +++ b/examples/datetime-timezone/app.py @@ -0,0 +1,126 @@ +from datetime import datetime +from zoneinfo import ZoneInfo + +from flask import Flask, request, session, jsonify +from flask_sqlalchemy import SQLAlchemy +from markupsafe import Markup +from sqlalchemy import DateTime, String +from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column + +from flask_admin import Admin +from flask_admin.contrib.sqla import ModelView +from flask_admin.model import typefmt + + +# model +class Base(DeclarativeBase): + pass + + +# app +app = Flask(__name__) +app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///default.sqlite" +# Create dummy secret key so we can use sessions +app.config['SECRET_KEY'] = '123456789' +db = SQLAlchemy(model_class=Base) +db.init_app(app) + + +class Article(db.Model): + id: Mapped[int] = mapped_column(primary_key=True) + text: Mapped[str] = mapped_column(String(30)) + last_edit: Mapped[datetime] = mapped_column(DateTime(timezone=True)) + + +# admin +def date_format(view, value): + """ + Ensure consistent date format and inject class for timezone.js parser. + """ + if value is None: + return '' + return Markup( + f'{value.strftime("%Y-%m-%d %H:%M:%S")}' + ) + + +MY_DEFAULT_FORMATTERS = dict(typefmt.BASE_FORMATTERS) +MY_DEFAULT_FORMATTERS.update({ + datetime: date_format, +}) + + +class TimezoneAwareModelView(ModelView): + column_type_formatters = MY_DEFAULT_FORMATTERS + extra_js = ['/static/js/timezone.js'] + + def on_model_change(self, form, model, is_created): + """ + Save datetime fields after converting from session['timezone'] to UTC. + """ + user_timezone = session["timezone"] + + for field_name, field_value in form.data.items(): + if isinstance(field_value, datetime): + # Convert naive datetime to timezone-aware datetime + aware_time = field_value.replace(tzinfo=ZoneInfo(user_timezone)) + + # Convert the time to UTC + utc_time = aware_time.astimezone(ZoneInfo('UTC')) + + # Assign the UTC time to the model + setattr(model, field_name, utc_time) + + super(TimezoneAwareModelView, self).on_model_change(form, model, is_created) + + +# inherit TimeZoneAwareModelView to make any admin page timezone-aware +class TimezoneAwareBlogModelView(TimezoneAwareModelView): + column_labels = { + "last_edit": "Last Edit (local time)", + } + + +# compare with regular ModelView to display data as saved on db +class BlogModelView(ModelView): + column_labels = { + "last_edit": "Last Edit (UTC)", + } + + +# Flask views +@app.route('/') +def index(): + return 'Click me to get to Admin!' + + +@app.route('/set_timezone', methods=['POST']) +def set_timezone(): + """ + Save timezone to session so that datetime inputs can be correctly converted to UTC. + """ + session.permanent = True + timezone = request.get_json() + if timezone: + session['timezone'] = timezone + return jsonify({'message': 'Timezone set successfully'}), 200 + else: + return jsonify({'error': 'Invalid timezone'}), 400 + + +# create db on the fly +with app.app_context(): + Base.metadata.drop_all(db.engine) + Base.metadata.create_all(db.engine) + db.session.add(Article(text="Written at 9:00 UTC", + last_edit=datetime(2024, 8, 8, 9, 0, 0))) + db.session.commit() + admin = Admin(app, name='microblog') + admin.add_view( + BlogModelView(Article, db.session, name="Article", endpoint="article")) + admin.add_view( + TimezoneAwareBlogModelView(Article, db.session, name="Timezone Aware Article", + endpoint="timezone_aware_article")) + +if __name__ == '__main__': + app.run(debug=True) diff --git a/examples/datetime-timezone/requirements.txt b/examples/datetime-timezone/requirements.txt new file mode 100644 index 000000000..f248b7ae2 --- /dev/null +++ b/examples/datetime-timezone/requirements.txt @@ -0,0 +1,2 @@ +Flask-Admin +flask-sqlalchemy diff --git a/examples/datetime-timezone/static/js/timezone.js b/examples/datetime-timezone/static/js/timezone.js new file mode 100644 index 000000000..2054cb763 --- /dev/null +++ b/examples/datetime-timezone/static/js/timezone.js @@ -0,0 +1,38 @@ +// post client's timezone so that backend can correctly convert datetime inputs to UTC +fetch('/set_timezone', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(Intl.DateTimeFormat().resolvedOptions().timeZone) +}) + + +// convert all datetime fields to client timezone +function localizeDateTimes() { + const inputsOrSpans = document.querySelectorAll('input[data-date-format], span.timezone-aware'); + + inputsOrSpans.forEach(element => { + let localizedTime; + + const isInput = element.tagName.toLowerCase() === 'input' + // Check if the element is an input or a span + if (isInput) { + // For input elements, use the value attribute + localizedTime = new Date(element.getAttribute("value") + "Z"); + } else { + // For span elements, use the text content + localizedTime = new Date(element.textContent.trim() + "Z"); + } + + const formattedTime = moment(localizedTime).format('YYYY-MM-DD HH:mm:ss'); + + if (isInput) { + element.setAttribute("value", formattedTime); + } else { + element.textContent = formattedTime; + } + }); +} + +localizeDateTimes();