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

pass last_login_at to user crud UI from backend API correctly #133

Merged
merged 5 commits into from May 18, 2021
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions documentation/changelog.rst
Expand Up @@ -15,6 +15,7 @@ New features

Bugfixes
-----------
* Fix last login date display in user list [see `PR #133 <http://www.github.com/SeitaBV/flexmeasures/pull/133>`_]
* Choose better forecasting horizons when weather data is posted [see `PR #131 <http://www.github.com/SeitaBV/flexmeasures/pull/131>`_]

Infrastructure / Support
Expand Down
2 changes: 1 addition & 1 deletion flexmeasures/data/schemas/times.py
Expand Up @@ -65,7 +65,7 @@ def ground_from(


class AwareDateTimeField(fields.AwareDateTime, MarshmallowClickMixin):
"""Field that deserializes to a timezone aware datetime
"""Field that de-serializes to a timezone aware datetime
and serializes back to a string."""

def _deserialize(self, value: str, attr, obj, **kwargs) -> datetime:
Expand Down
2 changes: 2 additions & 0 deletions flexmeasures/data/schemas/users.py
Expand Up @@ -3,6 +3,7 @@

from flexmeasures.data import ma
from flexmeasures.data.models.user import User as UserModel
from flexmeasures.data.schemas.times import AwareDateTimeField


class UserSchema(ma.SQLAlchemySchema):
Expand All @@ -24,3 +25,4 @@ def validate_timezone(self, timezone):
active = ma.auto_field()
timezone = ma.auto_field()
flexmeasures_roles = ma.auto_field()
last_login_at = AwareDateTimeField()
2 changes: 1 addition & 1 deletion flexmeasures/data/scripts/cli_tasks/data_add.py
Expand Up @@ -502,7 +502,7 @@ def create_forecasts(
)
def collect_weather_data(region, location, num_cells, method, store_in_db):
"""
Collect weather forecasts from the DarkSky API
Collect weather forecasts from the OpenWeatherMap API

This function can get weather data for one location or for several locations within
a geometrical grid (See the --location parameter).
Expand Down
5 changes: 5 additions & 0 deletions flexmeasures/ui/crud/users.py
@@ -1,4 +1,5 @@
from typing import Optional, Union
from datetime import datetime

from flask import request, url_for
from flask_classful import FlaskView
Expand Down Expand Up @@ -55,6 +56,10 @@ def process_internal_api_response(
role_ids = tuple(user_data.get("flexmeasures_roles", []))
user_data["flexmeasures_roles"] = Role.query.filter(Role.id.in_(role_ids)).all()
user_data.pop("status", None) # might have come from requests.response
if "last_login_at" in user_data and user_data["last_login_at"] is not None:
user_data["last_login_at"] = datetime.fromisoformat(
user_data["last_login_at"]
)
if user_id:
user_data["id"] = user_id
if make_obj:
Expand Down
1 change: 1 addition & 0 deletions flexmeasures/ui/tests/utils.py
Expand Up @@ -64,6 +64,7 @@ def mock_user_response(
active=active,
password="secret",
flexmeasures_roles=[1],
last_login_at="2021-05-14T20:00:00+02:00",
)
if as_list:
user_list = [user]
Expand Down
17 changes: 5 additions & 12 deletions flexmeasures/ui/utils/view_utils.py
Expand Up @@ -9,7 +9,6 @@
from flask_security.core import current_user
from werkzeug.exceptions import BadRequest
import iso8601
import pytz

from flexmeasures import __version__ as flexmeasures_version
from flexmeasures.utils import time_utils
Expand Down Expand Up @@ -103,6 +102,8 @@ def clear_session():

def set_time_range_for_session():
"""Set period (start_date, end_date and resolution) on session if they are not yet set.
The datepicker sends times as tz-aware UTC strings.
We re-interpret them as being in the server's timezone.
Also set the forecast horizon, if given."""
if "start_time" in request.values:
session["start_time"] = time_utils.localized_datetime(
Expand All @@ -113,12 +114,8 @@ def set_time_range_for_session():
else:
if (
session["start_time"].tzinfo is None
): # session storage seems to lose tz info
session["start_time"] = (
session["start_time"]
.replace(tzinfo=pytz.utc)
.astimezone(time_utils.get_timezone())
)
): # session storage seems to lose tz info and becomes UTC
session["start_time"] = time_utils.as_server_time(session["start_time"])

if "end_time" in request.values:
session["end_time"] = time_utils.localized_datetime(
Expand All @@ -128,11 +125,7 @@ def set_time_range_for_session():
session["end_time"] = time_utils.get_default_end_time()
else:
if session["end_time"].tzinfo is None:
session["end_time"] = (
session["end_time"]
.replace(tzinfo=pytz.utc)
.astimezone(time_utils.get_timezone())
)
session["end_time"] = time_utils.as_server_time(session["end_time"])

# Our demo server's UI should only work with the current year's data
if current_app.config.get("FLEXMEASURES_MODE", "") == "demo":
Expand Down
43 changes: 43 additions & 0 deletions flexmeasures/utils/tests/test_time_utils.py
@@ -0,0 +1,43 @@
from datetime import datetime, timedelta

import pytz
import pytest

from flexmeasures.utils.time_utils import (
server_now,
naturalized_datetime_str,
)


@pytest.mark.parametrize(
"dt_tz,now,server_tz,delta_in_h,exp_result",
[
(None, datetime.utcnow(), "UTC", 3, "3 hours ago"),
(None, datetime(2021, 5, 17, 3), "Europe/Amsterdam", 48, "May 15"),
("Asia/Seoul", "server_now", "Europe/Amsterdam", 1, "an hour ago"),
("UTC", datetime(2021, 5, 17, 3), "Asia/Seoul", 24 * 7, "May 10"),
("UTC", datetime(2021, 5, 17, 3), "Asia/Seoul", None, "never"),
],
)
def test_naturalized_datetime_str(
app,
monkeypatch,
dt_tz,
now,
server_tz,
delta_in_h,
exp_result,
):
monkeypatch.setitem(app.config, "FLEXMEASURES_TIMEZONE", server_tz)
if now == "server_now":
now = server_now() # done this way as it needs app context
if now.tzinfo is None:
now.replace(tzinfo=pytz.utc) # assuming UTC
if delta_in_h is not None:
h_ago = now - timedelta(hours=delta_in_h)
if dt_tz is not None:
h_ago = h_ago.astimezone(pytz.timezone(dt_tz))
else:
h_ago = None
print(h_ago)
assert naturalized_datetime_str(h_ago, now=now) == exp_result
70 changes: 51 additions & 19 deletions flexmeasures/utils/time_utils.py
Expand Up @@ -33,12 +33,25 @@ def ensure_local_timezone(


def as_server_time(dt: datetime) -> datetime:
"""The datetime represented in the timezone of the FlexMeasures platform."""
"""The datetime represented in the timezone of the FlexMeasures platform.
If dt is naive, we assume it is UTC time.
"""
return naive_utc_from(dt).replace(tzinfo=pytz.utc).astimezone(get_timezone())


def localized_datetime(dt: datetime) -> datetime:
"""
Localise a datetime to the timezone of the FlexMeasures platform.
Note: this will change nothing but the tzinfo field.
"""
return get_timezone().localize(naive_utc_from(dt))


def naive_utc_from(dt: datetime) -> datetime:
"""Return a naive datetime, that is localised to UTC if it has a timezone."""
"""
Return a naive datetime, that is localised to UTC if it has a timezone.
If dt is naive, we assume it is already in UTC time.
"""
if not hasattr(dt, "tzinfo") or dt.tzinfo is None:
# let's hope this is the UTC time you expect
return dt
Expand All @@ -58,16 +71,13 @@ def tz_index_naively(
return data


def localized_datetime(dt: datetime) -> datetime:
"""Localise a datetime to the timezone of the FlexMeasures platform."""
return get_timezone().localize(naive_utc_from(dt))


def localized_datetime_str(dt: datetime, dt_format: str = "%Y-%m-%d %I:%M %p") -> str:
"""Localise a datetime to the timezone of the FlexMeasures platform.
Hint: This can be set as a jinja filter, so we can display local time in the app, e.g.:
app.jinja_env.filters['datetime'] = localized_datetime_filter
"""
Localise a datetime to the timezone of the FlexMeasures platform.
If no datetime is passed in, use server_now() as basis.

Hint: This can be set as a jinja filter, so we can display local time in the app, e.g.:
app.jinja_env.filters['localized_datetime'] = localized_datetime_str
"""
if dt is None:
dt = server_now()
Expand All @@ -76,16 +86,36 @@ def localized_datetime_str(dt: datetime, dt_format: str = "%Y-%m-%d %I:%M %p") -
return local_dt.strftime(dt_format)


def naturalized_datetime_str(dt: Optional[datetime]) -> str:
""" Naturalise a datetime object."""
def naturalized_datetime_str(
dt: Optional[datetime], now: Optional[datetime] = None
) -> str:
"""
Naturalise a datetime object (into a human-friendly string).
The dt parameter (as well as the now parameter if you use it)
can be either naive or tz-aware. We assume UTC in the naive case.

We use the the humanize library to generate a human-friendly string.
If dt is not longer ago than 24 hours, we use humanize.naturaltime (e.g. "3 hours ago"),
otherwise humanize.naturaldate (e.g. "one week ago")

Hint: This can be set as a jinja filter, so we can display local time in the app, e.g.:
app.jinja_env.filters['naturalized_datetime'] = naturalized_datetime_str
"""
if dt is None:
return "never"
if now is None:
now = datetime.utcnow()
# humanize uses the local now internally, so let's make dt local
local_timezone = tzlocal.get_localzone()
local_dt = (
dt.replace(tzinfo=pytz.utc).astimezone(local_timezone).replace(tzinfo=None)
)
if dt >= datetime.utcnow() - timedelta(hours=24):
if dt.tzinfo is None:
local_dt = (
dt.replace(tzinfo=pytz.utc)
.astimezone(tzlocal.get_localzone())
.replace(tzinfo=None)
)
else:
local_dt = dt.astimezone(tzlocal.get_localzone()).replace(tzinfo=None)
# decide which humanize call to use for naturalization
if naive_utc_from(dt) >= naive_utc_from(now) - timedelta(hours=24):
return naturaltime(local_dt)
else:
return naturaldate(local_dt)
Expand Down Expand Up @@ -123,9 +153,11 @@ def decide_resolution(start: Optional[datetime], end: Optional[datetime]) -> str
return resolution


def get_timezone(of_user=False):
def get_timezone(of_user=False) -> pytz.BaseTzInfo:
"""Return the FlexMeasures timezone, or if desired try to return the timezone of the current user."""
default_timezone = pytz.timezone(current_app.config.get("FLEXMEASURES_TIMEZONE"))
default_timezone = pytz.timezone(
current_app.config.get("FLEXMEASURES_TIMEZONE", "")
)
if not of_user:
return default_timezone
if current_user.is_anonymous:
Expand Down