2150 lines
44 KiB
Python
2150 lines
44 KiB
Python
import sys
|
|
import pwd
|
|
import sqlite3
|
|
|
|
from PySide6.QtCore import Qt, QTime
|
|
from PySide6.QtWidgets import (
|
|
QApplication,
|
|
QCheckBox,
|
|
QDialog,
|
|
QDialogButtonBox,
|
|
QFormLayout,
|
|
QFrame,
|
|
QGridLayout,
|
|
QHBoxLayout,
|
|
QLabel,
|
|
QLineEdit,
|
|
QMainWindow,
|
|
QMessageBox,
|
|
QPushButton,
|
|
QProgressBar,
|
|
QScrollArea,
|
|
QSpinBox,
|
|
QStackedWidget,
|
|
QTimeEdit,
|
|
QVBoxLayout,
|
|
QWidget,
|
|
)
|
|
|
|
from app.database import DATABASE_PATH
|
|
|
|
|
|
# ============================================================
|
|
# Database
|
|
# ============================================================
|
|
|
|
class Database:
|
|
|
|
@staticmethod
|
|
def get_connection():
|
|
connection = sqlite3.connect(DATABASE_PATH)
|
|
connection.row_factory = sqlite3.Row
|
|
connection.execute("PRAGMA foreign_keys = ON")
|
|
return connection
|
|
|
|
@staticmethod
|
|
def get_users():
|
|
if not DATABASE_PATH.exists():
|
|
return []
|
|
|
|
connection = Database.get_connection()
|
|
|
|
try:
|
|
return connection.execute(
|
|
"""
|
|
SELECT *
|
|
FROM users
|
|
ORDER BY username COLLATE NOCASE
|
|
"""
|
|
).fetchall()
|
|
finally:
|
|
connection.close()
|
|
|
|
@staticmethod
|
|
def get_user(user_id):
|
|
connection = Database.get_connection()
|
|
|
|
try:
|
|
return connection.execute(
|
|
"""
|
|
SELECT *
|
|
FROM users
|
|
WHERE id = ?
|
|
""",
|
|
(user_id,),
|
|
).fetchone()
|
|
finally:
|
|
connection.close()
|
|
|
|
@staticmethod
|
|
def get_remaining_time(user_id):
|
|
if not DATABASE_PATH.exists():
|
|
return 0, 0
|
|
|
|
connection = Database.get_connection()
|
|
|
|
try:
|
|
allowance_row = connection.execute(
|
|
"""
|
|
SELECT COALESCE(
|
|
allowance_seconds,
|
|
0
|
|
) AS total
|
|
FROM daily_allowances
|
|
WHERE user_id = ?
|
|
AND weekday = CAST(
|
|
strftime('%w', 'now', 'localtime')
|
|
AS INTEGER
|
|
)
|
|
""",
|
|
(user_id,),
|
|
).fetchone()
|
|
|
|
allowance = (
|
|
allowance_row["total"]
|
|
if allowance_row
|
|
else 0
|
|
)
|
|
|
|
usage_row = connection.execute(
|
|
"""
|
|
SELECT COALESCE(
|
|
used_seconds,
|
|
0
|
|
) AS total
|
|
FROM usage
|
|
WHERE user_id = ?
|
|
AND date = date('now', 'localtime')
|
|
""",
|
|
(user_id,),
|
|
).fetchone()
|
|
|
|
used = (
|
|
usage_row["total"]
|
|
if usage_row
|
|
else 0
|
|
)
|
|
|
|
remaining = max(
|
|
allowance - used,
|
|
0,
|
|
)
|
|
|
|
return remaining, allowance
|
|
|
|
finally:
|
|
connection.close()
|
|
|
|
@staticmethod
|
|
def get_allowances(user_id):
|
|
connection = Database.get_connection()
|
|
|
|
try:
|
|
rows = connection.execute(
|
|
"""
|
|
SELECT weekday, allowance_seconds
|
|
FROM daily_allowances
|
|
WHERE user_id = ?
|
|
""",
|
|
(user_id,),
|
|
).fetchall()
|
|
|
|
return {
|
|
row["weekday"]: row["allowance_seconds"]
|
|
for row in rows
|
|
}
|
|
|
|
finally:
|
|
connection.close()
|
|
|
|
@staticmethod
|
|
def get_access_windows(user_id):
|
|
connection = Database.get_connection()
|
|
|
|
try:
|
|
rows = connection.execute(
|
|
"""
|
|
SELECT
|
|
weekday,
|
|
start_minute,
|
|
end_minute
|
|
FROM access_windows
|
|
WHERE user_id = ?
|
|
ORDER BY weekday, start_minute
|
|
""",
|
|
(user_id,),
|
|
).fetchall()
|
|
|
|
result = {}
|
|
|
|
for row in rows:
|
|
result.setdefault(
|
|
row["weekday"],
|
|
[],
|
|
).append(
|
|
(
|
|
row["start_minute"],
|
|
row["end_minute"],
|
|
)
|
|
)
|
|
|
|
return result
|
|
|
|
finally:
|
|
connection.close()
|
|
|
|
@staticmethod
|
|
def create_user(
|
|
username,
|
|
enabled,
|
|
allowances,
|
|
access_windows,
|
|
):
|
|
connection = Database.get_connection()
|
|
|
|
try:
|
|
cursor = connection.execute(
|
|
"""
|
|
INSERT INTO users (
|
|
username,
|
|
enabled
|
|
)
|
|
VALUES (?, ?)
|
|
""",
|
|
(
|
|
username,
|
|
1 if enabled else 0,
|
|
),
|
|
)
|
|
|
|
user_id = cursor.lastrowid
|
|
|
|
for weekday, seconds in allowances.items():
|
|
connection.execute(
|
|
"""
|
|
INSERT INTO daily_allowances (
|
|
user_id,
|
|
weekday,
|
|
allowance_seconds
|
|
)
|
|
VALUES (?, ?, ?)
|
|
""",
|
|
(
|
|
user_id,
|
|
weekday,
|
|
seconds,
|
|
),
|
|
)
|
|
|
|
for weekday, windows in access_windows.items():
|
|
for start_minute, end_minute in windows:
|
|
connection.execute(
|
|
"""
|
|
INSERT INTO access_windows (
|
|
user_id,
|
|
weekday,
|
|
start_minute,
|
|
end_minute
|
|
)
|
|
VALUES (?, ?, ?, ?)
|
|
""",
|
|
(
|
|
user_id,
|
|
weekday,
|
|
start_minute,
|
|
end_minute,
|
|
),
|
|
)
|
|
|
|
connection.commit()
|
|
|
|
return user_id
|
|
|
|
finally:
|
|
connection.close()
|
|
|
|
@staticmethod
|
|
def update_user(
|
|
user_id,
|
|
enabled,
|
|
allowances,
|
|
access_windows,
|
|
):
|
|
connection = Database.get_connection()
|
|
|
|
try:
|
|
connection.execute(
|
|
"""
|
|
UPDATE users
|
|
SET enabled = ?
|
|
WHERE id = ?
|
|
""",
|
|
(
|
|
1 if enabled else 0,
|
|
user_id,
|
|
),
|
|
)
|
|
|
|
for weekday, seconds in allowances.items():
|
|
connection.execute(
|
|
"""
|
|
INSERT INTO daily_allowances (
|
|
user_id,
|
|
weekday,
|
|
allowance_seconds
|
|
)
|
|
VALUES (?, ?, ?)
|
|
ON CONFLICT(user_id, weekday)
|
|
DO UPDATE SET
|
|
allowance_seconds =
|
|
excluded.allowance_seconds
|
|
""",
|
|
(
|
|
user_id,
|
|
weekday,
|
|
seconds,
|
|
),
|
|
)
|
|
|
|
# Replace access windows completely.
|
|
connection.execute(
|
|
"""
|
|
DELETE FROM access_windows
|
|
WHERE user_id = ?
|
|
""",
|
|
(user_id,),
|
|
)
|
|
|
|
for weekday, windows in access_windows.items():
|
|
for start_minute, end_minute in windows:
|
|
connection.execute(
|
|
"""
|
|
INSERT INTO access_windows (
|
|
user_id,
|
|
weekday,
|
|
start_minute,
|
|
end_minute
|
|
)
|
|
VALUES (?, ?, ?, ?)
|
|
""",
|
|
(
|
|
user_id,
|
|
weekday,
|
|
start_minute,
|
|
end_minute,
|
|
),
|
|
)
|
|
|
|
connection.commit()
|
|
|
|
finally:
|
|
connection.close()
|
|
|
|
@staticmethod
|
|
def delete_user(user_id):
|
|
connection = Database.get_connection()
|
|
|
|
try:
|
|
connection.execute(
|
|
"""
|
|
DELETE FROM users
|
|
WHERE id = ?
|
|
""",
|
|
(user_id,),
|
|
)
|
|
|
|
connection.commit()
|
|
|
|
finally:
|
|
connection.close()
|
|
|
|
@staticmethod
|
|
def set_enabled(user_id, enabled):
|
|
connection = Database.get_connection()
|
|
|
|
try:
|
|
connection.execute(
|
|
"""
|
|
UPDATE users
|
|
SET enabled = ?
|
|
WHERE id = ?
|
|
""",
|
|
(
|
|
1 if enabled else 0,
|
|
user_id,
|
|
),
|
|
)
|
|
|
|
connection.commit()
|
|
|
|
finally:
|
|
connection.close()
|
|
|
|
@staticmethod
|
|
def add_temporary_time(
|
|
user_id,
|
|
seconds,
|
|
):
|
|
connection = Database.get_connection()
|
|
|
|
try:
|
|
connection.execute(
|
|
"""
|
|
INSERT INTO temporary_grants (
|
|
user_id,
|
|
seconds,
|
|
remaining_seconds
|
|
)
|
|
VALUES (?, ?, ?)
|
|
""",
|
|
(
|
|
user_id,
|
|
seconds,
|
|
seconds,
|
|
),
|
|
)
|
|
|
|
connection.commit()
|
|
|
|
finally:
|
|
connection.close()
|
|
|
|
|
|
# ============================================================
|
|
# Linux users
|
|
# ============================================================
|
|
|
|
def linux_user_exists(username):
|
|
try:
|
|
pwd.getpwnam(username)
|
|
return True
|
|
except KeyError:
|
|
return False
|
|
|
|
|
|
# ============================================================
|
|
# Helpers
|
|
# ============================================================
|
|
|
|
# SQLite strftime('%w') numbering:
|
|
#
|
|
# Sunday = 0
|
|
# Monday = 1
|
|
# Tuesday = 2
|
|
# Wednesday = 3
|
|
# Thursday = 4
|
|
# Friday = 5
|
|
# Saturday = 6
|
|
|
|
WEEKDAYS = [
|
|
(0, "Sunday"),
|
|
(1, "Monday"),
|
|
(2, "Tuesday"),
|
|
(3, "Wednesday"),
|
|
(4, "Thursday"),
|
|
(5, "Friday"),
|
|
(6, "Saturday"),
|
|
]
|
|
|
|
|
|
def format_seconds(seconds):
|
|
seconds = max(
|
|
int(seconds),
|
|
0,
|
|
)
|
|
|
|
hours = seconds // 3600
|
|
minutes = (seconds % 3600) // 60
|
|
|
|
if hours:
|
|
return f"{hours}h {minutes:02d}m"
|
|
|
|
return f"{minutes}m"
|
|
|
|
|
|
# ============================================================
|
|
# User Card
|
|
# ============================================================
|
|
|
|
class UserCard(QFrame):
|
|
|
|
def __init__(
|
|
self,
|
|
user,
|
|
edit_callback,
|
|
toggle_callback,
|
|
parent=None,
|
|
):
|
|
super().__init__(parent)
|
|
|
|
self.user_id = user["id"]
|
|
|
|
self.setObjectName("UserCard")
|
|
|
|
layout = QVBoxLayout(self)
|
|
|
|
layout.setContentsMargins(
|
|
20,
|
|
18,
|
|
20,
|
|
18,
|
|
)
|
|
|
|
layout.setSpacing(12)
|
|
|
|
# ----------------------------------------------------
|
|
# Header
|
|
# ----------------------------------------------------
|
|
|
|
header = QHBoxLayout()
|
|
|
|
username = QLabel(
|
|
user["username"]
|
|
)
|
|
|
|
username.setObjectName(
|
|
"UserName"
|
|
)
|
|
|
|
header.addWidget(username)
|
|
header.addStretch()
|
|
|
|
status = QLabel(
|
|
"Enabled"
|
|
if user["enabled"]
|
|
else "Disabled"
|
|
)
|
|
|
|
status.setObjectName(
|
|
"StatusEnabled"
|
|
if user["enabled"]
|
|
else "StatusDisabled"
|
|
)
|
|
|
|
header.addWidget(status)
|
|
|
|
layout.addLayout(header)
|
|
|
|
# ----------------------------------------------------
|
|
# Remaining time
|
|
# ----------------------------------------------------
|
|
|
|
remaining, allowance = (
|
|
Database.get_remaining_time(
|
|
self.user_id
|
|
)
|
|
)
|
|
|
|
today_label = QLabel(
|
|
"Today's allowance"
|
|
)
|
|
|
|
today_label.setObjectName(
|
|
"SecondaryText"
|
|
)
|
|
|
|
layout.addWidget(today_label)
|
|
|
|
time_label = QLabel(
|
|
f"{format_seconds(remaining)} remaining"
|
|
)
|
|
|
|
time_label.setObjectName(
|
|
"TimeRemaining"
|
|
)
|
|
|
|
layout.addWidget(time_label)
|
|
|
|
progress = QProgressBar()
|
|
|
|
progress.setTextVisible(False)
|
|
|
|
progress.setMinimum(0)
|
|
|
|
progress.setMaximum(
|
|
max(allowance, 1)
|
|
)
|
|
|
|
progress.setValue(
|
|
min(
|
|
remaining,
|
|
allowance,
|
|
)
|
|
)
|
|
|
|
layout.addWidget(progress)
|
|
|
|
# ----------------------------------------------------
|
|
# Buttons
|
|
# ----------------------------------------------------
|
|
|
|
buttons = QHBoxLayout()
|
|
|
|
manage_button = QPushButton(
|
|
"Manage"
|
|
)
|
|
|
|
manage_button.clicked.connect(
|
|
lambda: edit_callback(
|
|
self.user_id
|
|
)
|
|
)
|
|
|
|
buttons.addWidget(
|
|
manage_button
|
|
)
|
|
|
|
toggle_text = (
|
|
"Disable"
|
|
if user["enabled"]
|
|
else "Enable"
|
|
)
|
|
|
|
toggle_button = QPushButton(
|
|
toggle_text
|
|
)
|
|
|
|
toggle_button.clicked.connect(
|
|
lambda: toggle_callback(
|
|
self.user_id,
|
|
not bool(
|
|
user["enabled"]
|
|
),
|
|
)
|
|
)
|
|
|
|
buttons.addWidget(
|
|
toggle_button
|
|
)
|
|
|
|
layout.addLayout(buttons)
|
|
|
|
|
|
# ============================================================
|
|
# Dashboard
|
|
# ============================================================
|
|
|
|
class DashboardPage(QWidget):
|
|
|
|
def __init__(
|
|
self,
|
|
edit_callback,
|
|
toggle_callback,
|
|
parent=None,
|
|
):
|
|
super().__init__(parent)
|
|
|
|
self.edit_callback = edit_callback
|
|
self.toggle_callback = toggle_callback
|
|
|
|
self.layout = QVBoxLayout(self)
|
|
|
|
self.layout.setContentsMargins(
|
|
30,
|
|
30,
|
|
30,
|
|
30,
|
|
)
|
|
|
|
self.layout.setSpacing(20)
|
|
|
|
title = QLabel(
|
|
"Dashboard"
|
|
)
|
|
|
|
title.setObjectName(
|
|
"PageTitle"
|
|
)
|
|
|
|
self.layout.addWidget(title)
|
|
|
|
self.subtitle = QLabel()
|
|
|
|
self.subtitle.setObjectName(
|
|
"SecondaryText"
|
|
)
|
|
|
|
self.layout.addWidget(
|
|
self.subtitle
|
|
)
|
|
|
|
self.scroll = QScrollArea()
|
|
|
|
self.scroll.setWidgetResizable(
|
|
True
|
|
)
|
|
|
|
self.scroll.setFrameShape(
|
|
QFrame.NoFrame
|
|
)
|
|
|
|
self.container = QWidget()
|
|
|
|
self.cards_layout = QVBoxLayout(
|
|
self.container
|
|
)
|
|
|
|
self.cards_layout.setContentsMargins(
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
)
|
|
|
|
self.cards_layout.setSpacing(
|
|
15
|
|
)
|
|
|
|
self.scroll.setWidget(
|
|
self.container
|
|
)
|
|
|
|
self.layout.addWidget(
|
|
self.scroll
|
|
)
|
|
|
|
self.refresh()
|
|
|
|
def refresh(self):
|
|
|
|
while self.cards_layout.count():
|
|
|
|
item = self.cards_layout.takeAt(0)
|
|
|
|
widget = item.widget()
|
|
|
|
if widget:
|
|
widget.deleteLater()
|
|
|
|
users = Database.get_users()
|
|
|
|
self.subtitle.setText(
|
|
f"{len(users)} managed user"
|
|
+ (
|
|
""
|
|
if len(users) == 1
|
|
else "s"
|
|
)
|
|
)
|
|
|
|
if not users:
|
|
|
|
empty = QLabel(
|
|
"No users are currently managed."
|
|
)
|
|
|
|
empty.setObjectName(
|
|
"EmptyText"
|
|
)
|
|
|
|
empty.setAlignment(
|
|
Qt.AlignCenter
|
|
)
|
|
|
|
self.cards_layout.addWidget(
|
|
empty
|
|
)
|
|
|
|
else:
|
|
|
|
for user in users:
|
|
|
|
card = UserCard(
|
|
user,
|
|
edit_callback=self.edit_callback,
|
|
toggle_callback=self.toggle_callback,
|
|
)
|
|
|
|
self.cards_layout.addWidget(
|
|
card
|
|
)
|
|
|
|
self.cards_layout.addStretch()
|
|
|
|
|
|
# ============================================================
|
|
# Manage User Dialog
|
|
# ============================================================
|
|
|
|
class UserDialog(QDialog):
|
|
|
|
def __init__(
|
|
self,
|
|
user=None,
|
|
parent=None,
|
|
):
|
|
super().__init__(parent)
|
|
|
|
self.user = user
|
|
|
|
if user:
|
|
|
|
self.setWindowTitle(
|
|
f"Manage User: "
|
|
f"{user['username']}"
|
|
)
|
|
|
|
else:
|
|
|
|
self.setWindowTitle(
|
|
"Add User"
|
|
)
|
|
|
|
self.setMinimumWidth(720)
|
|
|
|
main_layout = QVBoxLayout(self)
|
|
|
|
main_layout.setContentsMargins(
|
|
25,
|
|
25,
|
|
25,
|
|
25,
|
|
)
|
|
|
|
main_layout.setSpacing(18)
|
|
|
|
# ----------------------------------------------------
|
|
# User information
|
|
# ----------------------------------------------------
|
|
|
|
form = QFormLayout()
|
|
|
|
self.username_input = QLineEdit()
|
|
|
|
if user:
|
|
|
|
self.username_input.setText(
|
|
user["username"]
|
|
)
|
|
|
|
self.username_input.setReadOnly(
|
|
True
|
|
)
|
|
|
|
form.addRow(
|
|
"Linux username:",
|
|
self.username_input,
|
|
)
|
|
|
|
self.enabled_checkbox = QCheckBox(
|
|
"Enable user"
|
|
)
|
|
|
|
self.enabled_checkbox.setChecked(
|
|
bool(
|
|
user["enabled"]
|
|
)
|
|
if user
|
|
else True
|
|
)
|
|
|
|
form.addRow(
|
|
"",
|
|
self.enabled_checkbox,
|
|
)
|
|
|
|
main_layout.addLayout(
|
|
form
|
|
)
|
|
|
|
# ----------------------------------------------------
|
|
# Daily allowance
|
|
# ----------------------------------------------------
|
|
|
|
allowance_title = QLabel(
|
|
"Daily Allowance"
|
|
)
|
|
|
|
allowance_title.setObjectName(
|
|
"SectionTitle"
|
|
)
|
|
|
|
main_layout.addWidget(
|
|
allowance_title
|
|
)
|
|
|
|
allowance_description = QLabel(
|
|
"Maximum amount of computer time "
|
|
"the user can consume each day."
|
|
)
|
|
|
|
allowance_description.setObjectName(
|
|
"SecondaryText"
|
|
)
|
|
|
|
allowance_description.setWordWrap(
|
|
True
|
|
)
|
|
|
|
main_layout.addWidget(
|
|
allowance_description
|
|
)
|
|
|
|
existing_allowances = (
|
|
Database.get_allowances(
|
|
user["id"]
|
|
)
|
|
if user
|
|
else {}
|
|
)
|
|
|
|
self.day_controls = {}
|
|
|
|
allowance_grid = QGridLayout()
|
|
|
|
allowance_grid.setHorizontalSpacing(
|
|
15
|
|
)
|
|
|
|
allowance_grid.setVerticalSpacing(
|
|
8
|
|
)
|
|
|
|
allowance_grid.addWidget(
|
|
QLabel("Day"),
|
|
0,
|
|
0,
|
|
)
|
|
|
|
allowance_grid.addWidget(
|
|
QLabel("Hours"),
|
|
0,
|
|
1,
|
|
)
|
|
|
|
allowance_grid.addWidget(
|
|
QLabel("Minutes"),
|
|
0,
|
|
2,
|
|
)
|
|
|
|
for row, (
|
|
weekday,
|
|
name,
|
|
) in enumerate(
|
|
WEEKDAYS,
|
|
start=1,
|
|
):
|
|
|
|
allowance_grid.addWidget(
|
|
QLabel(name),
|
|
row,
|
|
0,
|
|
)
|
|
|
|
hours = QSpinBox()
|
|
|
|
hours.setRange(
|
|
0,
|
|
24,
|
|
)
|
|
|
|
minutes = QSpinBox()
|
|
|
|
minutes.setRange(
|
|
0,
|
|
59,
|
|
)
|
|
|
|
seconds = existing_allowances.get(
|
|
weekday,
|
|
0,
|
|
)
|
|
|
|
hours.setValue(
|
|
seconds // 3600
|
|
)
|
|
|
|
minutes.setValue(
|
|
(seconds % 3600) // 60
|
|
)
|
|
|
|
allowance_grid.addWidget(
|
|
hours,
|
|
row,
|
|
1,
|
|
)
|
|
|
|
allowance_grid.addWidget(
|
|
minutes,
|
|
row,
|
|
2,
|
|
)
|
|
|
|
self.day_controls[
|
|
weekday
|
|
] = (
|
|
hours,
|
|
minutes,
|
|
)
|
|
|
|
main_layout.addLayout(
|
|
allowance_grid
|
|
)
|
|
|
|
# ----------------------------------------------------
|
|
# Access windows
|
|
# ----------------------------------------------------
|
|
|
|
window_title = QLabel(
|
|
"Access Windows"
|
|
)
|
|
|
|
window_title.setObjectName(
|
|
"SectionTitle"
|
|
)
|
|
|
|
main_layout.addWidget(
|
|
window_title
|
|
)
|
|
|
|
window_description = QLabel(
|
|
"Define when the user is allowed to "
|
|
"use the computer. Disable a day "
|
|
"to prevent access on that day."
|
|
)
|
|
|
|
window_description.setObjectName(
|
|
"SecondaryText"
|
|
)
|
|
|
|
window_description.setWordWrap(
|
|
True
|
|
)
|
|
|
|
main_layout.addWidget(
|
|
window_description
|
|
)
|
|
|
|
existing_windows = (
|
|
Database.get_access_windows(
|
|
user["id"]
|
|
)
|
|
if user
|
|
else {}
|
|
)
|
|
|
|
self.window_controls = {}
|
|
|
|
windows_grid = QGridLayout()
|
|
|
|
windows_grid.setHorizontalSpacing(
|
|
15
|
|
)
|
|
|
|
windows_grid.setVerticalSpacing(
|
|
8
|
|
)
|
|
|
|
windows_grid.addWidget(
|
|
QLabel("Day"),
|
|
0,
|
|
0,
|
|
)
|
|
|
|
windows_grid.addWidget(
|
|
QLabel("Allowed"),
|
|
0,
|
|
1,
|
|
)
|
|
|
|
windows_grid.addWidget(
|
|
QLabel("Start"),
|
|
0,
|
|
2,
|
|
)
|
|
|
|
windows_grid.addWidget(
|
|
QLabel("End"),
|
|
0,
|
|
3,
|
|
)
|
|
|
|
for row, (
|
|
weekday,
|
|
name,
|
|
) in enumerate(
|
|
WEEKDAYS,
|
|
start=1,
|
|
):
|
|
|
|
windows_grid.addWidget(
|
|
QLabel(name),
|
|
row,
|
|
0,
|
|
)
|
|
|
|
enabled = QCheckBox()
|
|
|
|
start_time = QTimeEdit()
|
|
|
|
start_time.setDisplayFormat(
|
|
"HH:mm"
|
|
)
|
|
|
|
start_time.setTime(
|
|
QTime(16, 0)
|
|
)
|
|
|
|
end_time = QTimeEdit()
|
|
|
|
end_time.setDisplayFormat(
|
|
"HH:mm"
|
|
)
|
|
|
|
end_time.setTime(
|
|
QTime(21, 0)
|
|
)
|
|
|
|
existing = existing_windows.get(
|
|
weekday,
|
|
[]
|
|
)
|
|
|
|
if existing:
|
|
|
|
start_minute, end_minute = (
|
|
existing[0]
|
|
)
|
|
|
|
enabled.setChecked(
|
|
True
|
|
)
|
|
|
|
start_time.setTime(
|
|
QTime(
|
|
start_minute // 60,
|
|
start_minute % 60,
|
|
)
|
|
)
|
|
|
|
end_time.setTime(
|
|
QTime(
|
|
end_minute // 60,
|
|
end_minute % 60,
|
|
)
|
|
)
|
|
|
|
else:
|
|
|
|
enabled.setChecked(
|
|
False
|
|
)
|
|
|
|
start_time.setEnabled(
|
|
enabled.isChecked()
|
|
)
|
|
|
|
end_time.setEnabled(
|
|
enabled.isChecked()
|
|
)
|
|
|
|
enabled.toggled.connect(
|
|
start_time.setEnabled
|
|
)
|
|
|
|
enabled.toggled.connect(
|
|
end_time.setEnabled
|
|
)
|
|
|
|
windows_grid.addWidget(
|
|
enabled,
|
|
row,
|
|
1,
|
|
)
|
|
|
|
windows_grid.addWidget(
|
|
start_time,
|
|
row,
|
|
2,
|
|
)
|
|
|
|
windows_grid.addWidget(
|
|
end_time,
|
|
row,
|
|
3,
|
|
)
|
|
|
|
self.window_controls[
|
|
weekday
|
|
] = (
|
|
enabled,
|
|
start_time,
|
|
end_time,
|
|
)
|
|
|
|
main_layout.addLayout(
|
|
windows_grid
|
|
)
|
|
|
|
# ----------------------------------------------------
|
|
# Extra time
|
|
# ----------------------------------------------------
|
|
|
|
if user:
|
|
|
|
extra_title = QLabel(
|
|
"Add Extra Time"
|
|
)
|
|
|
|
extra_title.setObjectName(
|
|
"SectionTitle"
|
|
)
|
|
|
|
main_layout.addWidget(
|
|
extra_title
|
|
)
|
|
|
|
extra_description = QLabel(
|
|
"Add temporary time without changing "
|
|
"the normal daily allowance."
|
|
)
|
|
|
|
extra_description.setObjectName(
|
|
"SecondaryText"
|
|
)
|
|
|
|
extra_description.setWordWrap(
|
|
True
|
|
)
|
|
|
|
main_layout.addWidget(
|
|
extra_description
|
|
)
|
|
|
|
extra_row = QHBoxLayout()
|
|
|
|
self.extra_hours = QSpinBox()
|
|
|
|
self.extra_hours.setRange(
|
|
0,
|
|
24,
|
|
)
|
|
|
|
self.extra_hours.setSuffix(
|
|
" h"
|
|
)
|
|
|
|
self.extra_minutes = QSpinBox()
|
|
|
|
self.extra_minutes.setRange(
|
|
0,
|
|
59,
|
|
)
|
|
|
|
self.extra_minutes.setSuffix(
|
|
" min"
|
|
)
|
|
|
|
add_time_button = QPushButton(
|
|
"Add Time"
|
|
)
|
|
|
|
add_time_button.clicked.connect(
|
|
self.add_extra_time
|
|
)
|
|
|
|
extra_row.addWidget(
|
|
self.extra_hours
|
|
)
|
|
|
|
extra_row.addWidget(
|
|
self.extra_minutes
|
|
)
|
|
|
|
extra_row.addWidget(
|
|
add_time_button
|
|
)
|
|
|
|
extra_row.addStretch()
|
|
|
|
main_layout.addLayout(
|
|
extra_row
|
|
)
|
|
|
|
# ----------------------------------------------------
|
|
# Dialog buttons
|
|
# ----------------------------------------------------
|
|
|
|
buttons = QDialogButtonBox(
|
|
QDialogButtonBox.Save
|
|
| QDialogButtonBox.Cancel
|
|
)
|
|
|
|
buttons.accepted.connect(
|
|
self.save_user
|
|
)
|
|
|
|
buttons.rejected.connect(
|
|
self.reject
|
|
)
|
|
|
|
main_layout.addWidget(
|
|
buttons
|
|
)
|
|
|
|
def add_extra_time(self):
|
|
|
|
hours = self.extra_hours.value()
|
|
minutes = self.extra_minutes.value()
|
|
|
|
seconds = (
|
|
hours * 3600
|
|
+ minutes * 60
|
|
)
|
|
|
|
if seconds <= 0:
|
|
|
|
QMessageBox.warning(
|
|
self,
|
|
"Invalid Time",
|
|
"Please enter an amount of time "
|
|
"greater than zero.",
|
|
)
|
|
|
|
return
|
|
|
|
try:
|
|
|
|
Database.add_temporary_time(
|
|
self.user["id"],
|
|
seconds,
|
|
)
|
|
|
|
self.extra_hours.setValue(
|
|
0
|
|
)
|
|
|
|
self.extra_minutes.setValue(
|
|
0
|
|
)
|
|
|
|
QMessageBox.information(
|
|
self,
|
|
"Time Added",
|
|
f"Added {format_seconds(seconds)} "
|
|
f"to {self.user['username']}.",
|
|
)
|
|
|
|
except Exception as exc:
|
|
|
|
QMessageBox.critical(
|
|
self,
|
|
"Error",
|
|
f"Could not add time:\n\n{exc}",
|
|
)
|
|
|
|
def save_user(self):
|
|
|
|
username = (
|
|
self.username_input
|
|
.text()
|
|
.strip()
|
|
)
|
|
|
|
if not username:
|
|
|
|
QMessageBox.warning(
|
|
self,
|
|
"Missing Username",
|
|
"Enter a Linux username.",
|
|
)
|
|
|
|
return
|
|
|
|
if (
|
|
not self.user
|
|
and not linux_user_exists(
|
|
username
|
|
)
|
|
):
|
|
|
|
QMessageBox.warning(
|
|
self,
|
|
"User Not Found",
|
|
f"The Linux user "
|
|
f"'{username}' does not exist.",
|
|
)
|
|
|
|
return
|
|
|
|
# ----------------------------------------------------
|
|
# Allowances
|
|
# ----------------------------------------------------
|
|
|
|
allowances = {}
|
|
|
|
for weekday, controls in (
|
|
self.day_controls.items()
|
|
):
|
|
|
|
hours, minutes = controls
|
|
|
|
seconds = (
|
|
hours.value() * 3600
|
|
+ minutes.value() * 60
|
|
)
|
|
|
|
allowances[
|
|
weekday
|
|
] = seconds
|
|
|
|
# ----------------------------------------------------
|
|
# Access windows
|
|
# ----------------------------------------------------
|
|
|
|
access_windows = {}
|
|
|
|
for weekday, controls in (
|
|
self.window_controls.items()
|
|
):
|
|
|
|
enabled, start_time, end_time = (
|
|
controls
|
|
)
|
|
|
|
if not enabled.isChecked():
|
|
continue
|
|
|
|
start = start_time.time()
|
|
|
|
end = end_time.time()
|
|
|
|
start_minute = (
|
|
start.hour() * 60
|
|
+ start.minute()
|
|
)
|
|
|
|
end_minute = (
|
|
end.hour() * 60
|
|
+ end.minute()
|
|
)
|
|
|
|
if start_minute == end_minute:
|
|
|
|
QMessageBox.warning(
|
|
self,
|
|
"Invalid Access Window",
|
|
"Start and end time cannot "
|
|
"be the same.",
|
|
)
|
|
|
|
return
|
|
|
|
access_windows[
|
|
weekday
|
|
] = [
|
|
(
|
|
start_minute,
|
|
end_minute,
|
|
)
|
|
]
|
|
|
|
# ----------------------------------------------------
|
|
# Save
|
|
# ----------------------------------------------------
|
|
|
|
try:
|
|
|
|
if self.user:
|
|
|
|
Database.update_user(
|
|
self.user["id"],
|
|
self.enabled_checkbox.isChecked(),
|
|
allowances,
|
|
access_windows,
|
|
)
|
|
|
|
else:
|
|
|
|
Database.create_user(
|
|
username,
|
|
self.enabled_checkbox.isChecked(),
|
|
allowances,
|
|
access_windows,
|
|
)
|
|
|
|
except sqlite3.IntegrityError:
|
|
|
|
QMessageBox.warning(
|
|
self,
|
|
"User Already Exists",
|
|
"This user is already managed.",
|
|
)
|
|
|
|
return
|
|
|
|
except Exception as exc:
|
|
|
|
QMessageBox.critical(
|
|
self,
|
|
"Error",
|
|
f"Could not save the user:\n\n{exc}",
|
|
)
|
|
|
|
return
|
|
|
|
self.accept()
|
|
|
|
|
|
# ============================================================
|
|
# Users Page
|
|
# ============================================================
|
|
|
|
class UsersPage(QWidget):
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
|
|
layout = QVBoxLayout(self)
|
|
|
|
layout.setContentsMargins(
|
|
30,
|
|
30,
|
|
30,
|
|
30,
|
|
)
|
|
|
|
layout.setSpacing(20)
|
|
|
|
header = QHBoxLayout()
|
|
|
|
title = QLabel(
|
|
"Users"
|
|
)
|
|
|
|
title.setObjectName(
|
|
"PageTitle"
|
|
)
|
|
|
|
header.addWidget(title)
|
|
|
|
header.addStretch()
|
|
|
|
add_button = QPushButton(
|
|
"+ Add User"
|
|
)
|
|
|
|
add_button.clicked.connect(
|
|
lambda: self.window().add_user()
|
|
)
|
|
|
|
header.addWidget(
|
|
add_button
|
|
)
|
|
|
|
layout.addLayout(
|
|
header
|
|
)
|
|
|
|
description = QLabel(
|
|
"Manage Linux users and configure "
|
|
"their parental-control limits."
|
|
)
|
|
|
|
description.setObjectName(
|
|
"SecondaryText"
|
|
)
|
|
|
|
layout.addWidget(
|
|
description
|
|
)
|
|
|
|
self.scroll = QScrollArea()
|
|
|
|
self.scroll.setWidgetResizable(
|
|
True
|
|
)
|
|
|
|
self.scroll.setFrameShape(
|
|
QFrame.NoFrame
|
|
)
|
|
|
|
self.container = QWidget()
|
|
|
|
self.users_layout = QVBoxLayout(
|
|
self.container
|
|
)
|
|
|
|
self.users_layout.setContentsMargins(
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
)
|
|
|
|
self.users_layout.setSpacing(
|
|
12
|
|
)
|
|
|
|
self.scroll.setWidget(
|
|
self.container
|
|
)
|
|
|
|
layout.addWidget(
|
|
self.scroll
|
|
)
|
|
|
|
self.refresh()
|
|
|
|
def refresh(self):
|
|
|
|
while self.users_layout.count():
|
|
|
|
item = self.users_layout.takeAt(0)
|
|
|
|
widget = item.widget()
|
|
|
|
if widget:
|
|
widget.deleteLater()
|
|
|
|
users = Database.get_users()
|
|
|
|
for user in users:
|
|
|
|
row = QFrame()
|
|
|
|
row.setObjectName(
|
|
"UserRow"
|
|
)
|
|
|
|
row_layout = QHBoxLayout(
|
|
row
|
|
)
|
|
|
|
username = QLabel(
|
|
user["username"]
|
|
)
|
|
|
|
username.setObjectName(
|
|
"UserName"
|
|
)
|
|
|
|
row_layout.addWidget(
|
|
username
|
|
)
|
|
|
|
row_layout.addStretch()
|
|
|
|
status = QLabel(
|
|
"Enabled"
|
|
if user["enabled"]
|
|
else "Disabled"
|
|
)
|
|
|
|
row_layout.addWidget(
|
|
status
|
|
)
|
|
|
|
manage = QPushButton(
|
|
"Manage User"
|
|
)
|
|
|
|
manage.clicked.connect(
|
|
lambda checked=False,
|
|
user_id=user["id"]:
|
|
self.window().edit_user(
|
|
user_id
|
|
)
|
|
)
|
|
|
|
row_layout.addWidget(
|
|
manage
|
|
)
|
|
|
|
delete = QPushButton(
|
|
"Delete"
|
|
)
|
|
|
|
delete.clicked.connect(
|
|
lambda checked=False,
|
|
user_id=user["id"]:
|
|
self.window().delete_user(
|
|
user_id
|
|
)
|
|
)
|
|
|
|
row_layout.addWidget(
|
|
delete
|
|
)
|
|
|
|
self.users_layout.addWidget(
|
|
row
|
|
)
|
|
|
|
self.users_layout.addStretch()
|
|
|
|
|
|
# ============================================================
|
|
# Main Window
|
|
# ============================================================
|
|
|
|
class MainWindow(QMainWindow):
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
|
|
self.setWindowTitle(
|
|
"Linux Parental Control"
|
|
)
|
|
|
|
self.resize(
|
|
1100,
|
|
750,
|
|
)
|
|
|
|
self.pages = QStackedWidget()
|
|
|
|
self.dashboard = DashboardPage(
|
|
edit_callback=self.edit_user,
|
|
toggle_callback=self.toggle_user,
|
|
)
|
|
|
|
self.users = UsersPage()
|
|
|
|
self.pages.addWidget(
|
|
self.dashboard
|
|
)
|
|
|
|
self.pages.addWidget(
|
|
self.users
|
|
)
|
|
|
|
self.create_sidebar()
|
|
|
|
# --------------------------------------------------------
|
|
# Sidebar
|
|
# --------------------------------------------------------
|
|
|
|
def create_sidebar(self):
|
|
|
|
central = QWidget()
|
|
|
|
main_layout = QHBoxLayout(
|
|
central
|
|
)
|
|
|
|
main_layout.setContentsMargins(
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
)
|
|
|
|
main_layout.setSpacing(
|
|
0
|
|
)
|
|
|
|
sidebar = QFrame()
|
|
|
|
sidebar.setObjectName(
|
|
"Sidebar"
|
|
)
|
|
|
|
sidebar.setFixedWidth(
|
|
220
|
|
)
|
|
|
|
sidebar_layout = QVBoxLayout(
|
|
sidebar
|
|
)
|
|
|
|
sidebar_layout.setContentsMargins(
|
|
15,
|
|
20,
|
|
15,
|
|
20,
|
|
)
|
|
|
|
sidebar_layout.setSpacing(
|
|
8
|
|
)
|
|
|
|
title = QLabel(
|
|
"Parental Control"
|
|
)
|
|
|
|
title.setObjectName(
|
|
"SidebarTitle"
|
|
)
|
|
|
|
sidebar_layout.addWidget(
|
|
title
|
|
)
|
|
|
|
sidebar_layout.addSpacing(
|
|
20
|
|
)
|
|
|
|
dashboard_button = QPushButton(
|
|
"Dashboard"
|
|
)
|
|
|
|
dashboard_button.clicked.connect(
|
|
lambda: self.show_page(0)
|
|
)
|
|
|
|
sidebar_layout.addWidget(
|
|
dashboard_button
|
|
)
|
|
|
|
users_button = QPushButton(
|
|
"Users"
|
|
)
|
|
|
|
users_button.clicked.connect(
|
|
lambda: self.show_page(1)
|
|
)
|
|
|
|
sidebar_layout.addWidget(
|
|
users_button
|
|
)
|
|
|
|
sidebar_layout.addStretch()
|
|
|
|
refresh_button = QPushButton(
|
|
"Refresh"
|
|
)
|
|
|
|
refresh_button.clicked.connect(
|
|
self.refresh_all
|
|
)
|
|
|
|
sidebar_layout.addWidget(
|
|
refresh_button
|
|
)
|
|
|
|
main_layout.addWidget(
|
|
sidebar
|
|
)
|
|
|
|
main_layout.addWidget(
|
|
self.pages
|
|
)
|
|
|
|
self.setCentralWidget(
|
|
central
|
|
)
|
|
|
|
def show_page(self, index):
|
|
self.pages.setCurrentIndex(
|
|
index
|
|
)
|
|
|
|
# --------------------------------------------------------
|
|
# Add user
|
|
# --------------------------------------------------------
|
|
|
|
def add_user(self):
|
|
|
|
dialog = UserDialog(
|
|
parent=self
|
|
)
|
|
|
|
if dialog.exec():
|
|
|
|
self.refresh_all()
|
|
|
|
# --------------------------------------------------------
|
|
# Manage user
|
|
# --------------------------------------------------------
|
|
|
|
def edit_user(self, user_id):
|
|
|
|
user = Database.get_user(
|
|
user_id
|
|
)
|
|
|
|
if not user:
|
|
|
|
QMessageBox.warning(
|
|
self,
|
|
"User Not Found",
|
|
"The selected user no longer exists.",
|
|
)
|
|
|
|
return
|
|
|
|
dialog = UserDialog(
|
|
user=user,
|
|
parent=self,
|
|
)
|
|
|
|
if dialog.exec():
|
|
|
|
self.refresh_all()
|
|
|
|
# --------------------------------------------------------
|
|
# Enable / Disable
|
|
# --------------------------------------------------------
|
|
|
|
def toggle_user(
|
|
self,
|
|
user_id,
|
|
enabled,
|
|
):
|
|
|
|
user = Database.get_user(
|
|
user_id
|
|
)
|
|
|
|
if not user:
|
|
return
|
|
|
|
action = (
|
|
"enable"
|
|
if enabled
|
|
else "disable"
|
|
)
|
|
|
|
reply = QMessageBox.question(
|
|
self,
|
|
f"{action.title()} User",
|
|
f"Are you sure you want to "
|
|
f"{action} user "
|
|
f"\"{user['username']}\"?",
|
|
QMessageBox.Yes
|
|
| QMessageBox.No,
|
|
)
|
|
|
|
if reply != QMessageBox.Yes:
|
|
return
|
|
|
|
try:
|
|
|
|
Database.set_enabled(
|
|
user_id,
|
|
enabled,
|
|
)
|
|
|
|
except Exception as exc:
|
|
|
|
QMessageBox.critical(
|
|
self,
|
|
"Error",
|
|
f"Could not update the user:\n\n{exc}",
|
|
)
|
|
|
|
return
|
|
|
|
self.refresh_all()
|
|
|
|
# --------------------------------------------------------
|
|
# Delete user
|
|
# --------------------------------------------------------
|
|
|
|
def delete_user(self, user_id):
|
|
|
|
user = Database.get_user(
|
|
user_id
|
|
)
|
|
|
|
if not user:
|
|
return
|
|
|
|
reply = QMessageBox.question(
|
|
self,
|
|
"Delete Managed User",
|
|
f"Remove "
|
|
f"\"{user['username']}\" "
|
|
f"from parental control?\n\n"
|
|
f"This will NOT delete the Linux account.",
|
|
QMessageBox.Yes
|
|
| QMessageBox.No,
|
|
)
|
|
|
|
if reply != QMessageBox.Yes:
|
|
return
|
|
|
|
try:
|
|
|
|
Database.delete_user(
|
|
user_id
|
|
)
|
|
|
|
except Exception as exc:
|
|
|
|
QMessageBox.critical(
|
|
self,
|
|
"Error",
|
|
f"Could not delete the user:\n\n{exc}",
|
|
)
|
|
|
|
return
|
|
|
|
self.refresh_all()
|
|
|
|
# --------------------------------------------------------
|
|
# Refresh
|
|
# --------------------------------------------------------
|
|
|
|
def refresh_all(self):
|
|
|
|
self.dashboard.refresh()
|
|
self.users.refresh()
|
|
|
|
|
|
# ============================================================
|
|
# Theme
|
|
# ============================================================
|
|
|
|
def apply_theme(app):
|
|
|
|
app.setStyleSheet(
|
|
"""
|
|
QWidget {
|
|
font-family: "Noto Sans";
|
|
font-size: 14px;
|
|
}
|
|
|
|
QMainWindow {
|
|
background: #f5f5f5;
|
|
}
|
|
|
|
QFrame#Sidebar {
|
|
background: #202124;
|
|
}
|
|
|
|
QLabel#SidebarTitle {
|
|
color: white;
|
|
font-size: 20px;
|
|
font-weight: bold;
|
|
}
|
|
|
|
QPushButton {
|
|
padding: 9px 14px;
|
|
border-radius: 7px;
|
|
border: 1px solid #cccccc;
|
|
background: white;
|
|
}
|
|
|
|
QPushButton:hover {
|
|
background: #eeeeee;
|
|
}
|
|
|
|
QPushButton:pressed {
|
|
background: #dddddd;
|
|
}
|
|
|
|
QFrame#Sidebar QPushButton {
|
|
color: white;
|
|
background: transparent;
|
|
border: none;
|
|
text-align: left;
|
|
padding: 12px;
|
|
}
|
|
|
|
QFrame#Sidebar QPushButton:hover {
|
|
background: #303134;
|
|
}
|
|
|
|
QLabel#PageTitle {
|
|
font-size: 28px;
|
|
font-weight: bold;
|
|
}
|
|
|
|
QLabel#SectionTitle {
|
|
font-size: 18px;
|
|
font-weight: bold;
|
|
}
|
|
|
|
QLabel#UserName {
|
|
font-size: 18px;
|
|
font-weight: bold;
|
|
}
|
|
|
|
QLabel#SecondaryText {
|
|
color: #666666;
|
|
}
|
|
|
|
QLabel#TimeRemaining {
|
|
font-size: 22px;
|
|
font-weight: bold;
|
|
}
|
|
|
|
QLabel#StatusEnabled {
|
|
color: #188038;
|
|
font-weight: bold;
|
|
}
|
|
|
|
QLabel#StatusDisabled {
|
|
color: #d93025;
|
|
font-weight: bold;
|
|
}
|
|
|
|
QLabel#EmptyText {
|
|
color: #777777;
|
|
font-size: 16px;
|
|
padding: 40px;
|
|
}
|
|
|
|
QFrame#UserCard {
|
|
background: white;
|
|
border: 1px solid #dddddd;
|
|
border-radius: 12px;
|
|
}
|
|
|
|
QFrame#UserRow {
|
|
background: white;
|
|
border: 1px solid #dddddd;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
QProgressBar {
|
|
border: none;
|
|
border-radius: 5px;
|
|
background: #e5e5e5;
|
|
height: 10px;
|
|
}
|
|
|
|
QProgressBar::chunk {
|
|
border-radius: 5px;
|
|
background: #4f7cff;
|
|
}
|
|
|
|
QLineEdit,
|
|
QSpinBox,
|
|
QTimeEdit {
|
|
padding: 7px;
|
|
border: 1px solid #cccccc;
|
|
border-radius: 6px;
|
|
background: white;
|
|
}
|
|
|
|
QCheckBox {
|
|
spacing: 8px;
|
|
}
|
|
|
|
QScrollArea {
|
|
background: transparent;
|
|
}
|
|
"""
|
|
)
|
|
|
|
|
|
# ============================================================
|
|
# Main
|
|
# ============================================================
|
|
|
|
def main():
|
|
|
|
app = QApplication(sys.argv)
|
|
|
|
app.setApplicationName(
|
|
"Linux Parental Control"
|
|
)
|
|
|
|
apply_theme(app)
|
|
|
|
window = MainWindow()
|
|
|
|
window.show()
|
|
|
|
sys.exit(
|
|
app.exec()
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|