Skip to content

Commit ab8b653

Browse files
Add time_zone to committee json_upload (#3438) (#3490)
* Add time_zone to committee json_upload * Add get_valid_timezones presenter
1 parent 7a60573 commit ab8b653

File tree

18 files changed

+798
-17
lines changed

18 files changed

+798
-17
lines changed

docs/actions/committee.json_upload.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ The types noted below are the internal types after conversion in the backend. Se
1717
meeting_name: string,
1818
meeting_start_time: date,
1919
meeting_end_time: date,
20+
meeting_time_zone: string,
2021
meeting_admins: string[],
2122
meeting_template: string,
2223
}[]
@@ -50,6 +51,10 @@ Besides the usual headers as seen in the payload (`name`, `type`, `is_list`), th
5051
- `meeting_template`:
5152
- `done`: The meeting was found in the datastore, the new meeting will be cloned from it.
5253
- `warning`: The meeting was not found and the new meeting will not be cloned, but freshly created.
54+
- `meeting_time_zone`:
55+
- `done`: Valid timezone string (see [presenter](../presenters/get_valid_timezones.md)).
56+
- `warning`: Field empty and there's a start/end_time.
57+
- `error`: Not a valid timezone string.
5358

5459
The fields `forward_to_committee`, `organization_tags`, `managers`, `meeting_admins` and `meeting_template` store the `id` of the related model, if it exists, in the object for the import.
5560

docs/actions/meeting.create.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
// Optional
1010
description: string;
1111
location: string;
12-
timezone: string; // A valid IANA timezone string
12+
time_zone: string;
1313
start_time: datetime;
1414
end_time: datetime;
1515
organization_tag_ids: Id[];
@@ -25,6 +25,8 @@ Creates a meeting.
2525

2626
Checks whether the `organization.limit_of_meetings` is unlimited (=0) or lower than the amount of active meetings in `organization.active_meeting_ids`.
2727

28+
`time_zone` must be a valid timezone string (see [presenter](../presenters/get_valid_timezones.md)).
29+
2830
The following objects are created, too:
2931
- Groups: `Default`, `Admin`, `Delegates`, `Staff`, `Committees`. The first one is set as `meeting/default_group_id`, the second one as `meeting/admin_group_id`. The permissions can be found in the [initial-data.json](https://github.com/OpenSlides/openslides-backend/tree/main/data/initial-data.json)).
3032
- Projector: One projector named `"Default projector"` is created and set as `meeting/reference_projector_id`.

docs/actions/meeting.update.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
name: string;
1313
description: string;
1414
location: string;
15-
timezone: string; // A valid IANA timezone string
15+
time_zone: string;
1616
start_time: timestamp;
1717
end_time: timestamp;
1818
locked_from_inside: boolean;
@@ -209,6 +209,8 @@ Updates the meeting.
209209
If `set_as_template` is `True`, `template_for_organization_id` has to be set to `1`. If it is `False`, `template_for_organization_id` has to be set to `None` and if there are currently no users in the meetings admin group, an exception needs to be raised.
210210
`reference_projector_id` can only be set to a projector, which is not internal.
211211

212+
`time_zone` must be a valid timezone string (see [presenter](../presenters/get_valid_timezones.md)).
213+
212214
This action doesn't allow for a meeting to be set as a template and have `locked_from_inside` set to true at the same time. if this would be the result of an action call, an exception will be thrown. Same for `enable_anonymous` and `locked_from_inside` being true at the same time.
213215

214216
If `enable_anonymous` is set, this action will create an anonymous group for the meeting. This will have the name `Public` and otherwise differ from the other groups in the meeting due to having `anonymous_group_for_meeting_id` set. It will always have the lowest weight among all other groups in this meeting, meaning 0.

docs/actions/organization.update.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
login_text: string;
1414
theme_id: Id;
1515
default_language: string;
16-
time_zone: string; // A valid IANA timezone string
16+
time_zone: string;
1717
users_email_sender: string;
1818
users_email_replyto: string;
1919
users_email_subject: string;
@@ -43,6 +43,8 @@
4343
## Action
4444
Updates the organization.
4545
Checks if the theme_id is one of the theme_ids.
46+
`time_zone` must be a valid timezone string (see [presenter](../presenters/get_valid_timezones.md)).
47+
4648
This is an example of the saml_attr_mapping, where you can see the mappable fields.
4749
```json
4850
{

docs/actions/preface_special_imports.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ The internal types will be created by the backend service from the CSV-strings
116116
- **integer** Use something like "1234" without fraction
117117
- **number** means a `,` (comma) or `.` (point) separated value like `3123,45` or `3123.45`, but not `3.123,45` or `3,123.45`
118118
- **decimal** A decimal number with exactly 6 digits after the decimal seperator dot, e.g. `1.500000`
119-
- **date** Use a string in Isoformat "YYYY-MM-DD", for example "2023-04-26"
119+
- **date** Use a string in Isoformat "YYYY-MM-DD", for example "2023-04-26"
120120

121121
## Import_Preview to store the data to import in database
122122
```js
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Payload
2+
```js
3+
{
4+
}
5+
```
6+
7+
# Returns
8+
```js
9+
{
10+
[string]: string
11+
...
12+
}
13+
```
14+
15+
# Logic
16+
Gets all valid timezones from database.
17+
18+
Returns a timezone-name to current abbreviation dict.
19+
(Note: Abbreviations may change over the course of the year as certain timezones switch to and from DST)
20+
21+
In general this is going to return mostly canonic IANA timezone names.
22+
23+
# Permissions
24+
The user needs to either:
25+
- `OML.can_manage_organization`,
26+
- `CML.can_manage` in any committee, or
27+
- `meeting.can_manage_settings` in any meeting.

openslides_backend/action/actions/committee/import_.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ def create_models(self, rows: list[ImportRow]) -> None:
182182
"start_time",
183183
"end_time",
184184
"admin_ids",
185+
"time_zone",
185186
)
186187
if (meeting_field := f"meeting_{field}") in entry
187188
} | {

openslides_backend/action/actions/committee/json_upload.py

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
from datetime import datetime, timedelta, timezone
33
from typing import Any
44

5+
from psycopg import sql
6+
57
from openslides_backend.action.actions.meeting.mixins import MeetingCheckTimesMixin
68
from openslides_backend.shared.exceptions import ActionException
79
from openslides_backend.shared.filters import And, Filter, FilterOperator, Or
@@ -39,7 +41,9 @@ class CommitteeJsonUpload(
3941
**{
4042
f"meeting_{field}": prop
4143
for field, prop in Meeting()
42-
.get_properties("name", "start_time", "end_time")
44+
.get_properties(
45+
"name", "start_time", "end_time", "time_zone"
46+
)
4347
.items()
4448
},
4549
"meeting_admins": str_list_schema,
@@ -78,6 +82,7 @@ class CommitteeJsonUpload(
7882
{"property": "meeting_name", "type": "string"},
7983
{"property": "meeting_start_time", "type": "date"},
8084
{"property": "meeting_end_time", "type": "date"},
85+
{"property": "meeting_time_zone", "type": "string", "is_object": True},
8186
{
8287
"property": "meeting_admins",
8388
"type": "string",
@@ -88,6 +93,7 @@ class CommitteeJsonUpload(
8893
{"property": "parent", "type": "string", "is_object": True},
8994
]
9095
import_name = "committee"
96+
timezone_field_name = "meeting_time_zone"
9197

9298
permission = OrganizationManagementLevel.CAN_MANAGE_ORGANIZATION
9399
skip_archived_meeting_check = True
@@ -166,12 +172,40 @@ def validate_entry(self, entry: dict[str, Any]) -> dict[str, Any]:
166172
for field in (
167173
"meeting_start_time",
168174
"meeting_end_time",
175+
"meeting_time_zone",
169176
"meeting_admins",
170177
"meeting_template",
171178
)
172179
):
173180
messages.append("No meeting will be created without meeting_name")
174181
else:
182+
if tz := entry.get("meeting_time_zone"):
183+
valid = self.datastore.execute_custom_select(
184+
sql.SQL("is_timezone(%s) AS valid"), False, [tz]
185+
)[0].get("valid")
186+
if not valid:
187+
row_state = ImportState.ERROR
188+
entry["meeting_time_zone"] = {
189+
"value": tz,
190+
"info": ImportState.ERROR,
191+
}
192+
messages.append(
193+
f"Error: Invalid timezone format: '{tz}' (expected valid timezone name)"
194+
)
195+
else:
196+
entry["meeting_time_zone"] = {
197+
"value": tz,
198+
"info": ImportState.DONE,
199+
}
200+
elif entry.get("meeting_start_time") or entry.get("meeting_end_time"):
201+
entry["meeting_time_zone"] = {
202+
"info": ImportState.WARNING,
203+
"value": None,
204+
}
205+
zone = self.get_time_zone()
206+
messages.append(
207+
f"Since no timezone was given, the dates will be interpreted as being in the '{zone}' zone."
208+
)
175209
self.validate_with_lookup(
176210
entry, "meeting_admins", self.username_lookup, messages
177211
)
@@ -214,9 +248,10 @@ def check_meetings(self) -> None:
214248
FilterOperator("committee_id", "=", committee_id),
215249
FilterOperator("name", "=", meeting_name),
216250
]
251+
zone = self.get_time_zone_info(entry)
217252
for field in ("start_time", "end_time"):
218253
if time := entry.get(f"meeting_{field}"):
219-
start_of_day = datetime.fromtimestamp(time, timezone.utc)
254+
start_of_day = datetime.fromtimestamp(time, zone)
220255
start_of_day.replace(hour=0, minute=0, second=0, microsecond=0)
221256
end_of_day = start_of_day + timedelta(days=1)
222257
parts.extend(

openslides_backend/action/mixins/import_mixins.py

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
from datetime import datetime
66
from decimal import Decimal
77
from enum import Enum, StrEnum
8-
from time import mktime, strptime
98
from typing import Any, TypedDict, Union, cast
109
from zoneinfo import ZoneInfo
1110

@@ -22,6 +21,7 @@
2221
from ...shared.interfaces.write_request import WriteRequest
2322
from ...shared.patterns import fqid_from_collection_and_id
2423
from ...shared.schema import required_id_schema
24+
from ...shared.util import ONE_ORGANIZATION_FQID
2525
from ..util.default_schema import DefaultSchema
2626
from ..util.typing import ActionData, ActionResultElement
2727
from .singular_action_mixin import SingularActionMixin
@@ -391,6 +391,7 @@ class BaseJsonUploadAction(BaseImportJsonUploadAction):
391391
statistics: list[StatisticEntry]
392392
import_state: ImportState
393393
meeting_id: int
394+
timezone_field_name: str | None = None
394395

395396
def base_update_instance(self, instance: dict[str, Any]) -> dict[str, Any]:
396397
instance = super().base_update_instance(instance)
@@ -454,6 +455,40 @@ def add_payload_index_to_action_data(self, action_data: ActionData) -> ActionDat
454455
entry["payload_index"] = payload_index
455456
return action_data
456457

458+
def get_time_zone(self, entry: dict[str, Any] = {}) -> str:
459+
tz: str = (
460+
entry.get(self.timezone_field_name or "")
461+
or (
462+
self.datastore.get(
463+
fqid_from_collection_and_id("meeting", self.meeting_id),
464+
["time_zone"],
465+
).get("time_zone")
466+
if hasattr(self, "meeting_id")
467+
else None
468+
)
469+
or self.datastore.get(ONE_ORGANIZATION_FQID, ["time_zone"]).get("time_zone")
470+
or "UTC"
471+
)
472+
return tz
473+
474+
def get_time_zone_info(self, entry: dict[str, Any] = {}) -> ZoneInfo:
475+
tz = self.get_time_zone(entry)
476+
try:
477+
zone = ZoneInfo(tz)
478+
except Exception:
479+
if tz == entry.get(self.timezone_field_name or ""):
480+
zone = ZoneInfo(
481+
self.datastore.get(ONE_ORGANIZATION_FQID, ["time_zone"]).get(
482+
"time_zone"
483+
)
484+
or "UTC"
485+
)
486+
else:
487+
raise ActionException(
488+
f"Invalid timezone format: '{tz}' (expected valid timezone name)"
489+
)
490+
return zone
491+
457492
def validate_instance(self, instance: dict[str, Any]) -> None:
458493
# filter extra, not needed fields before validate and parse some fields
459494
property_to_type = {
@@ -506,9 +541,13 @@ def validate_instance(self, instance: dict[str, Any]) -> None:
506541
f"Could not parse {entry[field]} expect boolean"
507542
)
508543
elif type_ == "date":
544+
zone = self.get_time_zone_info(entry)
509545
try:
546+
y, m, d = entry[field].split("-")
510547
entry[field] = int(
511-
mktime(strptime(entry[field], "%Y-%m-%d"))
548+
datetime(
549+
int(y), int(m), int(d), tzinfo=zone
550+
).timestamp()
512551
)
513552
except Exception:
514553
raise ActionException(

openslides_backend/presenter/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
get_user_related_models,
1111
get_user_scope,
1212
get_users,
13+
get_valid_timezones,
1314
number_of_users,
1415
search_for_id_by_external_id,
1516
search_users,

0 commit comments

Comments
 (0)