Skip to content

Commit

Permalink
Added on copy url message + update start/end time validation + build …
Browse files Browse the repository at this point in the history
…url on copying

Signed-off-by: Nikolai Rozanov <nickolay.rozanov@gmail.com>
  • Loading branch information
nrozanov committed Apr 9, 2024
1 parent 042c703 commit c77a238
Show file tree
Hide file tree
Showing 6 changed files with 135 additions and 42 deletions.
7 changes: 6 additions & 1 deletion flexmeasures/data/schemas/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,9 @@
from .locations import LatitudeField, LongitudeField
from .sensors import SensorIdField, QuantityOrSensor
from .sources import DataSourceIdField as SourceIdField
from .times import AwareDateTimeField, DurationField, TimeIntervalField
from .times import (
AwareDateTimeField,
DurationField,
TimeIntervalField,
StartEndTimeSchema,
)
18 changes: 17 additions & 1 deletion flexmeasures/data/schemas/times.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from datetime import datetime, timedelta

from flask import current_app
from marshmallow import fields, Schema
from marshmallow import fields, Schema, validates_schema
from marshmallow.exceptions import ValidationError
import isodate
from isodate.isoerror import ISO8601Error
Expand Down Expand Up @@ -108,3 +108,19 @@ def _deserialize(self, value: str, attr, obj, **kwargs) -> dict:
raise ValidationError()

return TimeIntervalSchema().load(v)


class StartEndTimeSchema(Schema):
start_time = AwareDateTimeField(required=False)
end_time = AwareDateTimeField(required=False)

@validates_schema
def validate(self, data, **kwargs):
if not (data.get("start_time") or data.get("end_time")):
return
if not (data.get("start_time") and data.get("end_time")):
raise ValidationError(
"Both start_time and end_time must be provided together."
)
if data["start_time"] >= data["end_time"]:
raise ValidationError("start_time must be before end_time.")
12 changes: 3 additions & 9 deletions flexmeasures/ui/crud/assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from flexmeasures.data import db
from flexmeasures.auth.error_handling import unauthorized_handler
from flexmeasures.auth.policy import check_access
from flexmeasures.data.schemas import AwareDateTimeField
from flexmeasures.data.schemas import StartEndTimeSchema
from flexmeasures.data.models.generic_assets import (
GenericAssetType,
GenericAsset,
Expand Down Expand Up @@ -269,17 +269,11 @@ def owned_by(self, account_id: str):
user_can_create_assets=user_can_create_assets(),
)

@use_kwargs(
{
"start_time": AwareDateTimeField(format="iso", required=False),
"end_time": AwareDateTimeField(format="iso", required=False),
},
location="query",
)
@use_kwargs(StartEndTimeSchema, location="query")
@login_required
def get(self, id: str, **kwargs):
"""GET from /assets/<id> where id can be 'new' (and thus the form for asset creation is shown)
The following query parameters are supported:
The following query parameters are supported (should be used only together):
- start_time: minimum time of the events to be shown
- end_time: maximum time of the events to be shown
"""
Expand Down
7 changes: 6 additions & 1 deletion flexmeasures/ui/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@
{% endblock %}
</head>

<script>
// Define picker as a global variable
var picker;
</script>

<body>

{% block body %}
Expand Down Expand Up @@ -349,7 +354,7 @@

// Create date range picker and the logic for a new date range selection (mostly, fetching data and displaying the chart for it)
const date = Date();
const picker = new Litepicker({
picker = new Litepicker({
element: document.getElementById('datepicker'),
plugins: ['ranges', 'keyboardnav'],
ranges: {
Expand Down
85 changes: 55 additions & 30 deletions flexmeasures/ui/templates/crud/asset.html
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,61 @@ <h3>Edit {{ asset.name }}</h3>
<span class="sr-only">Loading...</span>
</div>
<div id="sensorchart" class="card" style="width: 100%;"></div>
<div class="row">
<div class="copy-url" title="Click to copy URL to clipboard">
<script>
function toIsoString(date) {
var tzo = -date.getTimezoneOffset(),
dif = tzo >= 0 ? '+' : '-',
pad = function(num) {
return (num < 10 ? '0' : '') + num;
};

return date.getFullYear() +
'-' + pad(date.getMonth() + 1) +
'-' + pad(date.getDate()) +
'T' + pad(date.getHours()) +
':' + pad(date.getMinutes()) +
':' + pad(date.getSeconds()) +
dif + pad(Math.floor(Math.abs(tzo) / 60)) +
':' + pad(Math.abs(tzo) % 60);
}
function copyUrl(event) {
event.preventDefault();

if (!window.getSelection) {
alert('Please copy the URL from the location bar.');
return;
}
const dummy = document.createElement('p');

var startDate = toIsoString(picker.getStartDate().toJSDate());
var endDate = toIsoString(picker.getEndDate().toJSDate());
var base_url = window.location.href.split("?")[0];
dummy.textContent = `${base_url}?start_time=${startDate}&end_time=${endDate}`
document.body.appendChild(dummy);

const range = document.createRange();
range.setStartBefore(dummy);
range.setEndAfter(dummy);

const selection = window.getSelection();
// First clear, in case the user already selected some other text
selection.removeAllRanges();
selection.addRange(range);

document.execCommand('copy');
document.body.removeChild(dummy);

$("#message").show().delay(1000).fadeOut();
}
</script>
<a href="#" onclick="copyUrl(event)" style="display: block; text-align: center;">
<i class="fa fa-link"></i>
</a>
<div id="message" style="display: none; text-align: center;">URL to this view has been copied to your clipboard.</div>
</div>
</div>
<div class="sensors-asset card">
<h3>All sensors for {{ asset.name }}</h3>
<table class="table table-striped table-responsive paginate nav-on-click" title="View data">
Expand Down Expand Up @@ -236,36 +291,6 @@ <h3>All child assets for {{ asset.name }}</h3>
</table>
</div>
</div>
<div class="row">
<div class="copy-url" title="Click to copy URL to clipboard">
<script>
function copyUrl() {
if (!window.getSelection) {
alert('Please copy the URL from the location bar.');
return;
}
const dummy = document.createElement('p');
dummy.textContent = window.location.href;
document.body.appendChild(dummy);

const range = document.createRange();
range.setStartBefore(dummy);
range.setEndAfter(dummy);

const selection = window.getSelection();
// First clear, in case the user already selected some other text
selection.removeAllRanges();
selection.addRange(range);

document.execCommand('copy');
document.body.removeChild(dummy);
}
</script>
<a href="#" onclick="copyUrl()">
<i class="fa fa-link"></i>
</a>
</div>
</div>
<div class="col-sm-2">
<div class="replay-container">
<div id="replay" title="Press 'p' to play/pause/resume or 's' to stop." class="stopped"></div>
Expand Down
48 changes: 48 additions & 0 deletions flexmeasures/ui/tests/test_asset_crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,54 @@ def test_asset_page(db, client, setup_assets, requests_mock, as_prosumer_user1):
)


@pytest.mark.parametrize(
"args, error",
[
(
{"start_time": "2022-10-01T00:00:00+02:00"},
"Both start_time and end_time must be provided together.",
),
(
{"end_time": "2022-10-01T00:00:00+02:00"},
"Both start_time and end_time must be provided together.",
),
(
{
"start_time": "2022-10-01T00:00:00+02:00",
"end_time": "2022-10-01T00:00:00+02:00",
},
"start_time must be before end_time.",
),
(
{
"start_time": "2022-10-01T00:00:00",
"end_time": "2022-10-02T00:00:00+02:00",
},
"Not a valid aware datetime",
),
],
)
def test_asset_page_dates_validation(
db, client, setup_assets, requests_mock, as_prosumer_user1, args, error
):
user = find_user_by_email("test_prosumer_user@seita.nl")
asset = user.account.generic_assets[0]
db.session.expunge(user)
mock_asset = mock_asset_response(as_list=False)

requests_mock.get(f"{api_path_assets}/{asset.id}", status_code=200, json=mock_asset)
asset_page = client.get(
url_for(
"AssetCrudUI:get",
id=asset.id,
**args,
),
follow_redirects=True,
)
assert error.encode() in asset_page.data
assert "UNPROCESSABLE_ENTITY".encode() in asset_page.data


def test_edit_asset(db, client, setup_assets, requests_mock, as_admin):
mock_asset = mock_asset_response(as_list=False)
requests_mock.patch(f"{api_path_assets}/1", status_code=200, json=mock_asset)
Expand Down

0 comments on commit c77a238

Please sign in to comment.