-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcli.py
More file actions
221 lines (182 loc) · 8.21 KB
/
Copy pathcli.py
File metadata and controls
221 lines (182 loc) · 8.21 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
from dotenv import dotenv_values
from app.blueprints import mail_sender, auth
from app.blueprints.database import DatabaseHandler
from app.models import Voter
from start import app
import csv
import uuid
import random
import click
import os
DOTENV_PATH = './app/.env'
def _cfg():
return dotenv_values(DOTENV_PATH)
def _promotion_list():
raw = _cfg().get('PROMOTION_LIST', '')
return [p.strip() for p in raw.split(',') if p.strip()]
# ── Email commands ─────────────────────────────────────────────────────────────
@click.command(help="Send invitation emails to voters who haven't received one yet")
def send_invitations():
with app.app_context():
voters = Voter.query.all()
pending = [v for v in voters if not v.invitation_sent]
if not pending:
click.echo('No pending invitations.')
return
ok, fail = 0, 0
for voter in pending:
try:
mail_sender.send_invitation_email(voter)
click.echo(f' ✓ {voter.email}')
ok += 1
except Exception as e:
click.echo(f' ✗ {voter.email}: {e}', err=True)
fail += 1
click.echo(f'\n{ok} sent, {fail} failed.')
@click.command(help="Send voting link emails to voters who haven't received one yet")
def send_links():
with app.app_context():
voters = Voter.query.all()
pending = [v for v in voters if not v.link_sent]
if not pending:
click.echo('No pending links.')
return
ok, fail = 0, 0
for voter in pending:
try:
mail_sender.send_link_email(voter)
click.echo(f' ✓ {voter.email}')
ok += 1
except Exception as e:
click.echo(f' ✗ {voter.email}: {e}', err=True)
fail += 1
click.echo(f'\n{ok} sent, {fail} failed.')
@click.command(help="Send reminder emails to voters who haven't voted yet")
def send_reminders():
with app.app_context():
voters = Voter.query.filter_by(voted=False).all()
if not voters:
click.echo('No voters to remind (all have voted or none registered).')
return
ok, fail = 0, 0
for voter in voters:
try:
mail_sender.send_reminder_email(voter)
click.echo(f' ✓ {voter.email}')
ok += 1
except Exception as e:
click.echo(f' ✗ {voter.email}: {e}', err=True)
fail += 1
click.echo(f'\n{ok} sent, {fail} failed.')
@click.command(help="Send a test email to the admin with the current configuration")
def send_admin_test():
with app.app_context():
try:
mail_sender.send_admin_test_email()
click.echo(f"Test email sent to {_cfg().get('ADMIN_EMAIL', '(admin)')}.")
except Exception as e:
click.echo(f'Failed: {e}', err=True)
# ── Voter commands ─────────────────────────────────────────────────────────────
@click.command(help="Import voters from a CSV file (columns: last_name, first_name, email, promotion)")
@click.option('--file', required=True, help='Path to the CSV file')
@click.option('--send-link', is_flag=True, default=False, help='Send voting link email to each imported voter')
def import_voters(file, send_link):
with app.app_context():
db = DatabaseHandler('app/var/db.sqlite')
ok, fail = 0, 0
try:
with open(file, 'r') as f:
reader = csv.reader(f)
next(reader)
for row in reader:
try:
last_name, first_name, email, promotion = row
link_string = str(uuid.uuid4())
secret = str(random.randint(0, 9999)).zfill(4)
db.add_voter(Voter(
last_name=last_name, first_name=first_name,
email=email, promotion=promotion,
link_string=link_string, secret=secret,
voted=False, invitation_sent=False, link_sent=False,
))
click.echo(f' ✓ Added {email}')
ok += 1
if send_link:
voter_obj = Voter.query.filter_by(email=email).first()
if voter_obj:
mail_sender.send_link_email(voter_obj)
click.echo(f' → Link sent to {email}')
except Exception as e:
click.echo(f' ✗ Row {row}: {e}', err=True)
fail += 1
except FileNotFoundError:
click.echo(f'File not found: {file}', err=True)
return
db.close_connection()
click.echo(f'\n{ok} imported, {fail} failed.')
@click.command(help="Validate a voters CSV file (checks email domains and promotions)")
@click.option('--file', required=True, help='Path to the CSV file')
def validate_csv(file):
with app.app_context():
promotions = _promotion_list()
errors = 0
try:
with open(file, 'r') as f:
reader = csv.reader(f)
next(reader)
for i, row in enumerate(reader, start=2):
try:
last_name, first_name, email, promotion = row
except ValueError:
click.echo(f' Line {i}: bad row format — {row}', err=True)
errors += 1
continue
if not mail_sender.validate_email_domain(email):
click.echo(f' Line {i}: invalid email or domain — {email}')
errors += 1
if promotions and promotion.strip() not in promotions:
click.echo(f' Line {i}: unknown promotion "{promotion}" for {email}')
errors += 1
except FileNotFoundError:
click.echo(f'File not found: {file}', err=True)
return
if errors:
click.echo(f'\n{errors} error(s) found.')
else:
click.echo('CSV is valid.')
# ── User commands ──────────────────────────────────────────────────────────────
@click.command(help="Create a system user (admin or technician)")
@click.option('--role', required=True, type=click.Choice(['admin', 'technician']), help='Role of the new user')
def create_user(role):
with app.app_context():
auth.create_user(role)
# ── Maintenance commands ───────────────────────────────────────────────────────
@click.command(help="Reset the application: wipe the database and RSA keys")
@click.confirmation_option(prompt='This will wipe the entire database. Are you sure?')
def reset():
removed, missing = [], []
for path in ('app/var/db.sqlite', 'app/var/pubkey.pem', 'app/var/privkey.pem'):
try:
os.remove(path)
removed.append(path)
except FileNotFoundError:
missing.append(path)
for p in removed:
click.echo(f' Deleted {p}')
for p in missing:
click.echo(f' Not found (skipped): {p}')
click.echo('Reset complete.')
# ── CLI group ─────────────────────────────────────────────────────────────────
@click.group()
def cli():
pass
cli.add_command(send_invitations)
cli.add_command(send_links)
cli.add_command(send_reminders)
cli.add_command(send_admin_test)
cli.add_command(import_voters)
cli.add_command(validate_csv)
cli.add_command(create_user)
cli.add_command(reset)
if __name__ == '__main__':
cli()