def check_dates(app): """Check existing deadlines and reminders in the database, executes any corresponding action if one has passed""" now = datetime.utcnow() with app.app_context(): deadline = db.session.query(Deadline).first() # if deadline exists, check if already passed if deadline is not None and not deadline.is_valid( now) and not deadline.executed: print(log_header('AUTOMATED EMAILS')) send_all_prof_emails() deadline.executed = True db.session.commit() else: reminders = db.session.query(Reminder).order_by( Reminder.datetime).all() r_sent = False for reminder in reminders: if not reminder.is_valid(now): # if a reminder has been sent already, other reminders that may have passed within the same interval should not trigger another reminder (and should be also removed) db.session.delete(reminder) db.session.commit() # if no emails have been sent yet if not r_sent: r_sent = True print(log_header('AUTOMATED EMAILS')) send_all_student_emails(True)
def send_all_student_emails(reminder=False): """This function sends emails with individualized links to *all* students in the database to take the survey""" sender = current_app.config['MAIL_ADDRESS'] deadline = Deadline.query.first() if deadline is not None and deadline.is_valid(): msgs = list() threads = list() current_app_context = current_app._get_current_object() for student in Student.query.all(): # skip this student if this is a reminder email and the student has already submitted their survey if not reminder or not student.survey_submitted: try: # get_survey_link() has to be done here because request context cannot be pushed t = Thread(target=lambda m, app_con, stu, sub, num, lin, dea, rem: \ m.insert(0, create_student_msg(app_con, stu, sub, num, lin, dea, rem)), \ args=(msgs, current_app_context, student, student.section.subject, \ student.section.course_num, student.get_survey_link(), deadline, reminder)) threads.append(t) t.start() except Exception as e: print('ERROR: Failed to create message for {} {} - {}'.format(student.s_id, student.email, e)) # block until all threads have finished for t in threads: t.join() # send emails if reminder: print(log_header('REMINDER EMAILS')) else: print(log_header('STUDENT EMAILS')) Thread(target=send_async_emails, args=(current_app_context,), kwargs={'msgs': msgs}).start() print('SENT {} EMAILS'.format(len(msgs))) elif deadline is None: print('ERROR: Student emails cannot be sent without a deadline') elif not deadline.is_valid(): print('ERROR: Student emails cannot be sent without a valid deadline')
def override(): """Allow developers to wipe database if needed""" if User.query.count() == 0: return redirect(url_for('main.login')) form = OverrideForm() if form.validate_on_submit(): if form.password.data == current_app.config['OVERRIDE_PW']: print(log_header('OVERRIDE - {}'.format(form.dev_id.data))) wipeAdmin() flash('Admin data erased') wipeSurveyData() flash('Survey data erased') return redirect(url_for('main.register')) else: flash('Incorrect password') return render_template('override.html', title='DEVELOPER OVERRIDE', form=form)
def send_all_prof_emails(): """This function sends emails with individualized statistics to *all* professors in the database (represented as sections)""" current_app_context = current_app._get_current_object() msgs = list() threads = list() pdfs = list() for section in Section.query.all(): t = None # create section data results_count = section.results.count() students_count = section.students.count() means = section.get_means() stds = section.get_stds() frq_responses = section.get_frq_responses() try: if results_count > 0: pdf = PDFPlotter(section) t = Thread(target=lambda m, app_con, s, r_c, s_c, me, st, f_r, f: \ m.insert(0, create_prof_emails(app_con, s, r_c, s_c, me, st, f_r, f)), \ args=(msgs, current_app_context, section, results_count, students_count, means, stds, frq_responses, pdf.file)) pdfs.append(pdf) else: # don't create PDF if no results were submitted t = Thread(target=lambda m, app_con, s, r_c, s_c, me, st, f_r: \ m.insert(0, create_prof_emails(app_con, s, r_c, s_c, me, st, f_r)), \ args=(msgs, current_app_context, section, results_count, students_count, means, stds, frq_responses)) threads.append(t) t.start() except Exception as e: print('ERROR: Failed to create message for {} {} - {}'.format(section.course_id, section.prof_email, e)) # block until all threads are finished for t in threads: t.join() # delete pdfs after emails are sent for pdf in pdfs: pdf.deleteFile() # send emails print(log_header('PROFESSOR EMAILS')) Thread(target=send_async_emails, args=(current_app_context,), kwargs={'msgs': msgs}).start() print('SENT {} EMAILS'.format(len(msgs)))
def parse_roster(form_roster_data): """Use uploaded roster to create corresponding database objects - expects a wtforms.fields.FileField object (i.e. form.<uploaded_file>.data)""" # save file locally filename = secure_filename(form_roster_data.filename) form_roster_data.save(filename) csv_filepath = os.path.join('documents', filename) ext = filename[filename.rindex('.'):] # if Excel file, convert to CSV and remove Excel version if ext == '.xlsx' or ext == '.xls': wb = xlrd.open_workbook(filename) sheet = wb.sheet_by_index(0) # convert with open(csv_filepath, 'w', newline='') as f_roster: csv_roster = csv.writer(f_roster, delimiter=',') for row_num in range(sheet.nrows): csv_roster.writerow(sheet.row_values(row_num)) # remove Excel file os.remove(filename) # if already CSV file, simply move file elif ext == '.csv': os.rename(filename, csv_filepath) # indices as expected by the given SCU roster template C_ID_I = 1 SUBJECT_I = 2 COURSE_I = 3 PROF_NAME_I = 6 PROF_EMAIL_I = 7 S_ID_I = 8 STUDENT_EMAIL_I = 9 # expected headers: C_ID_S = 'Class Nbr' SUBJECT_S = 'Subject' COURSE_S = 'Catalog' PROF_NAME_S = 'Instructor' PROF_EMAIL_S = 'Instructor Email' S_ID_S = 'Student ID' STUDENT_EMAIL_S = 'Student Email' # counters section_count = 0 student_count = 0 with open(csv_filepath, 'r', newline='') as f_roster: rows = csv.reader(f_roster, delimiter=',') header_row = next(rows) # ensure important columns have correct headers if not (header_row[C_ID_I].lower() == C_ID_S.lower() and \ header_row[SUBJECT_I].lower() == SUBJECT_S.lower() and \ header_row[COURSE_I].lower() == COURSE_S.lower() and \ header_row[PROF_NAME_I].lower() == PROF_NAME_S.lower() and \ header_row[PROF_EMAIL_I].lower() == PROF_EMAIL_S.lower() and \ header_row[S_ID_I].lower() == S_ID_S.lower() and \ header_row[STUDENT_EMAIL_I].lower() == STUDENT_EMAIL_S.lower()): # remove and return false (therefore exiting early) if roster does not have expected headers print('COMPARED {} vs. {}'.format(header_row[C_ID_I].lower(), C_ID_S.lower())) print('COMPARED {} vs. {}'.format(header_row[SUBJECT_I].lower(), SUBJECT_S.lower())) print('COMPARED {} vs. {}'.format(header_row[COURSE_I].lower(), COURSE_S.lower())) print('COMPARED {} vs. {}'.format(header_row[PROF_NAME_I].lower(), PROF_NAME_S.lower())) print('COMPARED {} vs. {}'.format(header_row[PROF_EMAIL_I].lower(), PROF_EMAIL_S.lower())) print('COMPARED {} vs. {}'.format(header_row[S_ID_I].lower(), S_ID_S.lower())) print('COMPARED {} vs. {}'.format( header_row[STUDENT_EMAIL_I].lower(), STUDENT_EMAIL_S.lower())) os.remove(csv_filepath) return False prev_c_id = -1 print(log_header('ROSTER UPLOADED - PARSING')) student_threads = list() for row in rows: # add sections - addSection() avoids repeats subject = row[SUBJECT_I] course_num = row[COURSE_I] c_id = removeZeroes(row[C_ID_I]) prof_name = row[PROF_NAME_I] prof_email = row[PROF_EMAIL_I] # only attempt to add a new section if moved onto new valid section ID with properly formatted SCU email if prev_c_id != c_id and isValidEmail(prof_email) and isID(c_id): addSection(subject, course_num, c_id, prof_name, prof_email) section_count += 1 prev_c_id = c_id # make one student per row s_id = removeZeroes(row[S_ID_I]) stud_email = row[STUDENT_EMAIL_I] if isValidEmail(stud_email) and isID(s_id) and prev_c_id == c_id: # only add student if has correctly formatted SCU email # prev_c_id only matches c_id if section with c_id was created - if not, don't add student because his/her section DNE in DB t = Thread(target=addStudent, args=(current_app._get_current_object(), s_id, c_id, stud_email)) student_threads.append(t) t.start() # make sure all adding threads finish before exiting (because emailing is called next and it might be called before all addStudent threads finish) for t in student_threads: t.join() student_count += 1 os.remove(csv_filepath) print('ADDED {} SECTIONS AND {} STUDENTS'.format(section_count, student_count)) return True