diff --git a/app/forms.py b/app/forms.py
index f487645..8e3493e 100644
--- a/app/forms.py
+++ b/app/forms.py
@@ -116,3 +116,24 @@ class DeleteForm(FlaskForm):
class LoginForm(FlaskForm):
openid = StringField('openid', validators=[DataRequired()])
remember_me = BooleanField('remember_me', default=False)
+
+class AdminBeerEditForm(ModelForm, FlaskForm):
+ class Meta:
+ model = Beer
+
+ name = StringField('För- och efternamn', validators=[DataRequired()])
+ nickname = StringField('Kårnamn')
+ email = StringField('epost@din.com', validators=[Email()])
+ person_number = StringField(
+ 'Personnummer',
+ validators=[
+ DataRequired(),
+ Regexp(
+ "^[12]{1}[90]{1}[0-9]{6}-[0-9]{4}$",
+ message="Skriv personnummer på formatet ååååmmdd-xxxx"
+ ),
+ validate_age
+ ]
+ )
+ mobile_number = StringField('Mobilnummer', validators=[DataRequired()])
+ has_payed = BooleanField('Betalat')
diff --git a/app/mail_ohlreise.py b/app/mail_ohlreise.py
index 2a96ce4..7cff4cc 100644
--- a/app/mail_ohlreise.py
+++ b/app/mail_ohlreise.py
@@ -1,10 +1,16 @@
# -*- coding: utf-8 -*-
-from app import app
-import smtplib
+import os
+import json
+import base64
from email.mime.text import MIMEText
+from app import app
+from google.auth.transport.requests import Request
+from google.oauth2.credentials import Credentials
+from googleapiclient.discovery import build
+
+SCOPES = ['https://www.googleapis.com/auth/gmail.send']
-FROM_ADDRESS = 'info.brutalakademien'
SUBJECT = "Bokningskonfirmation"
BODY = """
Tack för din anmälan!
@@ -20,28 +26,79 @@
"""
-def send(to_address="andreas.cederstrom@gmail.com", price=1337,
- team_name="Aporna"):
+def send(to_address="andreas.cederstrom@gmail.com", price=1337, team_name="Aporna"):
bank = app.config['ÖHLREISE']['payment']['bank']
account_number = app.config['ÖHLREISE']['payment']['account_number']
last_payment_date = app.config['ÖHLREISE']['payment']['last_payment_date']
- mail = _build_mail(to_address, price, bank, account_number, last_payment_date, team_name)
- _do_send(mail)
+ subject, body = _build_mail_content(
+ price,
+ bank,
+ account_number,
+ last_payment_date,
+ team_name
+ )
+ return _do_send(to_address, subject, body)
-def _build_mail(to_address, price, bank, account_number, last_payment_date , team_name):
+
+def _build_mail_content(price, bank, account_number, last_payment_date, team_name):
body = BODY % (price, bank, account_number, last_payment_date, team_name)
- mail = MIMEText(body)
- mail['Subject'] = SUBJECT
- mail['From'] = FROM_ADDRESS
- mail['To'] = to_address
- return mail
-
-
-def _do_send(mail):
- server = smtplib.SMTP('smtp.gmail.com', 587)
- server.ehlo()
- server.starttls()
- server.login(app.config['GMAIL_USERNAME'], app.config['GMAIL_PASSWORD'])
- server.sendmail(mail['From'], mail['To'], mail.as_string())
- server.quit()
+ return SUBJECT, body
+
+
+def _load_credentials():
+ token_path = app.config['GOOGLE_OAUTH_TOKEN_FILE']
+
+ if not os.path.exists(token_path):
+ raise RuntimeError(
+ "Gmail OAuth token.json is missing. Connect Gmail via the admin OAuth route first."
+ )
+
+ with open(token_path, 'r') as f:
+ token_data = json.load(f)
+
+ creds = Credentials.from_authorized_user_info(token_data, SCOPES)
+
+ if creds.expired and creds.refresh_token:
+ creds.refresh(Request())
+ _save_credentials(creds)
+
+ if not creds.valid:
+ raise RuntimeError("Gmail OAuth credentials are invalid. Reconnect Gmail.")
+
+ return creds
+
+
+def _save_credentials(creds):
+ token_path = app.config['GOOGLE_OAUTH_TOKEN_FILE']
+ with open(token_path, 'w') as f:
+ f.write(creds.to_json())
+
+
+def _build_gmail_service():
+ creds = _load_credentials()
+ return build('gmail', 'v1', credentials=creds)
+
+
+def _create_message(to_address, subject, body):
+ from_address = app.config['GMAIL_FROM_ADDRESS']
+
+ message = MIMEText(body, _charset='utf-8')
+ message['To'] = to_address
+ message['From'] = from_address
+ message['Reply-To'] = from_address
+ message['Subject'] = subject
+
+ raw = base64.urlsafe_b64encode(message.as_bytes()).decode('utf-8')
+ return {'raw': raw}
+
+
+def _do_send(to_address, subject, body):
+ service = _build_gmail_service()
+ message = _create_message(to_address, subject, body)
+
+ service.users().messages().send(
+ userId='me',
+ body=message
+ ).execute()
+
\ No newline at end of file
diff --git a/app/templates/ohlreise/countdown.html b/app/templates/ohlreise/countdown.html
index 052521f..fad0900 100644
--- a/app/templates/ohlreise/countdown.html
+++ b/app/templates/ohlreise/countdown.html
@@ -2,7 +2,7 @@
{% block title %}Anmälan{% endblock %}
{% block content %}
@@ -18,7 +32,36 @@
Alla anmälda bussresenärer
Totalt har vi {{ beer|length }} anmälda bussresenärer.
+
+ {% if is_admin %}
+
+ Antal som har betalat: {{ beer|selectattr("has_payed")|list|length }}
+ Antal som inte har betalat: {{ beer|rejectattr("has_payed")|list|length }}
+
+
+
+
+
+
+
+
+ Ladda ner bussresenärer till CSV
+
+
+
+
+ {% endif %}
+
@@ -28,26 +71,31 @@ Alla anmälda bussresenärer
Kårnamn |
{% if is_admin %}
Personnummer |
- Allergier |
Email |
Har betalat |
{% endif %}
- {% for beer in beer %}
+ {% for member in beer %}
- | {{ beer.name }} |
- {{ beer.nickname }} |
+ {{ member.name }} |
+ {{ member.nickname }} |
{% if is_admin %}
- {{ beer.person_number }} |
- {{ beer.allergies }} |
- {{ beer.email }} |
- {{ 'Ja' if beer.has_payed else 'Nej' }} |
- Edit |
-
+ {{ member.person_number }} |
+ {{ member.email }} |
+ {{ 'Ja' if member.has_payed else 'Nej' }} |
+
+ Justera
+ |
+
+
+ |
{% endif %}
{% endfor %}
diff --git a/app/templates/ohlreise/submit.html b/app/templates/ohlreise/submit.html
index 2c57f9b..b61b874 100644
--- a/app/templates/ohlreise/submit.html
+++ b/app/templates/ohlreise/submit.html
@@ -3,24 +3,27 @@
{% block content %}
@@ -30,64 +33,83 @@
Anmälan
Biljetterna har släppts!
- {% for ticket in config.ÖHLREISE.ticket_types %}
-
{{ticket.name}} - {{ticket.price}} Riksdaler
-
Biljetter kvar: {{remaining_tickets_for_type[loop.index0]}} st
+
+ {% for ticket in config.ÖHLREISE.ticket_types %}
+
{{ ticket.name }} - {{ ticket.price }} Riksdaler
+
Biljetter kvar: {{ remaining_tickets_for_type[loop.index0] }} st
Och i denna fantastiska biljett ingår då:
-
- {{ticket.description}}
+
+ {{ ticket.description }}
- {% endfor %}
+ {% endfor %}
-{% endblock %}
+{% endblock %}
\ No newline at end of file
diff --git a/app/views/ohlreise_views.py b/app/views/ohlreise_views.py
index fe02c53..9f233ca 100644
--- a/app/views/ohlreise_views.py
+++ b/app/views/ohlreise_views.py
@@ -1,8 +1,12 @@
# -*- coding: utf-8 -*-
-from flask import jsonify, render_template, redirect, url_for, request
+import csv
+import io
+from flask import Response
+
+from flask import render_template, redirect, url_for, request, flash
from app import app, db, logic, mail_ohlreise
from app.decorators import login_required
-from app.forms import BeerForm, DeleteForm
+from app.forms import BeerForm, DeleteForm, AdminBeerEditForm
from app.models import Beer
@@ -10,11 +14,16 @@
def beer():
return render_template("ohlreise/info.html")
-@app.route('/ohlreise/members', methods=['GET', 'POST'])
+@app.route('/ohlreise/members', methods=['GET'])
def ohlreise_members():
- form = DeleteForm()
- beer = Beer.query.order_by(Beer.name).all()
- return render_template("ohlreise/members.html", beer=beer, form=form)
+ beers, has_payed_arg = _get_filtered_beers()
+ beers = beers.order_by(Beer.name).all()
+
+ return render_template(
+ "ohlreise/members.html",
+ beer=beers,
+ has_payed=has_payed_arg
+ )
@app.route('/ohlreise/member/', methods=['GET', 'POST'])
@login_required
@@ -23,44 +32,68 @@ def ohlreise_edit_member(id):
assert member
if request.method == 'POST':
- form = BeerForm(request.form)
- form.populate_obj(member)
- db.session.add(member)
- db.session.commit()
- return redirect(url_for('ohlreise_members', _anchor=member.id))
- else:
- form = BeerForm(obj=member)
- return render_template("ohlreise/edit_member.html", form=form,
- title='Editera medlem')
-
-@app.route('/ohlreise/members/add-member', methods=['POST'])
+ form = AdminBeerEditForm(request.form)
+ form.Meta.csrf = False
+
+ if form.validate():
+ form.populate_obj(member)
+ db.session.add(member)
+ db.session.commit()
+ return redirect(url_for('ohlreise_members', _anchor=str(member.id)))
+
+ app.logger.warning("AdminBeerEditForm errors: %r", form.errors)
+ return render_template(
+ "ohlreise/edit_member.html",
+ form=form,
+ title='Editera medlem'
+ )
+
+ form = AdminBeerEditForm(obj=member)
+ return render_template(
+ "ohlreise/edit_member.html",
+ form=form,
+ title='Editera medlem'
+ )
+
+@app.route('/ohlreise/members/add-member', methods=['GET', 'POST'])
@login_required
def ohlreise_add_member():
+ if request.method == 'POST':
+ form = BeerForm(request.form)
- if request.method == 'POST':
+ if form.validate():
member = Beer()
- form = BeerForm(request.form)
form.populate_obj(member)
db.session.add(member)
db.session.commit()
return redirect(url_for('ohlreise_members'))
- else:
- form = BeerForm()
- return render_template("ohlreise/edit_member.html", form=form,
- title='Lägg till medlem')
+
+ return render_template(
+ "ohlreise/edit_member.html",
+ form=form,
+ title='Lägg till medlem'
+ )
+
+ form = BeerForm()
+ return render_template(
+ "ohlreise/edit_member.html",
+ form=form,
+ title='Lägg till medlem'
+ )
@app.route('/ohlreise/member//delete', methods=['POST'])
@login_required
def ohlreise_delete_member(id):
- if request.method == 'POST':
- beer = Beer.query.get(id)
- if beer:
- db.session.delete(beer)
- db.session.commit()
- return redirect(url_for('ohlreise_members')) # Redirect to the appropriate route
- else:
- flash('Gick inte att hitta resenäreren.', 'error')
- return render_template('ohlreise_members', form = form)
+ member = Beer.query.get(id)
+
+ if not member:
+ flash('Gick inte att hitta resenären.', 'error')
+ return redirect(url_for('ohlreise_members'))
+
+ db.session.delete(member)
+ db.session.commit()
+ flash('Resenären togs bort.', 'success')
+ return redirect(url_for('ohlreise_members'))
@app.route('/ohlreise/submit', methods=['GET', 'POST'])
def ohlreise_submit():
@@ -68,39 +101,123 @@ def ohlreise_submit():
milliseconds = logic.get_milliseconds_until_ohlreise_submit_opens()
return render_template("ohlreise/countdown.html", milliseconds=milliseconds)
- remaining_tickets_for_type = [logic.get_number_of_tickets_for_this_type_left_beer(ind) for ind, ticket in enumerate(app.config['ÖHLREISE']['ticket_types'])]
+ remaining_tickets_for_type = [
+ logic.get_number_of_tickets_for_this_type_left_beer(ind)
+ for ind, ticket in enumerate(app.config['ÖHLREISE']['ticket_types'])
+ ]
if sum(remaining_tickets_for_type) <= 0 or logic.has_submit_ohlreise_closed():
return render_template("ohlreise/submit_temp_closed.html")
form = BeerForm(request.form)
- error_message = "None"
form.Meta.csrf = False
+ error_message = "None"
if request.method == 'POST':
- person_numbers = [form.data['person_number']]
+ person_number = form.data.get('person_number')
if form.validate():
- duplicate_person_number = any(Beer.query.filter_by(person_number=pn).first() for pn in person_numbers if pn)
+ duplicate_person_number = (
+ Beer.query.filter_by(person_number=person_number).first()
+ if person_number else None
+ )
if duplicate_person_number:
- form.errors['person_number'] = ['Det finns redan en användare med detta personnummer.']
+ form.person_number.errors = list(form.person_number.errors) + [
+ 'Det finns redan en användare med detta personnummer.'
+ ]
error_message = "Det finns redan en användare med detta personnummer."
-
- if not duplicate_person_number:
+ else:
beer = Beer()
form.populate_obj(beer)
db.session.add(beer)
db.session.commit()
- mail_ohlreise.send(beer.email, beer.price, beer.name)
+
+ try:
+ mail_ohlreise.send(beer.email, beer.price, beer.name)
+ except Exception:
+ app.logger.exception(
+ "Failed to send Ohlreise confirmation email"
+ )
+
return render_template("ohlreise/confirmation.html", beer=beer)
+
else:
- print("Form validation errors: ", form.errors)
+ app.logger.warning("Beer form validation errors: %r", form.errors)
error_message = "Gör om, gör rätt."
- else:
- remaining_tickets_for_type = [logic.get_number_of_tickets_for_this_type_left_beer(ind) for ind, ticket in enumerate(app.config['ÖHLREISE']['ticket_types'])]
- return render_template("ohlreise/submit.html", remaining_tickets_for_type=remaining_tickets_for_type, form=form, error_message=error_message)
- # Handle the GET case
- remaining_tickets_for_type = [logic.get_number_of_tickets_for_this_type_left_beer(ind) for ind, ticket in enumerate(app.config['ÖHLREISE']['ticket_types'])]
- return render_template("ohlreise/submit.html", remaining_tickets_for_type=remaining_tickets_for_type, form=form)
+ return render_template(
+ "ohlreise/submit.html",
+ remaining_tickets_for_type=remaining_tickets_for_type,
+ form=form,
+ error_message=error_message
+ )
+
+ return render_template(
+ "ohlreise/submit.html",
+ remaining_tickets_for_type=remaining_tickets_for_type,
+ form=form,
+ error_message=error_message
+ )
+
+@app.route('/ohlreise/members/delete', methods=['POST'])
+@login_required
+def ohlreise_remove_all_members():
+ members = Beer.query.all()
+
+ if not members:
+ return redirect(url_for('ohlreise_members'))
+
+ for member in members:
+ db.session.delete(member)
+
+ db.session.commit()
+ return redirect(url_for('ohlreise_members'))
+
+def _get_filtered_beers():
+ beers = db.session.query(Beer)
+ has_payed_arg = request.args.get('has_payed')
+
+ if has_payed_arg is not None and has_payed_arg != '-':
+ has_payed = has_payed_arg == 'True'
+ beers = beers.filter_by(has_payed=has_payed)
+
+ return beers, has_payed_arg
+
+
+@app.route('/ohlreise/export/members/csv')
+@login_required
+def ohlreise_export_csv():
+ beers = db.session.query(Beer).order_by(Beer.name).all()
+ output = io.StringIO()
+ writer = csv.writer(output)
+
+ writer.writerow([
+ 'Namn',
+ 'Kårnamn',
+ 'Personnummer',
+ 'E-post',
+ 'Mobilnummer',
+ 'Betalat',
+ ])
+
+ for beer in beers:
+ writer.writerow([
+ beer.name,
+ beer.nickname,
+ beer.person_number,
+ beer.email,
+ '%s' % beer.mobile_number if beer.mobile_number else '',
+ 'Ja' if beer.has_payed else 'Nej',
+ ])
+
+ csv_data = output.getvalue()
+ output.close()
+
+ return Response(
+ csv_data,
+ mimetype='text/csv',
+ headers={
+ 'Content-Disposition': 'attachment; filename=ohlreise_members_export.csv'
+ }
+ )