def send_approval_queue_reminder(self): """ Send notification to approval authority for clubs awaiting approval. """ now = timezone.now() group_name = "Approvers" # only send notifications if it is currently a weekday if now.isoweekday() not in range(1, 6): return False # get users in group to send notification to group = Group.objects.filter(name=group_name).first() if group is None: self.stdout.write( self.style.WARNING( f"There is no Django admin group named '{group_name}' in the database. " "Cannot send out approval queue notification emails!")) return False emails = [ e for e in group.user_set.all().values_list("email", flat=True) if e ] if not emails: self.stdout.write( self.style.WARNING( f"There are no users or no associated emails in the '{group_name}' group. " "No emails will be sent out.")) return False # get clubs that need approval queued_clubs = Club.objects.filter(active=True, approved__isnull=True) if queued_clubs.exists(): context = { "num_clubs": queued_clubs.count(), "url": f"https://{settings.DOMAIN}/admin#queue", "clubs": list( queued_clubs.order_by("name").values_list("name", flat=True)), } count = queued_clubs.count() send_mail_helper( "approval_queue_reminder", "{} clubs awaiting review on {}".format( count, settings.BRANDING_SITE_NAME), emails, context, ) self.stdout.write( self.style.SUCCESS( f"Sent approval queue reminder for {count} clubs to {', '.join(emails)}" )) return True
def send_hap_intro_email(email, resources, recipient_string, template="intro"): """ Send the Hub@Penn introduction email given the email and the list of resources. """ send_mail_helper( template, None, [email], {"resources": resources, "recipient_string": recipient_string} )
def send_fair_email(club, email, template="fair"): """ Sends the SAC fair email for a club to the given email. """ domain = settings.DEFAULT_DOMAIN context = { "name": club.name, "url": settings.VIEW_URL.format(domain=domain, club=club.code), "flyer_url": settings.FLYER_URL.format(domain=domain, club=club.code), } send_mail_helper(template, "Making the SAC Fair Easier for You", [email], context)
def send_reminder_to_club(club): """ Sends an email reminder to clubs to update their information. """ receivers = None staff = club.members.filter(membership__role__lte=Membership.ROLE_OFFICER) # calculate email recipients if staff.exists(): # if there are staff members that can edit the page, send the update email to them receivers = list(staff.values_list("email", flat=True)) elif club.email: invites = club.membershipinvite_set.filter( active=True, role__lte=Membership.ROLE_OFFICER) if invites.exists(): # if there are existing invites, resend the invite emails for invite in invites: if invite.role <= Membership.ROLE_OWNER: invite.send_owner_invite() else: invite.send_mail() return True else: # if there are no owner-level invites or members, create and send an owner invite if club.email: invite = MembershipInvite.objects.create( club=club, email=club.email, creator=None, role=Membership.ROLE_OWNER, title="Owner", auto=True, ) invite.send_owner_invite() return True else: return False # send email if recipients exist if receivers is not None: domain = settings.DEFAULT_DOMAIN context = { "name": club.name, "url": settings.EDIT_URL.format(domain=domain, club=club.code), "view_url": settings.VIEW_URL.format(domain=domain, club=club.code), } send_mail_helper("remind", "Reminder to Update Your Club's Page", receivers, context) return True return False
def send_application_notifications(self): """ Send notifications about application deadlines three days before the deadline, for students that have subscribed to those organizations. Ignore students that have already graduated and students that are already in the club. """ now = timezone.now() + datetime.timedelta(days=3) apps = ClubApplication.objects.filter( Q(club__subscribe__person__profile__graduation_year__gte=now.year + (now.month >= 6)) | Q(club__subscribe__person__profile__graduation_year__isnull=True), application_end_time__date=now.date(), ).values_list( "club__code", "club__name", "club__subscribe__person__email", "club__subscribe__person__pk", ) # compute users already in clubs already_in_club = set( Membership.objects.filter(club__code__in=[x[0] for x in apps], person__pk__in=[x[3] for x in apps ]).values_list( "club__code", "person__pk")) # group clubs by user emails = collections.defaultdict(list) for code, name, email, user_pk in apps: if (code, user_pk) not in already_in_club: emails[email].append( (name, settings.APPLY_URL.format(domain=settings.DOMAIN, club=code))) # send out one email per user for email, data in emails.items(): context = {"clubs": data} send_mail_helper( "application_deadline_reminder", f"{len(data)} club(s) have application deadlines approaching", [email], context, ) self.stdout.write( self.style.SUCCESS( f"Sent application deadline reminder to {len(emails)} user(s)") )
def send_wc_intro_email(emails, clubs, recipient_string, template="wc_intro"): """ Send the Hub@Penn introduction email given the email and the list of resources. """ send_mail_helper(template, None, emails, {"clubs": clubs, "recipient_string": recipient_string})
def handle(self, *args, **kwargs): dry_run = kwargs["dry_run"] only_sheet = kwargs["only_sheet"] action = kwargs["type"] verbosity = kwargs["verbosity"] include_staff = kwargs["include_staff"] role = kwargs["role"] role_mapping = {k: v for k, v in Membership.ROLE_CHOICES} email_file = kwargs["emails"] test_email = kwargs.get("test", None) # download file if url if email_file is not None and re.match("^https?://", email_file, re.I): tf = tempfile.NamedTemporaryFile(delete=False) resp = requests.get(email_file) tf.write(resp.content) self.stdout.write(f"Downloaded '{email_file}' to '{tf.name}'.") email_file = tf.name tf.close() # handle custom Hub@Penn intro email if action in { "hap_intro", "hap_intro_remind", "hap_second_round", "hap_partner_communication", }: people = collections.defaultdict(dict) if action == "hap_partner_communication": emails = ( Membership.objects.filter(role__lte=Membership.ROLE_OFFICER) .values_list("person__email", flat=True) .distinct() ) if test_email is not None: emails = [test_email] for email in emails: if not dry_run: send_mail_helper("communication_to_partners", None, [email], {}) self.stdout.write(f"Sent {action} email to {email}") else: self.stdout.write(f"Would have sent {action} email to {email}") return # read recipients from csv file with open(email_file, "r") as f: header = [ re.sub(r"\W+", "", h.lower().strip().replace(" ", "_")) for h in f.readline().split(",") ] reader = csv.DictReader(f, fieldnames=header) try: for line in reader: name = line["name"].strip() email = line["email"].strip() if "contact" in line: contact = line["contact"].strip() else: contact = "" if test_email is not None: email = test_email if name and email: if email in people.keys(): people[email]["resources"].append(name) people[email]["contacts"].append(contact) else: people[email]["resources"] = [name] people[email]["contacts"] = [contact] except KeyError as e: raise ValueError( "Ensure the spreadsheet has a header with the 'name' and 'email' columns." ) from e # send emails grouped by recipients for email, context in people.items(): contacts = list(set(context["contacts"])) # No duplicate names contacts = list(filter(lambda x: x != "", contacts)) # No empty string names if len(contacts) == 0: contacts.append("Staff member") # Format names in comma separated form recipient_string = ", ".join(contacts) resources = context["resources"] if not dry_run: send_hap_intro_email( email, resources, recipient_string, template={ "hap_intro": "intro", "hap_intro_remind": "intro_remind", "hap_second_round": "second_round", "wc_intro": "wc_intro", }[action], ) self.stdout.write( f"Sent {action} email to {email} (recipients: " + f"{recipient_string}) for groups: {resources}" ) else: self.stdout.write( f"Would have sent {action} email to {email} (recipients: " + f"{recipient_string}) for groups: {resources}" ) self.stdout.write(f"Sent out {len(people)} emails!") return if action in {"wc_intro"}: people = collections.defaultdict(dict) # read recipients from csv file with open(email_file, "r") as f: header = [ re.sub(r"\W+", "", h.lower().strip().replace(" ", "_")) for h in f.readline().split(",") ] reader = csv.DictReader(f, fieldnames=header) try: for line in reader: name = line["name"].strip() email = line["email"].strip() contact = line["contact"].strip() if test_email is not None: email = test_email if name in people.keys(): people[name]["emails"].append(email) people[name]["contacts"].append(contact) else: people[name]["emails"] = [email] people[name]["contacts"] = [contact] except KeyError as e: raise ValueError( "Ensure the spreadsheet has a header with the 'name' and 'email' columns." ) from e # send emails grouped by recipients for name, context in people.items(): emails = list(set(context["emails"])) # No duplicate names contacts = list(set(context["contacts"])) # No duplicate names contacts = list(filter(lambda x: x != "", contacts)) # No empty string names if len(contacts) == 0: contacts.append("Staff member") # Format names in comma separated form recipient_string = ", ".join(contacts) clubs = [name] if not dry_run: send_wc_intro_email( emails, clubs, recipient_string, template={"wc_intro": "wc_intro"}[action], ) self.stdout.write( f"Sent {action} email to {email} (recipients: " + f"{recipient_string}) for groups: {clubs}" ) else: self.stdout.write( f"Would have sent {action} email to {email} (recipients: " + f"{recipient_string}) for groups: {clubs}" ) return # get club whitelist clubs_whitelist = [club.strip() for club in (kwargs.get("clubs") or "").split(",")] clubs_whitelist = [club for club in clubs_whitelist if club] found_whitelist = set( Club.objects.filter(code__in=clubs_whitelist).values_list("code", flat=True) ) missing = set(clubs_whitelist) - found_whitelist if missing: raise CommandError(f"Invalid club codes in clubs parameter: {missing}") # load fair now = timezone.now() if action in {"virtual_fair", "urgent_virtual_fair", "post_virtual_fair"}: fair_id = kwargs.get("fair") if fair_id is not None: fair = ClubFair.objects.get(id=fair_id) else: fair = ClubFair.objects.filter(end_time__gte=now).order_by("start_time").first() if fair is None: raise CommandError("Could not find an upcoming activities fair!") # handle sending out virtual fair emails if action == "virtual_fair": clubs = fair.participating_clubs.all() if clubs_whitelist: self.stdout.write(f"Using clubs whitelist: {clubs_whitelist}") clubs = clubs.filter(code__in=clubs_whitelist) self.stdout.write(f"Found {clubs.count()} clubs participating in the {fair.name} fair.") extra = kwargs.get("extra", False) limit = kwargs.get("limit", False) self.stdout.write(f"Extra flag status: {extra}") for club in clubs: emails = [test_email] if test_email else None emails_disp = emails or "officers" if limit: if club.events.filter( ~Q(url="") & ~Q(url__isnull=True), start_time__gte=now, type=Event.FAIR ).exists(): self.stdout.write(f"Skipping {club.name}, fair event already set up.") continue if not dry_run: status = club.send_virtual_fair_email(fair=fair, emails=emails, extra=extra) self.stdout.write( f"Sent virtual fair email to {club.name} ({emails_disp})... -> {status}" ) else: self.stdout.write( f"Would have sent virtual fair email to {club.name} ({emails_disp})..." ) return elif action == "urgent_virtual_fair": clubs = fair.participating_clubs.filter( Q(events__url="") | Q(events__url__isnull=True), events__type=Event.FAIR, events__start_time__gte=fair.start_time, events__end_time__lte=fair.end_time, ) if clubs_whitelist: self.stdout.write(f"Using clubs whitelist: {clubs_whitelist}") clubs = clubs.filter(code__in=clubs_whitelist) self.stdout.write(f"{clubs.count()} clubs have not registered for the {fair.name}.") extra = kwargs.get("extra", False) for club in clubs.distinct(): emails = [test_email] if test_email else None emails_disp = emails or "officers" if not dry_run: self.stdout.write( f"Sending fair urgent reminder for {club.name} to {emails_disp}..." ) club.send_virtual_fair_email( email="urgent", fair=fair, extra=extra, emails=emails ) else: self.stdout.write( f"Would have sent fair urgent reminder for {club.name} to {emails_disp}..." ) # don't continue return elif action == "post_virtual_fair": clubs = fair.participating_clubs.filter( events__type=Event.FAIR, events__start_time__gte=fair.start_time, events__end_time__lte=fair.end_time, ) if clubs_whitelist: clubs = clubs.filter(code__in=clubs_whitelist) self.stdout.write(f"{clubs.count()} post fair emails to send to participants.") for club in clubs.distinct(): self.stdout.write(f"Sending post fair reminder to {club.name}...") if not dry_run: club.send_virtual_fair_email(email="post") return # handle all other email events if only_sheet: clubs = Club.objects.all() else: # find all clubs without owners or owner invitations clubs = Club.objects.annotate( owner_count=Count( "membership", filter=Q(membership__role__lte=Membership.ROLE_OWNER) ), invite_count=Count( "membershipinvite", filter=Q(membershipinvite__role__lte=Membership.ROLE_OWNER, active=True), ), ).filter(owner_count=0, invite_count=0) self.stdout.write(f"Found {clubs.count()} active club(s) without owners.") if kwargs["only_active"]: clubs = clubs.filter(active=True) if clubs_whitelist: clubs = clubs.filter(code__in=clubs_whitelist) clubs_missing = 0 clubs_sent = 0 # parse CSV file emails = {} # verify email file if email_file is not None: if not os.path.isfile(email_file): raise CommandError(f'Email file "{email_file}" does not exist!') elif only_sheet: raise CommandError("Cannot specify only sheet option without an email file!") else: self.stdout.write(self.style.WARNING("No email spreadsheet file specified!")) # load email file if email_file is not None: with open(email_file, "r") as f: reader = csv.reader(f) for line in reader: if not line: self.stdout.write(self.style.WARNING("Skipping empty line in CSV file...")) continue raw_name = line[0].strip() club = fuzzy_lookup_club(raw_name) if club is not None: if verbosity >= 2: self.stdout.write(f"Mapped {raw_name} -> {club.name} ({club.code})") clubs_sent += 1 emails[club.id] = [x.strip() for x in line[1].split(",")] else: clubs_missing += 1 self.stdout.write( self.style.WARNING(f"Could not find club matching {raw_name}!") ) # send out emails for club in clubs: if club.email: receivers = [club.email] if club.id in emails: if only_sheet: receivers = emails[club.id] else: receivers += emails[club.id] elif only_sheet: continue if include_staff: receivers += list( club.membership_set.filter( role__lte=Membership.ROLE_OFFICER, person__email__isnull=False ) .exclude(person__email="") .values_list("person__email", flat=True) ) receivers = list(set(receivers)) receivers_str = ", ".join(receivers) self.stdout.write( self.style.SUCCESS(f"Sending {action} email for {club.name} to {receivers_str}") ) for receiver in receivers: if not dry_run: if action == "invite": existing_membership = Membership.objects.filter( person__email=receiver, club=club ) if not existing_membership.exists(): existing_invite = MembershipInvite.objects.filter( club=club, email=receiver, active=True ) if not existing_invite.exists(): invite = MembershipInvite.objects.create( club=club, email=receiver, creator=None, role=role, title=role_mapping[role], auto=True, ) else: invite = existing_invite.first() if invite.role <= Membership.ROLE_OWNER: invite.send_owner_invite() else: invite.send_mail() elif action == "physical_fair": send_fair_email(club, receiver) elif action == "physical_postfair": send_fair_email(club, receiver, template="postfair") self.stdout.write(f"Sent {clubs_sent} email(s), {clubs_missing} missing club(s)")