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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user