|
| 1 | +from members.models import User |
| 2 | +from django.core.management.base import BaseCommand |
| 3 | +import time |
| 4 | +from interview.notification import send_email |
| 5 | + |
| 6 | +SENDER_EMAIL = "[email protected]" |
| 7 | +COOL_DOWN = 1 |
| 8 | + |
| 9 | +email_template = ( |
| 10 | + lambda user: f""" |
| 11 | +<!DOCTYPE html> |
| 12 | +<html lang="en"> |
| 13 | +<head> |
| 14 | + <meta charset="UTF-8"> |
| 15 | + <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| 16 | + <title>Verify Your SWECC Account</title> |
| 17 | + <style> |
| 18 | + body {{ |
| 19 | + font-family: Arial, sans-serif; |
| 20 | + line-height: 1.6; |
| 21 | + color: #282A2E; |
| 22 | + margin: 0; |
| 23 | + padding: 0; |
| 24 | + background-color: #f4f4f4; |
| 25 | + }} |
| 26 | + .container {{ |
| 27 | + max-width: 600px; |
| 28 | + margin: 20px auto; |
| 29 | + background-color: #ffffff; |
| 30 | + padding: 20px; |
| 31 | + border-radius: 5px; |
| 32 | + box-shadow: 0 0 10px rgba(0,0,0,0.1); |
| 33 | + }} |
| 34 | + .header {{ |
| 35 | + text-align: center; |
| 36 | + margin-bottom: 20px; |
| 37 | + }} |
| 38 | + .logo {{ |
| 39 | + max-width: 200px; |
| 40 | + height: auto; |
| 41 | + }} |
| 42 | + h1 {{ |
| 43 | + color: #6D7EE7; |
| 44 | + margin-top: 0; |
| 45 | + }} |
| 46 | + .content {{ |
| 47 | + margin-bottom: 20px; |
| 48 | + }} |
| 49 | + .verification-info {{ |
| 50 | + background-color: #f0f0f0; |
| 51 | + padding: 15px; |
| 52 | + border-radius: 5px; |
| 53 | + margin-bottom: 20px; |
| 54 | + }} |
| 55 | + .command {{ |
| 56 | + background-color: #eaecf6; |
| 57 | + padding: 12px; |
| 58 | + border-radius: 4px; |
| 59 | + font-family: monospace; |
| 60 | + font-weight: bold; |
| 61 | + color: #5548c2; |
| 62 | + display: inline-block; |
| 63 | + margin: 10px 0; |
| 64 | + }} |
| 65 | + .footer {{ |
| 66 | + text-align: center; |
| 67 | + font-size: 0.9em; |
| 68 | + color: #666; |
| 69 | + margin-top: 20px; |
| 70 | + border-top: 1px solid #eee; |
| 71 | + padding-top: 10px; |
| 72 | + }} |
| 73 | + .button {{ |
| 74 | + display: inline-block; |
| 75 | + padding: 10px 20px; |
| 76 | + background-color: #8852F6; |
| 77 | + color: white; |
| 78 | + text-decoration: none; |
| 79 | + border-radius: 5px; |
| 80 | + margin-top: 10px; |
| 81 | + }} |
| 82 | + .important {{ |
| 83 | + font-weight: bold; |
| 84 | + }} |
| 85 | + </style> |
| 86 | +</head> |
| 87 | +<body> |
| 88 | + <div class="container"> |
| 89 | + <div class="header"> |
| 90 | + <img src="https://bbaxszapshozxdvglcjg.supabase.co/storage/v1/object/public/assets/brand/swecc-logo.png" alt="SWECC Logo" class="logo"> |
| 91 | + <h1>Verify Your SWECC Account</h1> |
| 92 | + </div> |
| 93 | + <div class="content"> |
| 94 | + <p>Hello {user.first_name},</p> |
| 95 | + <p>We noticed that you haven't verified your SWECC account yet. To get the most out of your membership in SWECC, it's important that you finish the verification process. Most noteably, referral program eligibility hinges on us being able to connect your Discord account to your profile.</p> |
| 96 | + <p>Follow the instructions below to verify your account:</p> |
| 97 | +
|
| 98 | + <div class="verification-info"> |
| 99 | + <h2>Verification Instructions</h2> |
| 100 | + <p>In the Discord server, use this command to verify your account:</p> |
| 101 | + <div class="command">/verify {user.username}</div> |
| 102 | +
|
| 103 | + <p> If you've forgotten your password, you can reset it using another command:</p> |
| 104 | + <div class="command">/reset_password</div> |
| 105 | +
|
| 106 | + <p class="important">For verification and password reset to work, your Discord username <strong>must</strong> match the username you entered: <strong>{user.discord_username}</strong></p> |
| 107 | +
|
| 108 | + <p>If your Discord username doesn't match, please contact us at <a href="mailto:[email protected]">[email protected]</a> to update your information.</p> |
| 109 | + </div> |
| 110 | + </div> |
| 111 | + <div class="footer"> |
| 112 | + <p>This is an automated message from SWECC</p> |
| 113 | + </div> |
| 114 | + </div> |
| 115 | +</body> |
| 116 | +</html> |
| 117 | +""" |
| 118 | +) |
| 119 | + |
| 120 | + |
| 121 | +class Command(BaseCommand): |
| 122 | + help = "Command to send a reminder email to all unverified users" |
| 123 | + |
| 124 | + def add_arguments(self, parser): |
| 125 | + parser.add_argument( |
| 126 | + "--all", action="store_true", help="Send reminder to all users" |
| 127 | + ) |
| 128 | + parser.add_argument( |
| 129 | + "--username", type=str, help="SWECC Interview Website Username" |
| 130 | + ) |
| 131 | + parser.add_argument( |
| 132 | + "--dry-run", action="store_true", help="Dry run, do not send emails" |
| 133 | + ) |
| 134 | + parser.add_argument( |
| 135 | + "--preview", action="store_true", help="Preview email content" |
| 136 | + ) |
| 137 | + |
| 138 | + def is_verified(self, user): |
| 139 | + return user.groups.filter(name="is_verified").exists() |
| 140 | + |
| 141 | + def get_target_users(self, options): |
| 142 | + if options["username"]: |
| 143 | + users = User.objects.filter(username=options["username"]) |
| 144 | + if not users.exists(): |
| 145 | + return None, f"User with username {options['username']} not found" |
| 146 | + return list(users), None |
| 147 | + |
| 148 | + if options["all"]: |
| 149 | + users = [user for user in User.objects.all() if not self.is_verified(user)] |
| 150 | + if not users: |
| 151 | + return None, "No unverified users found" |
| 152 | + return users, None |
| 153 | + |
| 154 | + return None, "Either --all or --username must be specified" |
| 155 | + |
| 156 | + def send_reminder_email(self, user): |
| 157 | + html_content = email_template(user) |
| 158 | + subject = "SWECC Account Verification Required" |
| 159 | + |
| 160 | + if not user.email: |
| 161 | + return False, f"⚠ User {user.username} has no email address" |
| 162 | + |
| 163 | + try: |
| 164 | + send_email( |
| 165 | + from_email=SENDER_EMAIL, |
| 166 | + to_email=user.email, |
| 167 | + subject=subject, |
| 168 | + html_content=html_content, |
| 169 | + ) |
| 170 | + return True, f"✓ Sent reminder to {user.email}" |
| 171 | + except Exception as e: |
| 172 | + return False, f"✗ Failed to send reminder to {user.email}: {str(e)}" |
| 173 | + |
| 174 | + def preview_email(self, user): |
| 175 | + html_content = email_template(user) |
| 176 | + self.stdout.write( |
| 177 | + self.style.SUCCESS(f"\nPreview of email for {user.username}:\n") |
| 178 | + ) |
| 179 | + self.stdout.write(html_content) |
| 180 | + self.stdout.write("\n") |
| 181 | + |
| 182 | + def handle(self, *args, **options): |
| 183 | + pp = ( |
| 184 | + lambda user: f"Username: {user.username}, Email: {user.email}, Discord ID: {user.discord_id}" |
| 185 | + ) |
| 186 | + |
| 187 | + users, error = self.get_target_users(options) |
| 188 | + if error is not None: |
| 189 | + self.stdout.write(self.style.ERROR(error)) |
| 190 | + return |
| 191 | + |
| 192 | + self.stdout.write( |
| 193 | + self.style.SUCCESS( |
| 194 | + f"Found {len(users)} {'user' if len(users) == 1 else 'users'} to send reminders to" |
| 195 | + ) |
| 196 | + ) |
| 197 | + |
| 198 | + for user in users: |
| 199 | + self.stdout.write( |
| 200 | + self.style.SUCCESS(pp(user) + f", Verified: {self.is_verified(user)}") |
| 201 | + ) |
| 202 | + |
| 203 | + if options.get("preview"): |
| 204 | + self.preview_email(users[0]) |
| 205 | + return |
| 206 | + |
| 207 | + if options["dry_run"]: |
| 208 | + self.stdout.write( |
| 209 | + self.style.SUCCESS("Dry run completed. No emails were sent.") |
| 210 | + ) |
| 211 | + return |
| 212 | + |
| 213 | + sent_count, error_count = 0, 0 |
| 214 | + for user in users: |
| 215 | + success, message = self.send_reminder_email(user) |
| 216 | + self.stdout.write( |
| 217 | + self.style.SUCCESS(message) if success else self.style.ERROR(message) |
| 218 | + ) |
| 219 | + |
| 220 | + if success: |
| 221 | + sent_count += 1 |
| 222 | + else: |
| 223 | + error_count += 1 |
| 224 | + |
| 225 | + time.sleep(COOL_DOWN) |
| 226 | + |
| 227 | + self.stdout.write( |
| 228 | + self.style.SUCCESS( |
| 229 | + f"Completed sending reminders: {sent_count} sent, {error_count} failed" |
| 230 | + ) |
| 231 | + ) |
0 commit comments