Skip to content

Commit 3e88a6a

Browse files
Karthikeyannhsdependabot[bot]oneeb-nhsrobbailiff2eddalmond1
authored
ELI-716 | apply UK timezone when parsing start/end dates and iteration date/time (#601)
* ELI-615 | campaign having recent - active start_date supersedes the others sharing same best-status * ELI-615 | more linting * ELI-615 | revert commit * ELI-615 | wip * ELI-615 | wip * ELI-615 | wip * Bump werkzeug from 3.1.5 to 3.1.6 Bumps [werkzeug](https://github.com/pallets/werkzeug) from 3.1.5 to 3.1.6. - [Release notes](https://github.com/pallets/werkzeug/releases) - [Changelog](https://github.com/pallets/werkzeug/blob/main/CHANGES.rst) - [Commits](pallets/werkzeug@3.1.5...3.1.6) --- updated-dependencies: - dependency-name: werkzeug dependency-version: 3.1.6 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> * Updated not_member_of operator to NotMemberOf (#594) * Added vulture to workflows (#585) * Added vulture to workflows * Added new make commands and added to project * Added updated lockfile * Minimal config with no errors * Corrected vulture commands * Generating new lock file * ELI-615 | modified iterations_result to iteration result * ELI-615 | fix - naming issues | handle stop iter exception * ELI-615 | campaign_configs - fixture updated | test case fixed * ELI-615 | fix flaky tests do to fixture scope * ELI-615 | fix flaky tests - removed best status test * ELI-615 | used raw campagin config for tests using iteration dates * ELI-615 | fix - campaign group is used correctly * ELI-615 | fix test_campaigns_grouped_by_condition_name_filters_correctly * ELI-615 | fix tests * ELI-615 | linting * ELI-615 | renamed best_iteration_result to iteration_result_summary * ELI-615 | add more test cases - it tests * ELI-615 | test commit - try git leaks ignore * updated iteration time * Eli 615 : fix - multi campaign target collision (#593) * ELI-615 | campaign having recent - active start_date supersedes the others sharing same best-status * ELI-615 | more linting * ELI-615 | revert commit * ELI-615 | wip * ELI-615 | wip * ELI-615 | wip * Bump werkzeug from 3.1.5 to 3.1.6 Bumps [werkzeug](https://github.com/pallets/werkzeug) from 3.1.5 to 3.1.6. - [Release notes](https://github.com/pallets/werkzeug/releases) - [Changelog](https://github.com/pallets/werkzeug/blob/main/CHANGES.rst) - [Commits](pallets/werkzeug@3.1.5...3.1.6) --- updated-dependencies: - dependency-name: werkzeug dependency-version: 3.1.6 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> * Updated not_member_of operator to NotMemberOf (#594) * Added vulture to workflows (#585) * Added vulture to workflows * Added new make commands and added to project * Added updated lockfile * Minimal config with no errors * Corrected vulture commands * Generating new lock file * ELI-615 | modified iterations_result to iteration result * ELI-615 | fix - naming issues | handle stop iter exception * ELI-615 | campaign_configs - fixture updated | test case fixed * ELI-615 | fix flaky tests do to fixture scope * ELI-615 | fix flaky tests - removed best status test * ELI-615 | used raw campagin config for tests using iteration dates * ELI-615 | fix - campaign group is used correctly * ELI-615 | fix test_campaigns_grouped_by_condition_name_filters_correctly * ELI-615 | fix tests * ELI-615 | linting * ELI-615 | renamed best_iteration_result to iteration_result_summary * ELI-615 | add more test cases - it tests * ELI-615 | test commit - try git leaks ignore * ELI-615 | incorporated review comments --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: oneeb-nhs <258801025+oneeb-nhs@users.noreply.github.com> Co-authored-by: Robert Bailiff <rob.bailiff1@nhs.net> * ELI-674 | fix - vs code alignment anomalies * ELI-674 |wip - updated iteration_datetime property * ELI-674 - test_iteration_full_datetime_validation checks for datetime * updated iteration time * updated test cases for iteration_time * ELI-674 - fixed flaky test, which was due to iteration/campaign factory * ELI-674 - tweaks to validator * ELI-674 - tweaks to validator * ELI-674 - tweaks to validator * ELI-674 - tweaks to validator * ELI-674 - tweaks to validator * reverted default_iteration_time to iteration_time * ELI-674 - fix errors * ELI-674 - new test case for validators * ELI-674 - linting * ELI-674 - fixed unit tests * ELI-674 - fixed integration tests * Create *.instructions.md * eja - refining instruction files using Github best practice prompt * eja - fixing secret scan and vale * added ignore for gitleaks as well as an allow for the file * Eli 615 : fix - multi campaign target collision (#593) * ELI-615 | campaign having recent - active start_date supersedes the others sharing same best-status * ELI-615 | more linting * ELI-615 | revert commit * ELI-615 | wip * ELI-615 | wip * ELI-615 | wip * Bump werkzeug from 3.1.5 to 3.1.6 Bumps [werkzeug](https://github.com/pallets/werkzeug) from 3.1.5 to 3.1.6. - [Release notes](https://github.com/pallets/werkzeug/releases) - [Changelog](https://github.com/pallets/werkzeug/blob/main/CHANGES.rst) - [Commits](pallets/werkzeug@3.1.5...3.1.6) --- updated-dependencies: - dependency-name: werkzeug dependency-version: 3.1.6 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> * Updated not_member_of operator to NotMemberOf (#594) * Added vulture to workflows (#585) * Added vulture to workflows * Added new make commands and added to project * Added updated lockfile * Minimal config with no errors * Corrected vulture commands * Generating new lock file * ELI-615 | modified iterations_result to iteration result * ELI-615 | fix - naming issues | handle stop iter exception * ELI-615 | campaign_configs - fixture updated | test case fixed * ELI-615 | fix flaky tests do to fixture scope * ELI-615 | fix flaky tests - removed best status test * ELI-615 | used raw campagin config for tests using iteration dates * ELI-615 | fix - campaign group is used correctly * ELI-615 | fix test_campaigns_grouped_by_condition_name_filters_correctly * ELI-615 | fix tests * ELI-615 | linting * ELI-615 | renamed best_iteration_result to iteration_result_summary * ELI-615 | add more test cases - it tests * ELI-615 | test commit - try git leaks ignore * ELI-615 | incorporated review comments --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: oneeb-nhs <258801025+oneeb-nhs@users.noreply.github.com> Co-authored-by: Robert Bailiff <rob.bailiff1@nhs.net> * eli-537 enabling WAF blocks * eli-537 deleting dev deployment * eli-537 added US to permitted geos, for preprod only, to allow github action tests to still flow * eli-537 amending rate limit * eli-537 minor changes based on initial review * eli-537 removing Production from description as we also deploy to PreProd * eli-537 adding ignore for gitleaks * Bump authlib from 1.6.6 to 1.6.7 Bumps [authlib](https://github.com/authlib/authlib) from 1.6.6 to 1.6.7. - [Release notes](https://github.com/authlib/authlib/releases) - [Changelog](https://github.com/authlib/authlib/blob/main/docs/changelog.rst) - [Commits](authlib/authlib@v1.6.6...v1.6.7) --- updated-dependencies: - dependency-name: authlib dependency-version: 1.6.7 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> * ELI-615 | campaign having recent - active start_date supersedes the others sharing same best-status * ELI-615 | wip * Added vulture to workflows (#585) * Added vulture to workflows * Added new make commands and added to project * Added updated lockfile * Minimal config with no errors * Corrected vulture commands * Generating new lock file * ELI-615 | campaign_configs - fixture updated | test case fixed * ELI-615 | renamed best_iteration_result to iteration_result_summary * ELI-674 - revert - rebasing * ELI-674 - pytest for rules_validation/app.py * ELI-674 - updated comments for sonar fix * ELI-674 - sonar suppression * Revert "ELI-674 - sonar suppression" This reverts commit b4efff2. * ELI-674 - linting suppression * enabled conversion of datetime to utc * ELI-716 | more test cases for time * ELI-716 | created start_time_utc, end_time_utc, iteration_datetime_utc * ELI-716 - Dateutil are functions now, not inside classes - review comments of 674 * ELI-716 - iteration_datetime_utc is use for ordering current_iteration * ELI-716 - current iteration code updated * ELI-716 | updated testcases and more linting * ELI-716 | sonar suppression * ELI-716 | wip * ELI-716 | wip * ELI-716 | wip * ELI-716 | wip * ELI-716 | renames variables * ELI-716 | duplicated removed - sonar * ELI-716 | more bst / gmt tests * revert back to UK timeZone * refined the codebase to reuse code * updated testcases to use UK time * link parent code updated * review comments - incorporated --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: oneeb-nhs <258801025+oneeb-nhs@users.noreply.github.com> Co-authored-by: Robert Bailiff <rob.bailiff1@nhs.net> Co-authored-by: eddalmond1 <102675624+eddalmond1@users.noreply.github.com>
1 parent e2493b7 commit 3e88a6a

10 files changed

Lines changed: 597 additions & 119 deletions

File tree

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import re
2+
from collections.abc import Callable
3+
from datetime import date, datetime, time
4+
from zoneinfo import ZoneInfo
5+
6+
UK_TIMEZONE = ZoneInfo("Europe/London")
7+
8+
9+
def now_uk() -> datetime:
10+
return datetime.now(tz=UK_TIMEZONE)
11+
12+
13+
def date_with_uk_timezone(parsed_date: date) -> date:
14+
return datetime.combine(parsed_date, time.min).replace(tzinfo=UK_TIMEZONE).date()
15+
16+
17+
def datetime_with_uk_timezone(parsed_date_time: datetime) -> datetime:
18+
return parsed_date_time.replace(tzinfo=UK_TIMEZONE)
19+
20+
21+
def _parse_with_format[T](
22+
value: str,
23+
regex: str,
24+
fmt: str,
25+
error_info: tuple[str, str],
26+
transform: Callable[[datetime], T],
27+
) -> T:
28+
"""Shared logic for regex validation and datetime parsing."""
29+
label, expected_format = error_info
30+
31+
if not re.fullmatch(regex, value):
32+
msg = f"Invalid format: {value}. Must be {expected_format}."
33+
raise ValueError(msg)
34+
try:
35+
dt = datetime.strptime(value, fmt) # noqa: DTZ007
36+
return transform(dt)
37+
except ValueError as err:
38+
msg = f"Invalid {label} value: {value}."
39+
raise ValueError(msg) from err
40+
41+
42+
def parse_date_yyyymmdd(v: str | date) -> date:
43+
if isinstance(v, date):
44+
return v
45+
# Pass the last two strings as a single tuple inside parentheses
46+
return _parse_with_format(str(v), r"\d{8}", "%Y%m%d", ("date", "YYYYMMDD"), lambda dt: dt.date())
47+
48+
49+
def parse_time_hhmmss(v: str | time | None) -> time | None:
50+
if v is None:
51+
return None
52+
if isinstance(v, time):
53+
return v
54+
# Pass the last two strings as a single tuple inside parentheses
55+
return _parse_with_format(
56+
str(v).strip(), r"^\d{2}:\d{2}:\d{2}$", "%H:%M:%S", ("time", "HH:MM:SS"), lambda dt: dt.time()
57+
)

src/eligibility_signposting_api/model/campaign_config.py

Lines changed: 38 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
from __future__ import annotations
22

33
import json
4-
import re
54
import typing
65
from collections import Counter
7-
from datetime import UTC, date, datetime, time
6+
from datetime import date, datetime, time
87
from enum import StrEnum
98
from functools import cached_property
109
from operator import attrgetter
@@ -21,11 +20,19 @@
2120
model_validator,
2221
)
2322

23+
from eligibility_signposting_api.common.date_util import (
24+
UK_TIMEZONE,
25+
datetime_with_uk_timezone,
26+
now_uk,
27+
parse_date_yyyymmdd,
28+
parse_time_hhmmss,
29+
)
2430
from eligibility_signposting_api.config.constants import ALLOWED_CONDITIONS, RULE_STOP_DEFAULT
2531

2632
if typing.TYPE_CHECKING: # pragma: no cover
2733
from pydantic import SerializationInfo
2834

35+
2936
CampaignName = NewType("CampaignName", str)
3037
CampaignVersion = NewType("CampaignVersion", int)
3138
CampaignID = NewType("CampaignID", str)
@@ -51,38 +58,6 @@
5158
RuleText = NewType("RuleText", str)
5259

5360

54-
class DateUtil:
55-
@staticmethod
56-
def parse_date_yyyymmdd(v: str | date) -> date:
57-
if isinstance(v, date):
58-
return v
59-
v_str = str(v)
60-
if not re.fullmatch(r"\d{8}", v_str):
61-
msg = f"Invalid format: {v_str}. Must be YYYYMMDD."
62-
raise ValueError(msg)
63-
try:
64-
return datetime.strptime(v_str, "%Y%m%d").date() # noqa: DTZ007
65-
except ValueError as err:
66-
msg = f"Invalid date value: {v_str}."
67-
raise ValueError(msg) from err
68-
69-
@staticmethod
70-
def parse_time_hhmmss(v: str | time | None) -> time | None:
71-
if not v:
72-
return None
73-
if isinstance(v, time):
74-
return v
75-
v_str = str(v).strip()
76-
if re.fullmatch(r"^\d{2}:\d{2}:\d{2}$", v_str):
77-
try:
78-
return datetime.strptime(v_str, "%H:%M:%S").time() # noqa: DTZ007
79-
except ValueError as err:
80-
msg = f"Invalid time value: {v_str}."
81-
raise ValueError(msg) from err
82-
msg = f"Invalid format: {v_str}. Must be HH:MM:SS."
83-
raise ValueError(msg)
84-
85-
8661
class RuleType(StrEnum):
8762
filter = "F"
8863
suppression = "S"
@@ -294,8 +269,12 @@ class Iteration(BaseModel):
294269
id: IterationID = Field(..., alias="ID")
295270
version: IterationVersion = Field(..., alias="Version")
296271
name: IterationName = Field(..., alias="Name")
297-
iteration_date: IterationDate = Field(..., alias="IterationDate")
298-
iteration_time: IterationTime | None = Field(default=None, alias="IterationTime")
272+
iteration_date: IterationDate = Field(
273+
..., alias="IterationDate", description="Iteration start date in Europe/London time Zone"
274+
)
275+
iteration_time: IterationTime | None = Field(
276+
default=None, alias="IterationTime", description="Iteration start time in Europe/London time Zone"
277+
)
299278
iteration_number: int | None = Field(None, alias="IterationNumber")
300279
approval_minimum: int | None = Field(None, alias="ApprovalMinimum")
301280
approval_maximum: int | None = Field(None, alias="ApprovalMaximum")
@@ -319,13 +298,14 @@ def _link_parent_to_iteration_rules(self) -> typing.Self:
319298

320299
@field_validator("iteration_date", mode="before")
321300
@classmethod
322-
def parse_dates(cls, v: str | date) -> date:
323-
return DateUtil.parse_date_yyyymmdd(v)
301+
def parse_dates_as_uk_local(cls, v: str | date) -> date:
302+
parsed_date = parse_date_yyyymmdd(v)
303+
return datetime.combine(parsed_date, time.min).replace(tzinfo=UK_TIMEZONE).date()
324304

325305
@field_validator("iteration_time", mode="before")
326306
@classmethod
327-
def parse_times(cls, v: str | time) -> time | None:
328-
return DateUtil.parse_time_hhmmss(v)
307+
def parse_times_as_uk_local(cls, v: str | time) -> time | None:
308+
return parse_time_hhmmss(v)
329309

330310
@field_serializer("iteration_date", when_used="always")
331311
@staticmethod
@@ -344,6 +324,9 @@ def set_parent(self, parent: CampaignConfig) -> None:
344324

345325
@cached_property
346326
def iteration_datetime(self) -> datetime:
327+
"""iteration_datetime is the datetime of the iteration,
328+
including the iteration_time if set, otherwise the parent's iteration_time.
329+
the return type is datetime in Europe/London time zone."""
347330
if self.iteration_time:
348331
iteration_time = self.iteration_time
349332
elif self._parent:
@@ -352,7 +335,7 @@ def iteration_datetime(self) -> datetime:
352335
msg = f"No iteration_time and no parent linked for iteration {self.id}"
353336
raise ValueError(msg)
354337

355-
return datetime.combine(self.iteration_date, iteration_time).replace(tzinfo=UTC)
338+
return datetime_with_uk_timezone(datetime.combine(self.iteration_date, iteration_time))
356339

357340
def __str__(self) -> str:
358341
return json.dumps(self.model_dump(by_alias=True), indent=2)
@@ -369,10 +352,14 @@ class CampaignConfig(BaseModel):
369352
reviewer: list[str] | None = Field(None, alias="Reviewer")
370353
iteration_frequency: Literal["X", "D", "W", "M", "Q", "A"] = Field(..., alias="IterationFrequency")
371354
iteration_type: Literal["A", "M", "S", "O"] = Field(..., alias="IterationType")
372-
iteration_time: IterationTime = Field(default=IterationTime(time(0, 0, 0)), alias="IterationTime")
355+
iteration_time: IterationTime = Field(
356+
default=IterationTime(time(0, 0, 0)),
357+
alias="IterationTime",
358+
description="Default Iteration start time in Europe/London time Zone",
359+
)
373360
default_comms_routing: str | None = Field(None, alias="DefaultCommsRouting")
374-
start_date: StartDate = Field(..., alias="StartDate")
375-
end_date: EndDate = Field(..., alias="EndDate")
361+
start_date: StartDate = Field(..., alias="StartDate", description="Campaign start date in Europe/London time Zone")
362+
end_date: EndDate = Field(..., alias="EndDate", description="Campaign end date in Europe/London time Zone")
376363
approval_minimum: int | None = Field(None, alias="ApprovalMinimum")
377364
approval_maximum: int | None = Field(None, alias="ApprovalMaximum")
378365
iterations: list[Iteration] = Field(..., min_length=1, alias="Iterations")
@@ -388,13 +375,14 @@ def _link_parent_to_iterations(self) -> typing.Self:
388375

389376
@field_validator("start_date", "end_date", mode="before")
390377
@classmethod
391-
def parse_dates(cls, v: str | date) -> date:
392-
return DateUtil.parse_date_yyyymmdd(v)
378+
def parse_dates_as_uk_local(cls, v: str | date) -> date:
379+
parsed_date = parse_date_yyyymmdd(v)
380+
return datetime.combine(parsed_date, time.min).replace(tzinfo=UK_TIMEZONE).date()
393381

394382
@field_validator("iteration_time", mode="before")
395383
@classmethod
396-
def parse_times(cls, v: str | time) -> time | None:
397-
return DateUtil.parse_time_hhmmss(v)
384+
def parse_times_as_uk_local(cls, v: str | time) -> time | None:
385+
return parse_time_hhmmss(v)
398386

399387
@field_serializer("start_date", "end_date", when_used="always")
400388
@staticmethod
@@ -435,14 +423,12 @@ def check_no_overlapping_iterations(self) -> typing.Self:
435423

436424
@cached_property
437425
def campaign_live(self) -> bool:
438-
today = datetime.now(tz=UTC).date()
439-
return self.start_date <= today <= self.end_date
426+
return self.start_date <= now_uk().date() <= self.end_date
440427

441428
@cached_property
442429
def current_iteration(self) -> Iteration:
443-
now = datetime.now(tz=UTC)
444430
iterations_by_date_descending = sorted(self.iterations, key=attrgetter("iteration_datetime"), reverse=True)
445-
return next(i for i in iterations_by_date_descending if i.iteration_datetime <= now)
431+
return next(i for i in iterations_by_date_descending if i.iteration_datetime <= now_uk())
446432

447433
def __str__(self) -> str:
448434
return json.dumps(self.model_dump(by_alias=True), indent=2)

src/eligibility_signposting_api/services/processors/campaign_evaluator.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,19 +42,19 @@ def get_campaign_with_latest_iteration(self, active_campaigns: list[CampaignConf
4242
latest_campaign = None
4343
else:
4444
max_date_time = max(item[0] for item in valid_items)
45-
cc_with_max_iteration_date: list[CampaignConfig] = [
45+
cc_with_max_iteration_date_time: list[CampaignConfig] = [
4646
item[1] for item in valid_items if item[0] == max_date_time
4747
]
48-
if len(cc_with_max_iteration_date) > 1:
48+
if len(cc_with_max_iteration_date_time) > 1:
4949
err_msg = (
50-
f"Ambiguous result: '{len(cc_with_max_iteration_date)}' active iterations "
51-
f"for target {cc_with_max_iteration_date[0].target} "
50+
f"Ambiguous result: '{len(cc_with_max_iteration_date_time)}' active iterations "
51+
f"for target {cc_with_max_iteration_date_time[0].target} "
5252
f"found for datetime '{max_date_time}' "
53-
f"across campaign(s) {[cc.id for cc in cc_with_max_iteration_date]}"
53+
f"across campaign(s) {[cc.id for cc in cc_with_max_iteration_date_time]}"
5454
)
5555
raise ValueError(err_msg)
5656

57-
latest_campaign = cc_with_max_iteration_date[0]
57+
latest_campaign = cc_with_max_iteration_date_time[0]
5858

5959
return latest_campaign
6060

src/rules_validation_api/app.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
import logging
44
import sys
55
from collections import defaultdict
6-
from datetime import UTC, datetime
6+
from datetime import datetime
77
from operator import attrgetter
88
from pathlib import Path
9+
from zoneinfo import ZoneInfo
910

1011
from pydantic import ValidationError
1112

@@ -24,6 +25,8 @@
2425
RED = "\033[91m"
2526
BLUE = "\033[34m"
2627

28+
UK_TIMEZONE = ZoneInfo("Europe/London")
29+
2730

2831
def refine_error(e: ValidationError) -> str:
2932
"""Return a very short, single-line error message."""
@@ -79,10 +82,10 @@ def display_current_iteration(result: RulesValidation) -> None:
7982
config = result.campaign_config
8083
iterations = config.iterations
8184
is_campaign_live = config.campaign_live
82-
today = datetime.now(tz=UTC).date()
85+
today_uk = datetime.now(tz=UK_TIMEZONE).date()
8386

8487
no_of_iterations = len(iterations)
85-
is_campaign_expired = config.end_date < today
88+
is_campaign_expired = config.end_date < today_uk
8689

8790
# ---- Current Iteration ----
8891
if is_campaign_live:
@@ -114,7 +117,7 @@ def display_current_iteration(result: RulesValidation) -> None:
114117
sorted_iterations = sorted(iterations, key=attrgetter("iteration_date"))
115118

116119
try:
117-
next_iteration = next((i for i in sorted_iterations if i.iteration_date > today), None)
120+
next_iteration = next((i for i in sorted_iterations if i.iteration_date > today_uk), None)
118121

119122
if next_iteration:
120123
sys.stdout.write(

src/rules_validation_api/validators/campaign_config_validator.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,15 @@ def validate_iterations_have_unique_number(self) -> typing.Self:
4646
@model_validator(mode="after")
4747
def validate_campaign_has_iteration_within_schedule(self) -> typing.Self:
4848
errors: list[str] = []
49-
iterations_by_date = sorted(self.iterations, key=attrgetter("iteration_date"))
49+
iterations_by_date = sorted(self.iterations, key=attrgetter("iteration_datetime"))
5050

5151
for idx, iteration in enumerate(iterations_by_date):
52-
if iteration.iteration_date < self.start_date:
52+
if iteration.iteration_datetime.date() < self.start_date:
5353
errors.append(
5454
f"CampaignConfig.Iterations.{idx}.IterationDate : "
5555
f"Starts before campaign start date {self.start_date} [type=invalid]"
5656
)
57-
if iteration.iteration_date > self.end_date:
57+
if iteration.iteration_datetime.date() > self.end_date:
5858
errors.append(
5959
f"CampaignConfig.Iterations.{idx}.IterationDate : "
6060
f"Starts after campaign end date {self.end_date} [type=invalid]"

0 commit comments

Comments
 (0)