Skip to content

Commit 3852af3

Browse files
Email notification fixes
1 parent e8b4112 commit 3852af3

File tree

8 files changed

+134
-39
lines changed

8 files changed

+134
-39
lines changed

app/__init__.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,38 @@ def inject_user_date_format():
163163
with app.app_context():
164164
db.create_all()
165165

166+
# Automatic database migration for existing installations
167+
try:
168+
inspector = db.inspect(db.engine)
169+
170+
# Check UserSettings table for missing columns
171+
user_settings_columns = [col['name'] for col in inspector.get_columns('user_settings')]
172+
migrations_applied = []
173+
174+
if 'last_notification_sent' not in user_settings_columns:
175+
print("🔄 Auto-migrating: Adding last_notification_sent column to user_settings...")
176+
db.engine.execute('ALTER TABLE user_settings ADD COLUMN last_notification_sent DATE')
177+
migrations_applied.append("last_notification_sent")
178+
179+
if 'notification_time' not in user_settings_columns:
180+
print("🔄 Auto-migrating: Adding notification_time column to user_settings...")
181+
db.engine.execute('ALTER TABLE user_settings ADD COLUMN notification_time INTEGER DEFAULT 9')
182+
migrations_applied.append("notification_time")
183+
184+
# Check Subscription table for missing columns
185+
subscription_columns = [col['name'] for col in inspector.get_columns('subscription')]
186+
187+
if 'custom_notification_days' not in subscription_columns:
188+
print("🔄 Auto-migrating: Adding custom_notification_days column to subscription...")
189+
db.engine.execute('ALTER TABLE subscription ADD COLUMN custom_notification_days INTEGER')
190+
migrations_applied.append("custom_notification_days")
191+
192+
if migrations_applied:
193+
print(f"✅ Database migration completed! Added: {', '.join(migrations_applied)}")
194+
195+
except Exception as e:
196+
print(f"⚠️ Database migration skipped: {e}")
197+
166198
# Create default admin user if no admin users exist
167199
from app.models import User, UserSettings
168200
admin_exists = User.query.filter_by(is_admin=True).first()

app/email.py

Lines changed: 42 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -176,9 +176,12 @@ def send_expiry_notification(app, user, subscriptions):
176176
print("📨 Sending email...")
177177
server.send_message(msg)
178178

179-
# Update last notification date for all subscriptions
180-
for subscription in subscriptions:
181-
subscription.last_notification = datetime.now().date()
179+
# Update last notification date for the user (not individual subscriptions)
180+
user_settings = user.settings or UserSettings()
181+
user_settings.last_notification_sent = datetime.now().date()
182+
if not user.settings:
183+
user_settings.user_id = user.id
184+
db.session.add(user_settings)
182185
db.session.commit()
183186

184187
print(f"✅ Notification sent to {user.username} for {len(subscriptions)} subscriptions")
@@ -202,7 +205,9 @@ def send_expiry_notification(app, user, subscriptions):
202205
def check_expiring_subscriptions(app):
203206
"""Check for expiring subscriptions and send notifications"""
204207
with app.app_context():
205-
print(f"🔍 Checking expiring subscriptions at {datetime.now()}")
208+
current_time = datetime.now()
209+
current_hour = current_time.hour
210+
print(f"🔍 Checking expiring subscriptions at {current_time} (hour: {current_hour})")
206211

207212
# Get all users with notification settings
208213
users = User.query.all()
@@ -215,62 +220,62 @@ def check_expiring_subscriptions(app):
215220
if not user_settings.email_notifications:
216221
print(f"⏭️ Skipping {user.username} - notifications disabled")
217222
continue
218-
219-
days_before = user_settings.notification_days
220-
check_date = datetime.now().date() + timedelta(days=days_before)
223+
224+
# Check if we already sent a notification today for this user
225+
today = current_time.date()
226+
if user_settings.last_notification_sent == today:
227+
print(f"⏭️ Skipping {user.username} - already notified today")
228+
continue
229+
230+
# Check if it's the user's preferred notification time (±1 hour window)
231+
preferred_hour = user_settings.notification_time or 9
232+
if not (preferred_hour - 1 <= current_hour <= preferred_hour + 1):
233+
print(f"⏭️ Skipping {user.username} - not their notification time (prefers {preferred_hour}:00, current: {current_hour}:00)")
234+
continue
221235

222-
# Find subscriptions that are:
223-
# 1. Expiring within the notification window
224-
# 2. Haven't been notified today
225-
# 3. Are still active
226-
subscriptions = Subscription.query.filter(
227-
Subscription.user_id == user.id,
228-
Subscription.is_active == True,
229-
Subscription.end_date.isnot(None),
230-
Subscription.end_date <= check_date,
231-
Subscription.end_date >= datetime.now().date(),
232-
(Subscription.last_notification == None) |
233-
(Subscription.last_notification < datetime.now().date())
234-
).all()
236+
# Find subscriptions expiring based on their individual or default notification days
237+
expiring_subscriptions = []
238+
239+
for subscription in user.subscriptions:
240+
if not subscription.is_active or not subscription.end_date:
241+
continue
242+
243+
# Get notification days for this specific subscription
244+
notification_days = subscription.get_notification_days(user_settings)
245+
check_date = today + timedelta(days=notification_days)
246+
247+
# Check if this subscription is expiring within its notification window
248+
if subscription.end_date <= check_date and subscription.end_date >= today:
249+
expiring_subscriptions.append(subscription)
235250

236-
if subscriptions:
237-
print(f"📧 Sending notification to {user.username} for {len(subscriptions)} subscriptions")
238-
success = send_expiry_notification(app, user, subscriptions)
251+
if expiring_subscriptions:
252+
print(f"📧 Sending notification to {user.username} for {len(expiring_subscriptions)} subscriptions at preferred time {preferred_hour}:00")
253+
success = send_expiry_notification(app, user, expiring_subscriptions)
239254
if success:
240255
total_notifications += 1
241256
else:
242257
print(f"❌ Failed to send notification to {user.username}")
243258
else:
244-
print(f"✅ No notifications needed for {user.username}")
259+
print(f"✅ No expiring subscriptions for {user.username}")
245260

246261
print(f"📊 Notification check completed. Sent {total_notifications} notifications.")
247262

248263
def start_scheduler(app):
249264
"""Start the background scheduler for checking expiring subscriptions"""
250265
scheduler = BackgroundScheduler()
251266

252-
# Check every 6 hours instead of daily for more timely notifications
267+
# Check every hour to respect user-specific notification times
253268
scheduler.add_job(
254269
func=lambda: check_expiring_subscriptions(app),
255270
trigger="interval",
256-
hours=6,
271+
hours=1,
257272
id='check_subscriptions',
258273
replace_existing=True
259274
)
260275

261-
# Also add a daily job at 9 AM for primary notifications
262-
scheduler.add_job(
263-
func=lambda: check_expiring_subscriptions(app),
264-
trigger="cron",
265-
hour=9,
266-
minute=0,
267-
id='daily_check_subscriptions',
268-
replace_existing=True
269-
)
270-
271276
scheduler.start()
272277
atexit.register(lambda: scheduler.shutdown())
273-
print("Email notification scheduler started")
278+
print("Email notification scheduler started (checking hourly)")
274279

275280
def send_test_email(app, user):
276281
"""Send a test email to verify email configuration"""

app/forms.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ class SubscriptionForm(FlaskForm):
4747
payment_method_id = SelectField('Payment Method', coerce=int, validators=[Optional()])
4848
start_date = DateField('Start Date', validators=[DataRequired()])
4949
end_date = DateField('End Date (Leave blank for infinite)', validators=[Optional()])
50+
custom_notification_days = IntegerField('Custom notification days (override default)',
51+
validators=[Optional(), NumberRange(min=1, max=365)],
52+
render_kw={'placeholder': 'Leave blank to use default'})
5053
notes = TextAreaField('Notes', validators=[Optional()])
5154

5255
def __init__(self, *args, **kwargs):
@@ -85,6 +88,10 @@ class NotificationSettingsForm(FlaskForm):
8588
email_notifications = BooleanField('Enable Email Notifications')
8689
notification_days = IntegerField('Days before expiry to send notification',
8790
validators=[DataRequired(), NumberRange(min=1, max=365)])
91+
notification_time = SelectField('Daily notification time',
92+
choices=[(i, f'{i:02d}:00') for i in range(24)],
93+
coerce=int,
94+
validators=[DataRequired()])
8895

8996
class GeneralSettingsForm(FlaskForm):
9097
currency = SelectField('Preferred Display Currency', validators=[DataRequired()])

app/models.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ class UserSettings(db.Model):
2828
# Notification settings
2929
email_notifications = db.Column(db.Boolean, default=True)
3030
notification_days = db.Column(db.Integer, default=7)
31+
notification_time = db.Column(db.Integer, default=9) # Hour of day (0-23) when to send notifications
32+
last_notification_sent = db.Column(db.Date) # Track when last daily summary was sent
3133

3234
# General settings
3335
currency = db.Column(db.String(3), default='EUR')
@@ -128,6 +130,7 @@ class Subscription(db.Model):
128130
last_notification = db.Column(db.Date)
129131
notes = db.Column(db.Text)
130132
is_active = db.Column(db.Boolean, default=True)
133+
custom_notification_days = db.Column(db.Integer) # Override default notification days for this subscription
131134

132135
# Relationships
133136
payment_method = db.relationship('PaymentMethod', backref='subscriptions')
@@ -220,6 +223,10 @@ def days_until_expiry(self):
220223
delta = self.end_date - datetime.now().date()
221224
return delta.days if delta.days >= 0 else 0
222225

226+
def get_notification_days(self, user_settings):
227+
"""Get effective notification days for this subscription (custom or user default)"""
228+
return self.custom_notification_days if self.custom_notification_days is not None else user_settings.notification_days
229+
223230
def get_monthly_cost_in_currency(self, target_currency):
224231
"""Get monthly cost converted to target currency using currency converter with timeout protection"""
225232
from app.currency import currency_converter

app/routes.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,7 @@ def add_subscription():
270270
payment_method_id=payment_method_id,
271271
start_date=form.start_date.data,
272272
end_date=form.end_date.data,
273+
custom_notification_days=form.custom_notification_days.data,
273274
notes=form.notes.data,
274275
user_id=current_user.id
275276
)
@@ -306,6 +307,7 @@ def edit_subscription(id):
306307
subscription.payment_method_id = payment_method_id
307308
subscription.start_date = form.start_date.data
308309
subscription.end_date = form.end_date.data
310+
subscription.custom_notification_days = form.custom_notification_days.data
309311
subscription.notes = form.notes.data
310312
db.session.commit()
311313
flash('Subscription updated successfully!', 'success')
@@ -374,6 +376,7 @@ def notification_settings():
374376
settings = current_user.settings
375377
settings.email_notifications = form.email_notifications.data
376378
settings.notification_days = form.notification_days.data
379+
settings.notification_time = form.notification_time.data
377380
db.session.commit()
378381
flash('Notification settings updated successfully!', 'success')
379382
return redirect(url_for('main.notification_settings'))

app/templates/add_subscription.html

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,19 @@ <h2 class="mb-0">
157157
</div>
158158
</div>
159159

160+
<div class="mb-3">
161+
{{ form.custom_notification_days.label(class="form-label") }}
162+
{{ form.custom_notification_days(class="form-control") }}
163+
<small class="form-text text-muted">Override default notification days for this subscription only</small>
164+
{% if form.custom_notification_days.errors %}
165+
<div class="text-danger mt-1">
166+
{% for error in form.custom_notification_days.errors %}
167+
<small>{{ error }}</small>
168+
{% endfor %}
169+
</div>
170+
{% endif %}
171+
</div>
172+
160173
<div class="mb-3">
161174
{{ form.notes.label(class="form-label") }}
162175
{{ form.notes(class="form-control", rows="3") }}

app/templates/edit_subscription.html

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,19 @@ <h2 class="mb-0">
172172
</div>
173173
</div>
174174

175+
<div class="mb-3">
176+
{{ form.custom_notification_days.label(class="form-label") }}
177+
{{ form.custom_notification_days(class="form-control") }}
178+
<small class="form-text text-muted">Override default notification days for this subscription only</small>
179+
{% if form.custom_notification_days.errors %}
180+
<div class="text-danger mt-1">
181+
{% for error in form.custom_notification_days.errors %}
182+
<small>{{ error }}</small>
183+
{% endfor %}
184+
</div>
185+
{% endif %}
186+
</div>
187+
175188
<div class="mb-3">
176189
{{ form.notes.label(class="form-label") }}
177190
{{ form.notes(class="form-control", rows="3") }}

app/templates/notification_settings.html

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,21 @@ <h2 class="mb-0">
4040
{% endif %}
4141
</div>
4242

43+
<div class="mb-3">
44+
{{ form.notification_time.label(class="form-label") }}
45+
{{ form.notification_time(class="form-control") }}
46+
<small class="form-text text-muted">
47+
Time of day to send daily notification emails (24-hour format)
48+
</small>
49+
{% if form.notification_time.errors %}
50+
<div class="text-danger mt-1">
51+
{% for error in form.notification_time.errors %}
52+
<small>{{ error }}</small>
53+
{% endfor %}
54+
</div>
55+
{% endif %}
56+
</div>
57+
4358
<div class="d-flex justify-content-between">
4459
<a href="{{ url_for('main.dashboard') }}" class="btn btn-secondary">
4560
<i class="fas fa-arrow-left me-2"></i>Back to Dashboard
@@ -86,8 +101,8 @@ <h6 class="card-title">
86101
<i class="fas fa-info-circle me-2"></i>Notification Information
87102
</h6>
88103
<ul class="list-unstyled mb-0">
89-
<li><i class="fas fa-check text-success me-2"></i>Notifications are checked every 6 hours</li>
90-
<li><i class="fas fa-check text-success me-2"></i>Daily notifications are sent at 9:00 AM</li>
104+
<li><i class="fas fa-check text-success me-2"></i>Notifications are checked every hour</li>
105+
<li><i class="fas fa-check text-success me-2"></i>Daily notifications are sent at your preferred time</li>
91106
<li><i class="fas fa-check text-success me-2"></i>Only active subscriptions trigger notifications</li>
92107
<li><i class="fas fa-check text-success me-2"></i>You won't receive duplicate notifications on the same day</li>
93108
<li><i class="fas fa-paper-plane text-info me-2"></i>Use the "Send Test Email" button above to verify your email configuration</li>

0 commit comments

Comments
 (0)