Initial commit
This commit is contained in:
commit
705c85177a
0
api/__init__.py
Normal file
0
api/__init__.py
Normal file
244
api/app.py
Normal file
244
api/app.py
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
from flask import Flask, request
|
||||||
|
|
||||||
|
from .data import DataFile, DateTimeSpan, MinuteSpan
|
||||||
|
|
||||||
|
|
||||||
|
DATA = DataFile("data.json")
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def get_action_at(point_in_time, schedules, overrides):
|
||||||
|
for override in overrides:
|
||||||
|
if override.start <= point_in_time < override.end:
|
||||||
|
return override.action
|
||||||
|
for span in schedules[point_in_time.weekday()]:
|
||||||
|
span = DateTimeSpan.from_minutespan(point_in_time, span)
|
||||||
|
if span.start <= point_in_time < span.end:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_next_action_change(point_in_time, action, schedules, overrides):
|
||||||
|
skip_schedule = False
|
||||||
|
if not any(schedules.values()):
|
||||||
|
# No daily schedule is set
|
||||||
|
overrides = [o for o in overrides if o.action]
|
||||||
|
if not overrides:
|
||||||
|
# No allow overrides exist => denied indefinitely
|
||||||
|
return None
|
||||||
|
skip_schedule = True
|
||||||
|
elif all(o == [MinuteSpan(0, 24 * 60)] for o in schedules.values()):
|
||||||
|
# Daily schedule allowed 24/7
|
||||||
|
overrides = [o for o in overrides if not o.action]
|
||||||
|
if not overrides:
|
||||||
|
# No deny overrides exist => allowed indefinitely
|
||||||
|
return None
|
||||||
|
skip_schedule = True
|
||||||
|
if skip_schedule:
|
||||||
|
# Schedule is either empty or allows 24/7, therefore irrelevant
|
||||||
|
# and only overrides are considered.
|
||||||
|
for override_span in overrides:
|
||||||
|
if override_span.start > point_in_time:
|
||||||
|
# Nearest override didn't start yet
|
||||||
|
return override_span.start
|
||||||
|
if override_span.start <= point_in_time < override_span.end:
|
||||||
|
# We're in override
|
||||||
|
return override_span.end
|
||||||
|
# No more overrides => allowed / denied indefinitely
|
||||||
|
return None
|
||||||
|
|
||||||
|
while True:
|
||||||
|
new_point_in_time = None
|
||||||
|
for override_span in overrides:
|
||||||
|
if override_span.start <= point_in_time < override_span.end:
|
||||||
|
# We're in override
|
||||||
|
new_point_in_time = override_span.end
|
||||||
|
if new_point_in_time is None:
|
||||||
|
dow = point_in_time.weekday()
|
||||||
|
for span in schedules[dow]:
|
||||||
|
span = DateTimeSpan.from_minutespan(point_in_time, span)
|
||||||
|
if span.end < point_in_time:
|
||||||
|
# Skip already elapsed spans
|
||||||
|
continue
|
||||||
|
if span.start <= point_in_time < span.end:
|
||||||
|
# We're in span allowed by schedule, get the span end
|
||||||
|
new_point_in_time = span.end
|
||||||
|
# Check if any upcoming override comes sooner that the span end
|
||||||
|
for override_span in overrides:
|
||||||
|
if override_span.start >= point_in_time and override_span.start < span.end:
|
||||||
|
new_point_in_time = override_span.start
|
||||||
|
break
|
||||||
|
break
|
||||||
|
if new_point_in_time is None:
|
||||||
|
# We're in span denied by schedule, get next span start
|
||||||
|
days = 0
|
||||||
|
while True:
|
||||||
|
for span in schedules[(dow + days) % 7]:
|
||||||
|
span = DateTimeSpan.from_minutespan(point_in_time + timedelta(days=days), span)
|
||||||
|
if span.start > point_in_time:
|
||||||
|
new_point_in_time = span.start
|
||||||
|
# Check if any upcoming override comes sooner that the span start
|
||||||
|
for override_span in overrides:
|
||||||
|
if (
|
||||||
|
override_span.start >= point_in_time
|
||||||
|
and override_span.start < span.start
|
||||||
|
):
|
||||||
|
new_point_in_time = override_span.start
|
||||||
|
break
|
||||||
|
break
|
||||||
|
if new_point_in_time:
|
||||||
|
break
|
||||||
|
days += 1
|
||||||
|
if action != get_action_at(new_point_in_time, schedules, overrides):
|
||||||
|
return new_point_in_time
|
||||||
|
point_in_time = new_point_in_time
|
||||||
|
|
||||||
|
|
||||||
|
def update_status(status, schedules, overrides):
|
||||||
|
now = datetime.now()
|
||||||
|
status.action = get_action_at(now, schedules, overrides)
|
||||||
|
status.until = get_next_action_change(now, status.action, schedules, overrides)
|
||||||
|
status.then_until = (
|
||||||
|
get_next_action_change(status.until, not status.action, schedules, overrides)
|
||||||
|
if status.until
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def update_schedule(schedule, span, allow):
|
||||||
|
if allow:
|
||||||
|
schedule.append(span)
|
||||||
|
schedule.sort()
|
||||||
|
i = len(schedule) - 1
|
||||||
|
while i:
|
||||||
|
if schedule[i - 1].end >= schedule[i].start:
|
||||||
|
# spans overlap -> merge together
|
||||||
|
schedule[i - 1].start = min(schedule[i - 1].start, schedule[i].start)
|
||||||
|
schedule[i - 1].end = max(schedule[i - 1].end, schedule[i].end)
|
||||||
|
del schedule[i]
|
||||||
|
i -= 1
|
||||||
|
else:
|
||||||
|
i = len(schedule) - 1
|
||||||
|
while i >= 0:
|
||||||
|
if span.start <= schedule[i].start and span.end >= schedule[i].end:
|
||||||
|
# deny period larger than span -> remove span
|
||||||
|
del schedule[i]
|
||||||
|
elif span.start > schedule[i].start and span.end < schedule[i].end:
|
||||||
|
# deny period shorter than span -> split span into two
|
||||||
|
schedule.append(MinuteSpan(span.end, schedule[i].end))
|
||||||
|
schedule[i].end = span.start
|
||||||
|
elif span.start <= schedule[i].start < span.end:
|
||||||
|
# span starts within deny period -> move start
|
||||||
|
schedule[i].start = span.end
|
||||||
|
elif span.start < schedule[i].end <= span.end:
|
||||||
|
# span ends within deny period -> move end
|
||||||
|
schedule[i].end = span.start
|
||||||
|
i -= 1
|
||||||
|
schedule.sort()
|
||||||
|
|
||||||
|
|
||||||
|
def update_overrides(overrides, span):
|
||||||
|
now = datetime.now()
|
||||||
|
if span.end <= now:
|
||||||
|
# The span is in the past, nothing to do
|
||||||
|
return
|
||||||
|
i = len(overrides) - 1
|
||||||
|
while i >= 0:
|
||||||
|
if span.start <= overrides[i].start and span.end >= overrides[i].end:
|
||||||
|
# new override period larger than old override -> remove old override
|
||||||
|
del overrides[i]
|
||||||
|
elif span.start > overrides[i].start and span.end < overrides[i].end:
|
||||||
|
if span.start > now:
|
||||||
|
# new override period shorter than old override -> split old override into two
|
||||||
|
overrides.append(DateTimeSpan(span.end, overrides[i].end, overrides[i].action))
|
||||||
|
overrides[i].end = span.start
|
||||||
|
else:
|
||||||
|
# new override period would split the override, but the first part
|
||||||
|
# has already elapsed -> move old override start
|
||||||
|
overrides[i].start = span.end
|
||||||
|
elif span.start <= overrides[i].start < span.end:
|
||||||
|
# new override starts within old overrides period -> move old override start
|
||||||
|
overrides[i].start = span.end
|
||||||
|
elif span.start < overrides[i].end <= span.end:
|
||||||
|
# new override ends within old overrides period -> move old override end
|
||||||
|
overrides[i].end = span.start
|
||||||
|
i -= 1
|
||||||
|
overrides.append(span)
|
||||||
|
overrides.sort()
|
||||||
|
i = len(overrides) - 1
|
||||||
|
while i:
|
||||||
|
if (
|
||||||
|
overrides[i - 1].end >= overrides[i].start
|
||||||
|
and overrides[i - 1].action == overrides[i].action
|
||||||
|
):
|
||||||
|
# overrides of the same type overlap -> merge together
|
||||||
|
overrides[i - 1].start = min(overrides[i - 1].start, overrides[i].start)
|
||||||
|
overrides[i - 1].end = max(overrides[i - 1].end, overrides[i].end)
|
||||||
|
del overrides[i]
|
||||||
|
i -= 1
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/data")
|
||||||
|
def get_data():
|
||||||
|
with DATA as data:
|
||||||
|
if data.status.until and datetime.now() >= data.status.until:
|
||||||
|
update_status(data.status, data.schedules, data.overrides)
|
||||||
|
data.save()
|
||||||
|
return data.to_dict()
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/override")
|
||||||
|
def post_override():
|
||||||
|
span = DateTimeSpan.from_args(**request.json)
|
||||||
|
with DATA as data:
|
||||||
|
update_overrides(data.overrides, span)
|
||||||
|
update_status(data.status, data.schedules, data.overrides)
|
||||||
|
data.save()
|
||||||
|
return data.to_dict()
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/delete-override")
|
||||||
|
def delete_override():
|
||||||
|
span = DateTimeSpan.from_args(**request.json)
|
||||||
|
with DATA as data:
|
||||||
|
try:
|
||||||
|
data.overrides.remove(span)
|
||||||
|
update_status(data.status, data.schedules, data.overrides)
|
||||||
|
data.save()
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
return data.to_dict()
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/schedule")
|
||||||
|
def post_schedule():
|
||||||
|
schedule = request.json
|
||||||
|
span = MinuteSpan.from_args(**schedule)
|
||||||
|
with DATA as data:
|
||||||
|
for day in schedule["days"]:
|
||||||
|
update_schedule(data.schedules[day], span, schedule["action"])
|
||||||
|
update_status(data.status, data.schedules, data.overrides)
|
||||||
|
data.save()
|
||||||
|
return data.to_dict()
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/fast-action/<int:minutes>")
|
||||||
|
def fast_action(minutes):
|
||||||
|
with DATA as data:
|
||||||
|
now = datetime.now().replace(second=0, microsecond=0)
|
||||||
|
if minutes:
|
||||||
|
start = now
|
||||||
|
action = get_action_at(now, data.schedules, data.overrides)
|
||||||
|
if action:
|
||||||
|
start = get_next_action_change(now, action, data.schedules, data.overrides)
|
||||||
|
end = start + timedelta(minutes=minutes)
|
||||||
|
span = DateTimeSpan(start, end, True)
|
||||||
|
else:
|
||||||
|
span = DateTimeSpan(now, now + timedelta(minutes=5), False)
|
||||||
|
update_overrides(data.overrides, span)
|
||||||
|
update_status(data.status, data.schedules, data.overrides)
|
||||||
|
data.save()
|
||||||
|
return data.to_dict()
|
144
api/data.py
Normal file
144
api/data.py
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
import fcntl
|
||||||
|
import json
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
MINS = "minutes"
|
||||||
|
|
||||||
|
|
||||||
|
class Span:
|
||||||
|
def __init__(self, start, end):
|
||||||
|
self.start = min(start, end)
|
||||||
|
self.end = max(start, end)
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return (self.start, self.end) == (other.start, other.end)
|
||||||
|
|
||||||
|
def __lt__(self, other):
|
||||||
|
return (self.start, self.end) < (other.start, other.end)
|
||||||
|
|
||||||
|
|
||||||
|
class MinuteSpan(Span):
|
||||||
|
@classmethod
|
||||||
|
def from_args(cls, **kwargs):
|
||||||
|
start = kwargs["start"].split(":")
|
||||||
|
start = int(start[0]) * 60 + int(start[1])
|
||||||
|
end = kwargs["end"].split(":")
|
||||||
|
end = int(end[0]) * 60 + int(end[1])
|
||||||
|
if end == 0:
|
||||||
|
# To 00:00 should mean until midnight (24:00)
|
||||||
|
end = 24 * 60
|
||||||
|
return cls(start, end)
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {
|
||||||
|
"start": f"{self.start // 60}:{self.start % 60:02}",
|
||||||
|
"end": f"{self.end // 60}:{self.end % 60:02}",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class DateTimeSpan(Span):
|
||||||
|
def __init__(self, start, end, action):
|
||||||
|
super().__init__(start, end)
|
||||||
|
self.action = action
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return (self.start, self.end, self.action) == (other.start, other.end, other.action)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_args(cls, **kwargs):
|
||||||
|
return cls(
|
||||||
|
datetime.fromisoformat(kwargs["start"]),
|
||||||
|
datetime.fromisoformat(kwargs["end"]),
|
||||||
|
kwargs["action"],
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_minutespan(cls, date, minutespan):
|
||||||
|
start = minutespan.start
|
||||||
|
end = minutespan.end
|
||||||
|
add_days = 0
|
||||||
|
if end == 24 * 60:
|
||||||
|
# Ending midnight needs to be set as 0:00 of the next day
|
||||||
|
end = 0
|
||||||
|
add_days = 1
|
||||||
|
return cls(
|
||||||
|
date.replace(hour=start // 60, minute=start % 60, second=0, microsecond=0),
|
||||||
|
date.replace(hour=end // 60, minute=end % 60, second=0, microsecond=0)
|
||||||
|
+ timedelta(days=add_days),
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {
|
||||||
|
"start": self.start.isoformat(timespec=MINS),
|
||||||
|
"end": self.end.isoformat(timespec=MINS),
|
||||||
|
"action": self.action,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Status:
|
||||||
|
def __init__(self, action, until=None, then_until=None):
|
||||||
|
self.action = action
|
||||||
|
self.until = until
|
||||||
|
self.then_until = then_until
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_args(cls, **kwargs):
|
||||||
|
until = kwargs.get("until")
|
||||||
|
then_until = kwargs.get("then_until")
|
||||||
|
return cls(
|
||||||
|
kwargs["action"],
|
||||||
|
datetime.fromisoformat(until) if until else None,
|
||||||
|
datetime.fromisoformat(then_until) if then_until else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {
|
||||||
|
"action": self.action,
|
||||||
|
"until": self.until.isoformat(timespec=MINS) if self.until else None,
|
||||||
|
"then_until": self.then_until.isoformat(timespec=MINS) if self.then_until else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class DataFile:
|
||||||
|
def __init__(self, path):
|
||||||
|
self._path = Path(path)
|
||||||
|
self._file = None
|
||||||
|
self.status = None
|
||||||
|
self.schedules = None
|
||||||
|
self.overrides = None
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
self._file = self._path.open("r+", encoding="utf-8")
|
||||||
|
fcntl.flock(self._file, fcntl.LOCK_EX)
|
||||||
|
data = json.load(self._file)
|
||||||
|
self.status = Status.from_args(**data.get("status", {"action": False}))
|
||||||
|
self.schedules = {
|
||||||
|
int(day): [MinuteSpan.from_args(**span) for span in schedule]
|
||||||
|
for day, schedule in data.get("schedules", {day: [] for day in range(7)}).items()
|
||||||
|
}
|
||||||
|
overrides = [DateTimeSpan.from_args(**override) for override in data.get("overrides", [])]
|
||||||
|
# Remove expired overrides
|
||||||
|
now = datetime.now()
|
||||||
|
self.overrides = [o for o in overrides if o.end > now]
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_value, traceback):
|
||||||
|
self._file.close()
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {
|
||||||
|
"status": self.status.to_dict(),
|
||||||
|
"schedules": {
|
||||||
|
day: [span.to_dict() for span in schedule]
|
||||||
|
for day, schedule in self.schedules.items()
|
||||||
|
},
|
||||||
|
"overrides": [override.to_dict() for override in self.overrides],
|
||||||
|
}
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
self._file.seek(0)
|
||||||
|
self._file.truncate(0)
|
||||||
|
json.dump(self.to_dict(), self._file)
|
5
client/Installer/Installer.wixproj
Normal file
5
client/Installer/Installer.wixproj
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<Project Sdk="WixToolset.Sdk/4.0.0">
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\ParentalControlService\ParentalControlService.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
41
client/Installer/Package.wxs
Normal file
41
client/Installer/Package.wxs
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
|
||||||
|
<Package Name="Marek Parental Control" Manufacturer="Disassembler" Version="1.0.0.0" UpgradeCode="feb429a6-684e-4506-816f-006384136754" Compressed="true">
|
||||||
|
<!-- Allow upgrades and prevent downgrades -->
|
||||||
|
<MajorUpgrade DowngradeErrorMessage="A later version of [ProductName] is already installed. Setup will now exit." />
|
||||||
|
|
||||||
|
<!-- Define the directory structure -->
|
||||||
|
<StandardDirectory Id="ProgramFiles64Folder">
|
||||||
|
<Directory Id="INSTALLDIR" Name="Marek Parental Control" />
|
||||||
|
</StandardDirectory>
|
||||||
|
|
||||||
|
<DirectoryRef Id="INSTALLDIR">
|
||||||
|
<Component Id="ServiceExecutable" Bitness="always64">
|
||||||
|
<File Id="ParentalControlService.exe"
|
||||||
|
Source="$(var.ParentalControlService.TargetDir)publish\ParentalControlService.exe"
|
||||||
|
KeyPath="true" />
|
||||||
|
|
||||||
|
<RemoveFile Id="ALLFILES" Name="*.*" On="both" />
|
||||||
|
|
||||||
|
<ServiceInstall Id="ServiceInstaller"
|
||||||
|
Type="ownProcess"
|
||||||
|
Name="ParentalControlService"
|
||||||
|
DisplayName="Marek Parental Control"
|
||||||
|
Description="Marek Parental Control"
|
||||||
|
Start="auto"
|
||||||
|
ErrorControl="normal" />
|
||||||
|
|
||||||
|
<ServiceControl Id="StartService"
|
||||||
|
Start="install"
|
||||||
|
Stop="both"
|
||||||
|
Remove="uninstall"
|
||||||
|
Name="ParentalControlService"
|
||||||
|
Wait="true" />
|
||||||
|
</Component>
|
||||||
|
</DirectoryRef>
|
||||||
|
|
||||||
|
<Feature Id="Service" Title="Marek Parental Control Setup" Level="1">
|
||||||
|
<ComponentRef Id="ServiceExecutable" />
|
||||||
|
</Feature>
|
||||||
|
|
||||||
|
</Package>
|
||||||
|
</Wix>
|
61
client/ParentalControlService.sln
Normal file
61
client/ParentalControlService.sln
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
# Visual Studio Version 17
|
||||||
|
VisualStudioVersion = 17.7.34024.191
|
||||||
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ParentalControlService", "ParentalControlService\ParentalControlService.csproj", "{050D342F-0A53-4A08-B56A-AA63A22A74A9}"
|
||||||
|
EndProject
|
||||||
|
Project("{B7DD6F7E-DEF8-4E67-B5B7-07EF123DB6F0}") = "Installer", "Installer\Installer.wixproj", "{C4838DE1-3AC8-4931-80C2-340EBA94996F}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Debug|ARM64 = Debug|ARM64
|
||||||
|
Debug|x64 = Debug|x64
|
||||||
|
Debug|x86 = Debug|x86
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
Release|ARM64 = Release|ARM64
|
||||||
|
Release|x64 = Release|x64
|
||||||
|
Release|x86 = Release|x86
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{050D342F-0A53-4A08-B56A-AA63A22A74A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{050D342F-0A53-4A08-B56A-AA63A22A74A9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{050D342F-0A53-4A08-B56A-AA63A22A74A9}.Debug|ARM64.ActiveCfg = Debug|Any CPU
|
||||||
|
{050D342F-0A53-4A08-B56A-AA63A22A74A9}.Debug|ARM64.Build.0 = Debug|Any CPU
|
||||||
|
{050D342F-0A53-4A08-B56A-AA63A22A74A9}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{050D342F-0A53-4A08-B56A-AA63A22A74A9}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{050D342F-0A53-4A08-B56A-AA63A22A74A9}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{050D342F-0A53-4A08-B56A-AA63A22A74A9}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{050D342F-0A53-4A08-B56A-AA63A22A74A9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{050D342F-0A53-4A08-B56A-AA63A22A74A9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{050D342F-0A53-4A08-B56A-AA63A22A74A9}.Release|ARM64.ActiveCfg = Release|Any CPU
|
||||||
|
{050D342F-0A53-4A08-B56A-AA63A22A74A9}.Release|ARM64.Build.0 = Release|Any CPU
|
||||||
|
{050D342F-0A53-4A08-B56A-AA63A22A74A9}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{050D342F-0A53-4A08-B56A-AA63A22A74A9}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{050D342F-0A53-4A08-B56A-AA63A22A74A9}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{050D342F-0A53-4A08-B56A-AA63A22A74A9}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{C4838DE1-3AC8-4931-80C2-340EBA94996F}.Debug|Any CPU.ActiveCfg = Debug|x64
|
||||||
|
{C4838DE1-3AC8-4931-80C2-340EBA94996F}.Debug|Any CPU.Build.0 = Debug|x64
|
||||||
|
{C4838DE1-3AC8-4931-80C2-340EBA94996F}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||||
|
{C4838DE1-3AC8-4931-80C2-340EBA94996F}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||||
|
{C4838DE1-3AC8-4931-80C2-340EBA94996F}.Debug|x64.ActiveCfg = Debug|x64
|
||||||
|
{C4838DE1-3AC8-4931-80C2-340EBA94996F}.Debug|x64.Build.0 = Debug|x64
|
||||||
|
{C4838DE1-3AC8-4931-80C2-340EBA94996F}.Debug|x86.ActiveCfg = Debug|x86
|
||||||
|
{C4838DE1-3AC8-4931-80C2-340EBA94996F}.Debug|x86.Build.0 = Debug|x86
|
||||||
|
{C4838DE1-3AC8-4931-80C2-340EBA94996F}.Release|Any CPU.ActiveCfg = Release|x64
|
||||||
|
{C4838DE1-3AC8-4931-80C2-340EBA94996F}.Release|Any CPU.Build.0 = Release|x64
|
||||||
|
{C4838DE1-3AC8-4931-80C2-340EBA94996F}.Release|ARM64.ActiveCfg = Release|ARM64
|
||||||
|
{C4838DE1-3AC8-4931-80C2-340EBA94996F}.Release|ARM64.Build.0 = Release|ARM64
|
||||||
|
{C4838DE1-3AC8-4931-80C2-340EBA94996F}.Release|x64.ActiveCfg = Release|x64
|
||||||
|
{C4838DE1-3AC8-4931-80C2-340EBA94996F}.Release|x64.Build.0 = Release|x64
|
||||||
|
{C4838DE1-3AC8-4931-80C2-340EBA94996F}.Release|x86.ActiveCfg = Release|x86
|
||||||
|
{C4838DE1-3AC8-4931-80C2-340EBA94996F}.Release|x86.Build.0 = Release|x86
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
|
HideSolutionNode = FALSE
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
|
SolutionGuid = {E4EF7269-DAC7-4C33-9AF1-FCAEBAE0AD77}
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
69
client/ParentalControlService/Data.cs
Normal file
69
client/ParentalControlService/Data.cs
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace ParentalControlService {
|
||||||
|
public struct Status
|
||||||
|
{
|
||||||
|
[JsonPropertyName("action")]
|
||||||
|
public bool Action { get; set; }
|
||||||
|
[JsonPropertyName("until")]
|
||||||
|
public DateTime? Until { get; set; }
|
||||||
|
[JsonPropertyName("then_until")]
|
||||||
|
public DateTime? ThenUntil { get; set; }
|
||||||
|
|
||||||
|
public override bool Equals(object? obj) {
|
||||||
|
return obj is Status other && Equals(other);
|
||||||
|
}
|
||||||
|
public readonly bool Equals(Status other) {
|
||||||
|
return Action == other.Action && Until == other.Until && ThenUntil == other.ThenUntil;
|
||||||
|
}
|
||||||
|
public override readonly int GetHashCode() {
|
||||||
|
return HashCode.Combine(Action, Until, ThenUntil);
|
||||||
|
}
|
||||||
|
public static bool operator ==(Status lhs, Status rhs) {
|
||||||
|
return lhs.Action == rhs.Action && lhs.Until == rhs.Until && lhs.ThenUntil == rhs.ThenUntil;
|
||||||
|
}
|
||||||
|
public static bool operator !=(Status lhs, Status rhs) {
|
||||||
|
return !(lhs == rhs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct Schedule
|
||||||
|
{
|
||||||
|
[JsonPropertyName("start")]
|
||||||
|
public string? Start { get; set; }
|
||||||
|
[JsonPropertyName("end")]
|
||||||
|
public string? End { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct Override
|
||||||
|
{
|
||||||
|
[JsonPropertyName("start")]
|
||||||
|
public DateTime Start { get; set; }
|
||||||
|
[JsonPropertyName("end")]
|
||||||
|
public DateTime End { get; set; }
|
||||||
|
[JsonPropertyName("action")]
|
||||||
|
public bool Action { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct Data
|
||||||
|
{
|
||||||
|
[JsonPropertyName("status")]
|
||||||
|
public Status Status { get; set; }
|
||||||
|
[JsonPropertyName("schedules")]
|
||||||
|
public Dictionary<int,List<Schedule>> Schedules { get; set; }
|
||||||
|
[JsonPropertyName("overrides")]
|
||||||
|
public List<Override> Overrides { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct Config
|
||||||
|
{
|
||||||
|
[JsonPropertyName("url")]
|
||||||
|
public string Url { get; set; }
|
||||||
|
[JsonPropertyName("username")]
|
||||||
|
public string Username { get; set; }
|
||||||
|
[JsonPropertyName("password")]
|
||||||
|
public string Password { get; set; }
|
||||||
|
}
|
||||||
|
}
|
18
client/ParentalControlService/ParentalControlService.csproj
Normal file
18
client/ParentalControlService/ParentalControlService.csproj
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Worker">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net7.0-windows</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>disable</ImplicitUsings>
|
||||||
|
<UserSecretsId>dotnet-ParentalControlService-c499de7d-03ae-4d69-a7cf-e634d0dce29d</UserSecretsId>
|
||||||
|
<OutputType>exe</OutputType>
|
||||||
|
<PublishSingleFile Condition="'$(Configuration)' == 'Release'">true</PublishSingleFile>
|
||||||
|
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||||
|
<PlatformTarget>x64</PlatformTarget>
|
||||||
|
<Version>1.1.0</Version>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="7.0.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
|
<PropertyGroup>
|
||||||
|
<_LastSelectedProfileId>C:\Users\Disassembler\Desktop\client\ParentalControlService\Properties\PublishProfiles\FolderProfile.pubxml</_LastSelectedProfileId>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
134
client/ParentalControlService/ParentalControlSvc.cs
Normal file
134
client/ParentalControlService/ParentalControlSvc.cs
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Timers;
|
||||||
|
using Timer = System.Timers.Timer;
|
||||||
|
|
||||||
|
namespace ParentalControlService;
|
||||||
|
|
||||||
|
public class ParentalControlSvc : BackgroundService
|
||||||
|
{
|
||||||
|
private readonly string configPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "config.json");
|
||||||
|
private readonly string statusPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "status.json");
|
||||||
|
private readonly ILogger<ParentalControlSvc> _logger;
|
||||||
|
private Config config;
|
||||||
|
private Status status;
|
||||||
|
private bool currentAction;
|
||||||
|
private readonly Timer checkTimer;
|
||||||
|
private readonly HttpClient httpClient;
|
||||||
|
|
||||||
|
public ParentalControlSvc(ILogger<ParentalControlSvc> logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
|
||||||
|
// Load status
|
||||||
|
try {
|
||||||
|
status = JsonSerializer.Deserialize<Status>(File.ReadAllText(statusPath))!;
|
||||||
|
}
|
||||||
|
catch (FileNotFoundException) {
|
||||||
|
status = new Status() { Action = true, Until = DateTime.MaxValue };
|
||||||
|
}
|
||||||
|
currentAction = status.Action;
|
||||||
|
|
||||||
|
// Load config
|
||||||
|
try
|
||||||
|
{
|
||||||
|
config = JsonSerializer.Deserialize<Config>(File.ReadAllText(configPath))!;
|
||||||
|
}
|
||||||
|
catch (FileNotFoundException)
|
||||||
|
{
|
||||||
|
config = new Config() { Username = "", Password = "" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create HTTP Client
|
||||||
|
httpClient = new() { BaseAddress = new Uri(config.Url), Timeout = TimeSpan.FromSeconds(5) };
|
||||||
|
string credentials = Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes($"{config.Username}:{config.Password}"));
|
||||||
|
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", credentials);
|
||||||
|
httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("ParentalControl", "1.1.0"));
|
||||||
|
|
||||||
|
// Initialize Timer
|
||||||
|
checkTimer = new(5000);
|
||||||
|
checkTimer.Elapsed += new ElapsedEventHandler(CheckTimerTick);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
await CheckTimerTick();
|
||||||
|
checkTimer.Start();
|
||||||
|
while (!stoppingToken.IsCancellationRequested) {
|
||||||
|
await Task.Delay(1000, stoppingToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (TaskCanceledException) {
|
||||||
|
checkTimer.Stop();
|
||||||
|
}
|
||||||
|
catch (Exception ex) {
|
||||||
|
_logger.LogError(ex, "{Message}", ex.Message);
|
||||||
|
Environment.Exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SaveStatus() {
|
||||||
|
File.WriteAllText(statusPath, JsonSerializer.Serialize(status));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RunScript(string scriptName) {
|
||||||
|
scriptName = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, $"{scriptName}.ps1");
|
||||||
|
if (!File.Exists(scriptName)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ProcessStartInfo psi = new() {
|
||||||
|
FileName = "powershell.exe",
|
||||||
|
Arguments = $"-ExecutionPolicy Bypass -File \"{scriptName}\"",
|
||||||
|
UseShellExecute = false,
|
||||||
|
WorkingDirectory = AppDomain.CurrentDomain.BaseDirectory,
|
||||||
|
};
|
||||||
|
Process.Start(psi);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CheckTimerTick() {
|
||||||
|
try {
|
||||||
|
Data data = await httpClient.GetFromJsonAsync<Data>("api/data");
|
||||||
|
if (data.Status != status) {
|
||||||
|
status = data.Status;
|
||||||
|
SaveStatus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException) {
|
||||||
|
_logger.LogError(ex, "{Message}", ex.Message);
|
||||||
|
}
|
||||||
|
if (status.Until != null && DateTime.Now >= status.Until) {
|
||||||
|
// Fetch failed, we need to advance the schedule on our own
|
||||||
|
status.Action = !status.Action;
|
||||||
|
status.Until = status.ThenUntil;
|
||||||
|
status.ThenUntil = null;
|
||||||
|
SaveStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentAction == status.Action) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
currentAction = !currentAction;
|
||||||
|
if (currentAction) {
|
||||||
|
_logger.LogInformation("[{time}] Unblocked", DateTime.Now);
|
||||||
|
RunScript("unblock");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
_logger.LogInformation("[{time}] Blocked", DateTime.Now);
|
||||||
|
RunScript("block");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void CheckTimerTick(object? sender, ElapsedEventArgs e) {
|
||||||
|
await CheckTimerTick();
|
||||||
|
}
|
||||||
|
}
|
20
client/ParentalControlService/Program.cs
Normal file
20
client/ParentalControlService/Program.cs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging.Configuration;
|
||||||
|
using Microsoft.Extensions.Logging.EventLog;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using ParentalControlService;
|
||||||
|
|
||||||
|
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
|
||||||
|
builder.Services.AddWindowsService(options => {
|
||||||
|
options.ServiceName = "Marek Parental Control";
|
||||||
|
});
|
||||||
|
|
||||||
|
LoggerProviderOptions.RegisterProviderOptions<EventLogSettings, EventLogLoggerProvider>(builder.Services);
|
||||||
|
|
||||||
|
builder.Services.AddSingleton<ParentalControlSvc>();
|
||||||
|
builder.Services.AddHostedService<ParentalControlSvc>();
|
||||||
|
builder.Logging.AddConfiguration(builder.Configuration.GetSection("Logging"));
|
||||||
|
|
||||||
|
IHost host = builder.Build();
|
||||||
|
host.Run();
|
@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||||
|
-->
|
||||||
|
<Project>
|
||||||
|
<PropertyGroup>
|
||||||
|
<Configuration>Release</Configuration>
|
||||||
|
<Platform>Any CPU</Platform>
|
||||||
|
<PublishDir>bin\Release\net7.0-windows\win-x64\publish\</PublishDir>
|
||||||
|
<PublishProtocol>FileSystem</PublishProtocol>
|
||||||
|
<_TargetId>Folder</_TargetId>
|
||||||
|
<TargetFramework>net7.0-windows</TargetFramework>
|
||||||
|
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||||
|
<SelfContained>true</SelfContained>
|
||||||
|
<PublishReadyToRun>true</PublishReadyToRun>
|
||||||
|
<PublishSingleFile>true</PublishSingleFile>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
11
client/ParentalControlService/Properties/launchSettings.json
Normal file
11
client/ParentalControlService/Properties/launchSettings.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"profiles": {
|
||||||
|
"ParentalControlService": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"environmentVariables": {
|
||||||
|
"DOTNET_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.Hosting.Lifetime": "Information"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
8
client/ParentalControlService/appsettings.json
Normal file
8
client/ParentalControlService/appsettings.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.Hosting.Lifetime": "Information"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
8
client/block.ps1
Normal file
8
client/block.ps1
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
Start-Sleep 1
|
||||||
|
C:\Windows\System32\net.exe user Marek /active:no
|
||||||
|
$session_id = ((C:\Windows\System32\quser.exe | Where-Object {$_ -match 'Marek' }) -split ' +')[2]
|
||||||
|
If ($session_id) {
|
||||||
|
logoff $session_id
|
||||||
|
Start-Sleep 1
|
||||||
|
shutdown /s /f /t 0
|
||||||
|
}
|
5
client/config.json
Normal file
5
client/config.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"url": "https://pc.dasm.cz",
|
||||||
|
"username": "parent",
|
||||||
|
"password": "DA2ptE{!my_hSN6="
|
||||||
|
}
|
1
client/unblock.ps1
Normal file
1
client/unblock.ps1
Normal file
@ -0,0 +1 @@
|
|||||||
|
C:\Windows\System32\net.exe user Marek /active:yes
|
1
data.json
Normal file
1
data.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"schedules": {"0": [], "1": [], "2": [], "3": [], "4": [], "5": [], "6": []}, "overrides": []}
|
71
index.html
Normal file
71
index.html
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="cs">
|
||||||
|
<head>
|
||||||
|
<title>Parental Control</title>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="icon" href="data:;base64,iVBORw0KGgo=">
|
||||||
|
<link rel="stylesheet" href="static/style.css" type="text/css" media="all">
|
||||||
|
<script type="text/javascript" src="static/script.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div>
|
||||||
|
<h2>Stav</h2>
|
||||||
|
<div id="status"> </div>
|
||||||
|
<input class="fast-action" data-mins="15" type="button" value="+15 min">
|
||||||
|
<input class="fast-action" data-mins="30" type="button" value="+30 min">
|
||||||
|
<input class="fast-action" data-mins="60" type="button" value="+1 hod">
|
||||||
|
<input class="fast-action" data-mins="0" type="button" value="Vypnout hned">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2>Výjimky</h2>
|
||||||
|
<div id="overrides"></div>
|
||||||
|
<form id="add-override">
|
||||||
|
<label for="override-action">Akce:</label>
|
||||||
|
<select id="override-action">
|
||||||
|
<option value="allow">Povolit</option>
|
||||||
|
<option value="deny">Zakázat</option>
|
||||||
|
</select><br>
|
||||||
|
<label for="override-start">Od:</label>
|
||||||
|
<input id="override-start" type="datetime-local"><br>
|
||||||
|
<label for="override-end">Do:</label>
|
||||||
|
<input id="override-end" type="datetime-local"><br>
|
||||||
|
<input type="submit" value="Přidat">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2>Plán</h2>
|
||||||
|
<div id="schedules">
|
||||||
|
<span>Pondělí:</span><span id="schedule-0"></span>
|
||||||
|
<span>Úterý:</span><span id="schedule-1"></span>
|
||||||
|
<span>Středa:</span><span id="schedule-2"></span>
|
||||||
|
<span>Čtvrtek:</span><span id="schedule-3"></span>
|
||||||
|
<span>Pátek:</span><span id="schedule-4"></span>
|
||||||
|
<span>Sobota:</span><span id="schedule-5"></span>
|
||||||
|
<span>Neděle:</span><span id="schedule-6"></span>
|
||||||
|
</div>
|
||||||
|
<form id="update-schedule">
|
||||||
|
<label for="schedule-action">Akce:</label>
|
||||||
|
<select id="schedule-action">
|
||||||
|
<option value="allow">Povolit</option>
|
||||||
|
<option value="deny">Zakázat</option>
|
||||||
|
</select>
|
||||||
|
<label for="schedule-days">Den:</label>
|
||||||
|
<select id="schedule-days" size="7" multiple>
|
||||||
|
<option value="0">Pondělí</option>
|
||||||
|
<option value="1">Úterý</option>
|
||||||
|
<option value="2">Středa</option>
|
||||||
|
<option value="3">Čtvrtek</option>
|
||||||
|
<option value="4">Pátek</option>
|
||||||
|
<option value="5">Sobota</option>
|
||||||
|
<option value="6">Neděle</option>
|
||||||
|
</select>
|
||||||
|
<label for="schedule-start">Od:</label>
|
||||||
|
<input id="schedule-start" type="time">
|
||||||
|
<label for="schedule-end">Do:</label>
|
||||||
|
<input id="schedule-end" type="time">
|
||||||
|
<input type="submit" value="Přidat">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
18
pyproject.toml
Normal file
18
pyproject.toml
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["setuptools"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "parental-control"
|
||||||
|
version = "0.0.1"
|
||||||
|
authors = [
|
||||||
|
{name = "Disassembler", email = "disassembler@dasm.cz"},
|
||||||
|
]
|
||||||
|
requires-python = ">=3.6"
|
||||||
|
license = {text = "MIT"}
|
||||||
|
dependencies = [
|
||||||
|
"flask",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.setuptools]
|
||||||
|
packages = ["api"]
|
122
static/script.js
Normal file
122
static/script.js
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
function humanReadableDate(str) {
|
||||||
|
let date = new Date();
|
||||||
|
let today = date.toISOString().split("T")[0];
|
||||||
|
date.setDate(date.getDate() - 1);
|
||||||
|
let yesterday = date.toISOString().split("T")[0];
|
||||||
|
date.setDate(date.getDate() + 2);
|
||||||
|
let tomorrow = date.toISOString().split("T")[0];
|
||||||
|
return str.replace("T", " ").replaceAll(yesterday, "včera").replaceAll(today, "dnes").replaceAll(tomorrow, "zítra");
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshStatus(data) {
|
||||||
|
let status = document.getElementById("status");
|
||||||
|
let action, then_action;
|
||||||
|
if (data["action"]) {
|
||||||
|
action = "Povoleno"
|
||||||
|
then_action = "zakázáno"
|
||||||
|
} else {
|
||||||
|
action = "Zakázáno"
|
||||||
|
then_action = "povoleno"
|
||||||
|
}
|
||||||
|
let text = action;
|
||||||
|
if (data["until"]) {
|
||||||
|
text += " do " + humanReadableDate(data["until"]) + ", pak " + then_action;
|
||||||
|
if (data["then_until"]) {
|
||||||
|
text += " do " + humanReadableDate(data["then_until"]);
|
||||||
|
} else {
|
||||||
|
text += " napořád"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
text += " napořád"
|
||||||
|
}
|
||||||
|
status.textContent = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshOverrides(data) {
|
||||||
|
let overrides = document.getElementById("overrides");
|
||||||
|
overrides.innerHTML = "";
|
||||||
|
for (const record of data) {
|
||||||
|
let overrideText = document.createElement("span");
|
||||||
|
overrideText.textContent = (record["action"] ? "Povoleno" : "Zakázáno") + " od " + humanReadableDate(record["start"]) + " do " + humanReadableDate(record["end"]);
|
||||||
|
let overrideButton = document.createElement("input");
|
||||||
|
overrideButton.type = "button";
|
||||||
|
overrideButton.value = "❌";
|
||||||
|
overrideButton.addEventListener("click", async () => {
|
||||||
|
let response = await fetch("api/delete-override", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {"Content-Type": "application/json"},
|
||||||
|
body: JSON.stringify({
|
||||||
|
action: record["action"],
|
||||||
|
start: record["start"],
|
||||||
|
end: record["end"]
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
await refresh(await response.json());
|
||||||
|
})
|
||||||
|
overrides.appendChild(overrideText);
|
||||||
|
overrides.appendChild(overrideButton);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshSchedules(data) {
|
||||||
|
for (const [day, entries] of Object.entries(data)) {
|
||||||
|
let schedule = "Zakázáno"
|
||||||
|
if (entries.length > 0) {
|
||||||
|
schedule = entries.map((entry) => entry["start"] + " - " + entry["end"]).join(", ");
|
||||||
|
}
|
||||||
|
document.getElementById("schedule-" + day).textContent = schedule;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refresh(data) {
|
||||||
|
data = data || await (await fetch("api/data")).json();
|
||||||
|
refreshStatus(data["status"]);
|
||||||
|
refreshOverrides(data["overrides"]);
|
||||||
|
refreshSchedules(data["schedules"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addOverride(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
let response = await fetch("api/override", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {"Content-Type": "application/json"},
|
||||||
|
body: JSON.stringify({
|
||||||
|
action: document.getElementById("override-action").value == "allow",
|
||||||
|
start: document.getElementById("override-start").value,
|
||||||
|
end: document.getElementById("override-end").value,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
await refresh(await response.json());
|
||||||
|
document.getElementById("add-override").reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateSchedule(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
let response = await fetch("api/schedule", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {"Content-Type": "application/json"},
|
||||||
|
body: JSON.stringify({
|
||||||
|
action: document.getElementById("schedule-action").value == "allow",
|
||||||
|
days: Array.from(document.getElementById("schedule-days").selectedOptions).map(v => parseInt(v.value)),
|
||||||
|
start: document.getElementById("schedule-start").value,
|
||||||
|
end: document.getElementById("schedule-end").value
|
||||||
|
})
|
||||||
|
});
|
||||||
|
await refresh(await response.json());
|
||||||
|
document.getElementById("update-schedule").reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fastAction(e) {
|
||||||
|
let response = await fetch("api/fast-action/" + e.target.dataset.mins);
|
||||||
|
await refresh(await response.json());
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("DOMContentLoaded", () => {
|
||||||
|
refresh();
|
||||||
|
document.getElementById("add-override").addEventListener("submit", addOverride);
|
||||||
|
document.getElementById("update-schedule").addEventListener("submit", updateSchedule);
|
||||||
|
Array.from(document.getElementsByClassName("fast-action")).forEach(function(element) {
|
||||||
|
element.addEventListener("click", fastAction);
|
||||||
|
});
|
||||||
|
window.setInterval(refresh, 5000);
|
||||||
|
});
|
65
static/style.css
Normal file
65
static/style.css
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
body {
|
||||||
|
max-width: 500px;
|
||||||
|
font-family: serif;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
margin: 1em 0 0.5em;
|
||||||
|
}
|
||||||
|
form {
|
||||||
|
display: grid;
|
||||||
|
padding: 0 1em 1em 1em;
|
||||||
|
background: #eee;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
input, select {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #999;
|
||||||
|
padding: 0.5em;
|
||||||
|
}
|
||||||
|
input[type="button"] {
|
||||||
|
background: #eee;
|
||||||
|
}
|
||||||
|
input[type="button"]:hover {
|
||||||
|
background: #ddd;
|
||||||
|
}
|
||||||
|
input[type="submit"] {
|
||||||
|
margin-top: 0.5em;
|
||||||
|
}
|
||||||
|
input[type="submit"]:hover {
|
||||||
|
background: #f6f6f6;
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
padding: 0.5em 0.5em 0.5em 0;
|
||||||
|
}
|
||||||
|
#status {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
#overrides {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 30px;
|
||||||
|
margin-top: -0.4em;
|
||||||
|
}
|
||||||
|
#schedules {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 5em 1fr;
|
||||||
|
margin-top: -0.4em;
|
||||||
|
}
|
||||||
|
#overrides span, #schedules span {
|
||||||
|
margin: 0.5em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 400px) {
|
||||||
|
form {
|
||||||
|
grid-template-columns: 50px 1fr;
|
||||||
|
grid-gap: 2px;
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
|
form label {
|
||||||
|
grid-column: 1 / 2;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
form input, form select {
|
||||||
|
grid-column: 2 / 3;
|
||||||
|
}
|
||||||
|
}
|
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
389
tests/test_app.py
Normal file
389
tests/test_app.py
Normal file
@ -0,0 +1,389 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from parental_control import DATETIME_FORMAT, app
|
||||||
|
from parental_control.span import DateTimeSpan, MinuteSpan
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"point_in_time, schedules, overrides, expected_result",
|
||||||
|
[
|
||||||
|
# Point in time within override
|
||||||
|
(
|
||||||
|
"2023-08-24 14:00",
|
||||||
|
{day: [] for day in range(7)},
|
||||||
|
[{"start": "2023-08-24 12:00", "end": "2023-08-24 18:00", "action": True}],
|
||||||
|
True,
|
||||||
|
),
|
||||||
|
# Point in time within schedule
|
||||||
|
(
|
||||||
|
"2023-08-24 14:00",
|
||||||
|
{3: [{"start": "10:00", "end": "16:00"}]},
|
||||||
|
[],
|
||||||
|
True,
|
||||||
|
),
|
||||||
|
# Point in time outside schedule and override
|
||||||
|
(
|
||||||
|
"2023-08-24 08:00",
|
||||||
|
{3: [{"start": "10:00", "end": "16:00"}]},
|
||||||
|
[{"start": "2023-08-24 12:00", "end": "2023-08-24 18:00", "action": True}],
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_get_action_at(point_in_time, schedules, overrides, expected_result):
|
||||||
|
point_in_time = datetime.strptime(point_in_time, DATETIME_FORMAT)
|
||||||
|
schedules = {
|
||||||
|
day: [MinuteSpan.from_args(**span) for span in schedule]
|
||||||
|
for day, schedule in schedules.items()
|
||||||
|
}
|
||||||
|
overrides = [DateTimeSpan.from_args(**o) for o in overrides]
|
||||||
|
|
||||||
|
result = app.get_action_at(point_in_time, schedules, overrides)
|
||||||
|
|
||||||
|
assert result == expected_result
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"point_in_time, action, schedules, overrides, expected_result",
|
||||||
|
[
|
||||||
|
# No schedule, no overrides, next change will never occur
|
||||||
|
(
|
||||||
|
"2023-08-24 08:00",
|
||||||
|
False,
|
||||||
|
{day: [] for day in range(7)},
|
||||||
|
[],
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
# Schedule 24/7, no overrides, next change will never occur
|
||||||
|
(
|
||||||
|
"2023-08-24 08:00",
|
||||||
|
True,
|
||||||
|
{day: [{"start": "00:00", "end": "24:00"}] for day in range(7)},
|
||||||
|
[],
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
# No schedule, next change occurs after allow override starts
|
||||||
|
(
|
||||||
|
"2023-08-24 08:00",
|
||||||
|
False,
|
||||||
|
{day: [] for day in range(7)},
|
||||||
|
[{"start": "2023-08-26 10:00", "end": "2023-08-26 14:00", "action": True}],
|
||||||
|
"2023-08-26 10:00",
|
||||||
|
),
|
||||||
|
# Schedule 24/7, next change occurs after deny override starts
|
||||||
|
(
|
||||||
|
"2023-08-24 08:00",
|
||||||
|
True,
|
||||||
|
{day: [{"start": "00:00", "end": "24:00"}] for day in range(7)},
|
||||||
|
[{"start": "2023-08-26 10:00", "end": "2023-08-26 14:00", "action": False}],
|
||||||
|
"2023-08-26 10:00",
|
||||||
|
),
|
||||||
|
# No schedule, expired overrides, next change will never occur
|
||||||
|
(
|
||||||
|
"2023-08-24 08:00",
|
||||||
|
False,
|
||||||
|
{day: [] for day in range(7)},
|
||||||
|
[{"start": "2023-08-23 10:00", "end": "2023-08-23 14:00", "action": True}],
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
# No schedule, next change occurs after allow override expires
|
||||||
|
(
|
||||||
|
"2023-08-24 08:00",
|
||||||
|
True,
|
||||||
|
{day: [] for day in range(7)},
|
||||||
|
[{"start": "2023-08-24 08:00", "end": "2023-08-26 14:00", "action": True}],
|
||||||
|
"2023-08-26 14:00",
|
||||||
|
),
|
||||||
|
# Next change occurs after allow override expires
|
||||||
|
(
|
||||||
|
"2023-08-24 08:00",
|
||||||
|
True,
|
||||||
|
{day: [{"start": "14:00", "end": "16:00"}] for day in range(7)},
|
||||||
|
[{"start": "2023-08-24 08:00", "end": "2023-08-24 10:00", "action": True}],
|
||||||
|
"2023-08-24 10:00",
|
||||||
|
),
|
||||||
|
# Next change occurs after schedule span ends
|
||||||
|
(
|
||||||
|
"2023-08-24 14:00",
|
||||||
|
True,
|
||||||
|
{
|
||||||
|
day: [{"start": "08:00", "end": "10:00"}, {"start": "14:00", "end": "16:00"}]
|
||||||
|
for day in range(7)
|
||||||
|
},
|
||||||
|
[{"start": "2023-08-24 08:00", "end": "2023-08-24 10:00", "action": True}],
|
||||||
|
"2023-08-24 16:00",
|
||||||
|
),
|
||||||
|
# Next change occurs after schedule span starts
|
||||||
|
(
|
||||||
|
"2023-08-24 13:00",
|
||||||
|
False,
|
||||||
|
{
|
||||||
|
day: [{"start": "08:00", "end": "10:00"}, {"start": "14:00", "end": "16:00"}]
|
||||||
|
for day in range(7)
|
||||||
|
},
|
||||||
|
[{"start": "2023-08-24 08:00", "end": "2023-08-24 10:00", "action": True}],
|
||||||
|
"2023-08-24 14:00",
|
||||||
|
),
|
||||||
|
# Next change occurs after deny override starts
|
||||||
|
(
|
||||||
|
"2023-08-24 11:00",
|
||||||
|
True,
|
||||||
|
{day: [{"start": "10:00", "end": "16:00"}] for day in range(7)},
|
||||||
|
[{"start": "2023-08-24 12:00", "end": "2023-08-24 14:00", "action": False}],
|
||||||
|
"2023-08-24 12:00",
|
||||||
|
),
|
||||||
|
# Next change occurs after allow override starts
|
||||||
|
(
|
||||||
|
"2023-08-24 10:00",
|
||||||
|
False,
|
||||||
|
{day: [{"start": "16:00", "end": "18:00"}] for day in range(7)},
|
||||||
|
[{"start": "2023-08-24 12:00", "end": "2023-08-24 14:00", "action": True}],
|
||||||
|
"2023-08-24 12:00",
|
||||||
|
),
|
||||||
|
# Next change occurs after schedule span ends after 7 days
|
||||||
|
(
|
||||||
|
"2023-08-21 13:00",
|
||||||
|
True,
|
||||||
|
{
|
||||||
|
0: [{"start": "00:00", "end": "04:00"}, {"start": "10:00", "end": "24:00"}],
|
||||||
|
1: [{"start": "00:00", "end": "24:00"}],
|
||||||
|
2: [{"start": "00:00", "end": "24:00"}],
|
||||||
|
3: [{"start": "00:00", "end": "24:00"}],
|
||||||
|
4: [{"start": "00:00", "end": "24:00"}],
|
||||||
|
5: [{"start": "00:00", "end": "24:00"}],
|
||||||
|
6: [{"start": "00:00", "end": "24:00"}],
|
||||||
|
},
|
||||||
|
[{"start": "2023-08-24 12:00", "end": "2023-08-24 14:00", "action": True}],
|
||||||
|
"2023-08-28 04:00",
|
||||||
|
),
|
||||||
|
# Next change occurs after schedule span starts after 7 days
|
||||||
|
(
|
||||||
|
"2023-08-21 13:00",
|
||||||
|
False,
|
||||||
|
{
|
||||||
|
0: [{"start": "00:00", "end": "04:00"}],
|
||||||
|
1: [],
|
||||||
|
2: [],
|
||||||
|
3: [],
|
||||||
|
4: [],
|
||||||
|
5: [],
|
||||||
|
6: [],
|
||||||
|
},
|
||||||
|
[{"start": "2023-08-31 12:00", "end": "2023-08-31 14:00", "action": True}],
|
||||||
|
"2023-08-28 00:00",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_get_next_action_change(point_in_time, action, schedules, overrides, expected_result):
|
||||||
|
point_in_time = datetime.strptime(point_in_time, DATETIME_FORMAT)
|
||||||
|
schedules = {
|
||||||
|
day: [MinuteSpan.from_args(**span) for span in schedule]
|
||||||
|
for day, schedule in schedules.items()
|
||||||
|
}
|
||||||
|
overrides = [DateTimeSpan.from_args(**o) for o in overrides]
|
||||||
|
|
||||||
|
result = app.get_next_action_change(point_in_time, action, schedules, overrides)
|
||||||
|
|
||||||
|
if expected_result:
|
||||||
|
expected_result = datetime.strptime(expected_result, DATETIME_FORMAT)
|
||||||
|
assert result == expected_result
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"schedules, overrides, expected_result",
|
||||||
|
[
|
||||||
|
(
|
||||||
|
{day: [] for day in range(7)},
|
||||||
|
[],
|
||||||
|
{"action": False, "until": "", "then_until": ""},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
{day: [{"start": "14:00", "end": "16:00"}] for day in range(7)},
|
||||||
|
[{"start": "2023-08-24 08:00", "end": "2023-08-24 10:00", "action": True}],
|
||||||
|
{"action": False, "until": "2023-08-24 14:00", "then_until": "2023-08-24 16:00"},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
{day: [] for day in range(7)},
|
||||||
|
[{"start": "2023-08-24 08:00", "end": "2023-08-24 12:00", "action": True}],
|
||||||
|
{"action": True, "until": "2023-08-24 12:00", "then_until": ""},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
@patch("parental_control.app.datetime", wraps=datetime)
|
||||||
|
def test_calculate_status(mock_datetime, schedules, overrides, expected_result):
|
||||||
|
mock_datetime.now.return_value = datetime(2023, 8, 24, 11)
|
||||||
|
schedules = {
|
||||||
|
day: [MinuteSpan.from_args(**span) for span in schedule]
|
||||||
|
for day, schedule in schedules.items()
|
||||||
|
}
|
||||||
|
overrides = [DateTimeSpan.from_args(**o) for o in overrides]
|
||||||
|
|
||||||
|
result = app.calculate_status(schedules, overrides)
|
||||||
|
|
||||||
|
assert result == expected_result
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"schedule, span, action, expected_result",
|
||||||
|
[
|
||||||
|
# Allow span in empty schedule
|
||||||
|
(
|
||||||
|
[],
|
||||||
|
{"start": "09:00", "end": "11:00"},
|
||||||
|
True,
|
||||||
|
[{"start": "9:00", "end": "11:00"}],
|
||||||
|
),
|
||||||
|
# Allow span same as already existing span
|
||||||
|
(
|
||||||
|
[{"start": "9:00", "end": "11:00"}],
|
||||||
|
{"start": "09:00", "end": "11:00"},
|
||||||
|
True,
|
||||||
|
[{"start": "9:00", "end": "11:00"}],
|
||||||
|
),
|
||||||
|
# Allow span (starting at 0:00) prepening it before existing span
|
||||||
|
(
|
||||||
|
[{"start": "9:00", "end": "11:00"}],
|
||||||
|
{"start": "00:00", "end": "05:00"},
|
||||||
|
True,
|
||||||
|
[{"start": "0:00", "end": "5:00"}, {"start": "9:00", "end": "11:00"}],
|
||||||
|
),
|
||||||
|
# Allow span (ending at 24:00) appending it after existing span
|
||||||
|
(
|
||||||
|
[{"start": "9:00", "end": "11:00"}],
|
||||||
|
{"start": "19:00", "end": "0:00"},
|
||||||
|
True,
|
||||||
|
[{"start": "9:00", "end": "11:00"}, {"start": "19:00", "end": "24:00"}],
|
||||||
|
),
|
||||||
|
# Allow span merging existing spans together
|
||||||
|
(
|
||||||
|
[{"start": "9:00", "end": "11:00"}, {"start": "14:00", "end": "16:00"}],
|
||||||
|
{"start": "11:00", "end": "14:00"},
|
||||||
|
True,
|
||||||
|
[{"start": "9:00", "end": "16:00"}],
|
||||||
|
),
|
||||||
|
# Deny span from empty schedule
|
||||||
|
(
|
||||||
|
[],
|
||||||
|
{"start": "12:00", "end": "14:00"},
|
||||||
|
False,
|
||||||
|
[],
|
||||||
|
),
|
||||||
|
# Deny span which is already denied
|
||||||
|
(
|
||||||
|
[{"start": "9:00", "end": "11:00"}, {"start": "16:00", "end": "18:00"}],
|
||||||
|
{"start": "12:00", "end": "14:00"},
|
||||||
|
False,
|
||||||
|
[{"start": "9:00", "end": "11:00"}, {"start": "16:00", "end": "18:00"}],
|
||||||
|
),
|
||||||
|
# Deny span removing existing allow spans
|
||||||
|
(
|
||||||
|
[{"start": "9:00", "end": "11:00"}, {"start": "16:00", "end": "18:00"}],
|
||||||
|
{"start": "0:00", "end": "0:00"},
|
||||||
|
False,
|
||||||
|
[],
|
||||||
|
),
|
||||||
|
# Deny span within allow span, splitting the allow span
|
||||||
|
(
|
||||||
|
[{"start": "9:00", "end": "16:00"}],
|
||||||
|
{"start": "11:00", "end": "14:00"},
|
||||||
|
False,
|
||||||
|
[{"start": "9:00", "end": "11:00"}, {"start": "14:00", "end": "16:00"}],
|
||||||
|
),
|
||||||
|
# Deny span moving end and start of existing allow spans
|
||||||
|
(
|
||||||
|
[{"start": "9:00", "end": "11:00"}, {"start": "16:00", "end": "18:00"}],
|
||||||
|
{"start": "10:00", "end": "17:00"},
|
||||||
|
False,
|
||||||
|
[{"start": "9:00", "end": "10:00"}, {"start": "17:00", "end": "18:00"}],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_update_schedule(schedule, span, action, expected_result):
|
||||||
|
schedule = [MinuteSpan.from_args(**s) for s in schedule]
|
||||||
|
span = MinuteSpan.from_args(**span)
|
||||||
|
|
||||||
|
app.update_schedule(schedule, span, action)
|
||||||
|
|
||||||
|
expected_result = [MinuteSpan.from_args(**s) for s in expected_result]
|
||||||
|
assert schedule == expected_result
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"overrides, start, end, action, expected_result",
|
||||||
|
[
|
||||||
|
# Add override into empty list of overrides
|
||||||
|
(
|
||||||
|
[],
|
||||||
|
{"start": "2023-08-24 08:00", "end": "2023-08-24 10:00", "action": True},
|
||||||
|
[{"start": "2023-08-24 08:00", "end": "2023-08-24 10:00", "action": True}],
|
||||||
|
),
|
||||||
|
# Add override into list of overrides
|
||||||
|
(
|
||||||
|
[
|
||||||
|
{"start": "2023-08-24 00:00", "end": "2023-08-24 06:00", "action": True},
|
||||||
|
{"start": "2023-08-24 15:00", "end": "2023-08-24 20:00", "action": True},
|
||||||
|
],
|
||||||
|
{"start": "2023-08-24 08:00", "end": "2023-08-24 10:00", "action": True},
|
||||||
|
[
|
||||||
|
{"start": "2023-08-24 00:00", "end": "2023-08-24 06:00", "action": True},
|
||||||
|
{"start": "2023-08-24 08:00", "end": "2023-08-24 10:00", "action": True},
|
||||||
|
{"start": "2023-08-24 15:00", "end": "2023-08-24 20:00", "action": True},
|
||||||
|
],
|
||||||
|
),
|
||||||
|
# Add override removing another override spanning over smaller period
|
||||||
|
(
|
||||||
|
[
|
||||||
|
{"start": "2023-08-24 00:00", "end": "2023-08-24 06:00", "action": True},
|
||||||
|
{"start": "2023-08-24 15:00", "end": "2023-08-24 20:00", "action": True},
|
||||||
|
],
|
||||||
|
{"start": "2023-08-24 15:00", "end": "2023-08-24 22:00", "action": False},
|
||||||
|
[
|
||||||
|
{"start": "2023-08-24 00:00", "end": "2023-08-24 06:00", "action": True},
|
||||||
|
{"start": "2023-08-24 15:00", "end": "2023-08-24 22:00", "action": False},
|
||||||
|
],
|
||||||
|
),
|
||||||
|
# Add override within existing override span, splitting it
|
||||||
|
(
|
||||||
|
[{"start": "2023-08-24 08:00", "end": "2023-08-24 20:00", "action": False}],
|
||||||
|
{"start": "2023-08-24 16:00", "end": "2023-08-24 18:00", "action": True},
|
||||||
|
[
|
||||||
|
{"start": "2023-08-24 08:00", "end": "2023-08-24 16:00", "action": False},
|
||||||
|
{"start": "2023-08-24 16:00", "end": "2023-08-24 18:00", "action": True},
|
||||||
|
{"start": "2023-08-24 18:00", "end": "2023-08-24 20:00", "action": False},
|
||||||
|
],
|
||||||
|
),
|
||||||
|
# Add override moving end and start of existing overrides
|
||||||
|
(
|
||||||
|
[
|
||||||
|
{"start": "2023-08-24 00:00", "end": "2023-08-24 06:00", "action": True},
|
||||||
|
{"start": "2023-08-24 15:00", "end": "2023-08-24 20:00", "action": True},
|
||||||
|
],
|
||||||
|
{"start": "2023-08-24 04:00", "end": "2023-08-24 18:00", "action": False},
|
||||||
|
[
|
||||||
|
{"start": "2023-08-24 00:00", "end": "2023-08-24 04:00", "action": True},
|
||||||
|
{"start": "2023-08-24 04:00", "end": "2023-08-24 18:00", "action": False},
|
||||||
|
{"start": "2023-08-24 18:00", "end": "2023-08-24 20:00", "action": True},
|
||||||
|
],
|
||||||
|
),
|
||||||
|
# Add override merging existing overrides
|
||||||
|
(
|
||||||
|
[
|
||||||
|
{"start": "2023-08-24 00:00", "end": "2023-08-24 06:00", "action": True},
|
||||||
|
{"start": "2023-08-24 15:00", "end": "2023-08-24 20:00", "action": True},
|
||||||
|
],
|
||||||
|
{"start": "2023-08-24 04:00", "end": "2023-08-24 18:00", "action": True},
|
||||||
|
[{"start": "2023-08-24 00:00", "end": "2023-08-24 20:00", "action": True}],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_update_overrides(overrides, span, expected_result):
|
||||||
|
overrides = [DateTimeSpan.from_args(**o) for o in overrides]
|
||||||
|
span = DateTimeSpan.from_args(**span)
|
||||||
|
|
||||||
|
app.update_overrides(overrides, span)
|
||||||
|
|
||||||
|
expected_result = [DateTimeSpan.from_args(**o) for o in expected_result]
|
||||||
|
assert overrides == expected_result
|
17
tox.ini
Normal file
17
tox.ini
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
[coverage:run]
|
||||||
|
branch = True
|
||||||
|
|
||||||
|
[tox]
|
||||||
|
envlist = pylint, pytest, black
|
||||||
|
|
||||||
|
[testenv:pylint]
|
||||||
|
deps = pylint
|
||||||
|
commands = pylint parental_control tests {posargs}
|
||||||
|
|
||||||
|
[testenv:pytest]
|
||||||
|
deps = pytest-cov
|
||||||
|
commands = pytest tests -vv --cov parental_control --cov-report term --cov-report xml --cov-fail-under 100 {posargs}
|
||||||
|
|
||||||
|
[testenv:black]
|
||||||
|
deps = black
|
||||||
|
commands = black -l 100 --check --diff parental_control tests {posargs}
|
Loading…
Reference in New Issue
Block a user