Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Example: display datetime fields in client's timezone and save user's input in UTC #2502

Merged
merged 8 commits into from
Aug 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions examples/datetime-timezone/README.rst
Original file line number Diff line number Diff line change
@@ -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
Empty file.
126 changes: 126 additions & 0 deletions examples/datetime-timezone/app.py
Original file line number Diff line number Diff line change
@@ -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'<span class="timezone-aware">{value.strftime("%Y-%m-%d %H:%M:%S")}</span>'
)


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 '<a href="/admin/timezone_aware_article">Click me to get to Admin!</a>'


@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)
2 changes: 2 additions & 0 deletions examples/datetime-timezone/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Flask-Admin
samuelhwilliams marked this conversation as resolved.
Show resolved Hide resolved
flask-sqlalchemy
38 changes: 38 additions & 0 deletions examples/datetime-timezone/static/js/timezone.js
Original file line number Diff line number Diff line change
@@ -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();