diff --git a/.gitignore b/.gitignore index 01d8029..a1fb918 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # Created by https://www.toptal.com/developers/gitignore/api/python,flask # Edit at https://www.toptal.com/developers/gitignore?templates=python,flask +uploads/ ### Flask ### instance/* !instance/.gitignore diff --git a/Makefile b/Makefile index 35c6a33..12a0d43 100644 --- a/Makefile +++ b/Makefile @@ -32,7 +32,7 @@ daemon: @echo "--- STARTING UWSGI DAEMON ---" @echo "" @echo "" - source .venv/bin/activate && flask run + source .venv/bin/activate && FLASK_DEBUG=True flask run @echo "" @echo "" @echo "--- STARTING UWSGI DAEMON ---" diff --git a/acmsite/__init__.py b/acmsite/__init__.py index 5794149..d7a7a3c 100644 --- a/acmsite/__init__.py +++ b/acmsite/__init__.py @@ -1,4 +1,5 @@ from flask import Flask +import os from flask_sqlalchemy import SQLAlchemy from flask_migrate import Migrate from flask_login import LoginManager @@ -42,6 +43,12 @@ def create_app(): ) from .models import User + + # Ensure that uploads directory exists + try: + os.mkdir(app.config["UPLOAD_FOLDER"]) + except FileExistsError: + pass from .main import bp as main_bp app.register_blueprint(main_bp) diff --git a/acmsite/admin/__init__.py b/acmsite/admin/__init__.py index 81331fc..3963c0c 100644 --- a/acmsite/admin/__init__.py +++ b/acmsite/admin/__init__.py @@ -1,12 +1,17 @@ -from flask import Blueprint, flash, redirect, render_template, request, send_file, url_for +from operator import pos +import os +from flask import Blueprint, current_app, flash, redirect, render_template, request, send_file, url_for import ulid import datetime from flask_login import current_user, login_required +from io import BytesIO +from PIL import Image +import base64 -from acmsite.models import Link, User, Event +from acmsite.models import Link, Officer, User, Event from acmsite import models -from .forms import EventForm, LinkForm +from .forms import EventForm, LinkForm, OfficerForm from acmsite import db @@ -29,7 +34,10 @@ def users(): user_list = User.query.all() - return render_template("admin/users.html", u_list=user_list) + position_form = OfficerForm(request.form) + + return render_template("admin/users.html", u_list=user_list, + form=position_form) @bp.route("/users.csv") @login_required @@ -44,7 +52,6 @@ def users_csv(): return send_file('./tmp/members.csv') - @bp.route("/events") @login_required def events(): @@ -184,3 +191,107 @@ def update_create_link(id): db.session.commit() return redirect(url_for("admin.links")) + +def error_json(message): + return {"status": "error", "message": message} + +def success_json(): + return {"status": "success"} + +@bp.route("/officer/") +@login_required +def officer_positions(user_id): + if not current_user.is_admin: + flash("Unauthorized") + return redirect(url_for("dashboard.home")) + + form = OfficerForm(request.form) + + position_list = Officer.query.filter_by(user_id=user_id).order_by(Officer.term_end).all() + + return render_template("admin/officers.html", form=form, + position_list=position_list, user_id=user_id) + +@bp.route("/officer/get/") +@login_required +def get_position(pos_id): + if not current_user.is_admin: + return error_json("Unauthorized") + + pos = Officer.query.filter_by(id=pos_id).first() + + if pos is None: + return error_json("Invalid ID") + + return pos.create_json() + + +@bp.route("/officer/new/", methods=["POST"]) +@login_required +def create_officer(user_id): + if not current_user.is_admin: + error_json("Unauthorized") + + form = OfficerForm(request.form) + + if form.validate_on_submit: + position = request.form.get("position") + term_end = request.form.get("term_end") + term_start = request.form.get("term_start") + + position = Officer( + id=ulid.ulid(), + user_id=user_id, + position=position, + term_start=term_start, + term_end=term_end) + + db.session.add(position) + db.session.commit() + + return redirect(url_for("admin.officer_positions", user_id=user_id)) + +@bp.route("/officer/update//", methods=["POST"]) +@login_required +def update_officer(user_id, officer_id): + if not current_user.is_admin: + flash("Unauthorized") + return redirect(url_for('dashboard.home')) + + form = OfficerForm(request.form) + + if form.validate_on_submit: + officer = Officer.query.filter_by(id=officer_id).first() + if officer is None: + flash("Invalid ID") + return redirect(url_for('admin.officer_positions', user_id=user_id)) + + officer.position = request.form.get("position") + officer.term_start = request.form.get("term_start") + officer.term_end = request.form.get("term_end") + + db.session.commit() + + return redirect(url_for('admin.officer_positions', user_id=user_id)) + +@bp.route("/officer/photo") +@login_required +def upload_photo(): + if not current_user.is_admin: + return error_json("Unauthorized") + + return render_template("admin/officer_photo.html") + +@bp.route("/officer/photo/upload", methods=["POST"]) +@login_required +def upload_photo_post(): + if not current_user.is_admin: + return error_json("Unauthorized") + + img_path = os.path.join(current_app.config["UPLOAD_FOLDER"], f"{current_user.username()}.png") + b64_string = request.data.decode() + b64_string += '=' * (len(b64_string) % 4) + + im = Image.open(BytesIO(base64.b64decode(b64_string.split(',')[1]))) + im.save(img_path, format="PNG") + return success_json() diff --git a/acmsite/admin/forms.py b/acmsite/admin/forms.py index 75ed1c7..c901639 100644 --- a/acmsite/admin/forms.py +++ b/acmsite/admin/forms.py @@ -1,5 +1,5 @@ from flask_wtf import FlaskForm -from wtforms import DateTimeField, DateField, StringField, TextAreaField, TimeField +from wtforms import DateTimeField, DateField, SelectField, StringField, TextAreaField, TimeField from wtforms.validators import DataRequired class EventForm(FlaskForm): @@ -14,3 +14,11 @@ class EventForm(FlaskForm): class LinkForm(FlaskForm): slug = StringField("Slug", validators=[DataRequired()]) destination = StringField("Destination", validators=[DataRequired()]) + +class OfficerForm(FlaskForm): + position = SelectField("Position", choices=["President", "Vice President", + "Treasurer", "Secretary", "PR Chair", "Hackathon Manager 1", + "Hackathon Manager 2", "System Administrator"], + validators=[DataRequired()]) + term_start = DateField("Term Start", validators=[DataRequired()]) + term_end = DateField("Term End", validators=[DataRequired()]) diff --git a/acmsite/models.py b/acmsite/models.py index a66b15c..b3e6244 100644 --- a/acmsite/models.py +++ b/acmsite/models.py @@ -17,6 +17,9 @@ class User(db.Model, UserMixin): active = Column(Boolean, nullable=False, default=True) is_admin = Column(Boolean, nullable=False, default=False) + def username(self): + return self.email.split("@")[0] + def create_acm_csv(user_list): with open('acmsite/tmp/members.csv', 'w') as members_csv: header = ['last', 'first', 'email'] @@ -43,6 +46,15 @@ class Officer(db.Model): term_end = Column(Date, nullable=True) position = Column(String, nullable=False) + def create_json(self): + return { + "id": self.id, + "user_id": self.user_id, + "term_start": self.term_start, + "term_end": self.term_end, + "position": self.position + } + class PwResetRequest(db.Model): id = Column(String, primary_key=True) user_id = Column(String, ForeignKey('acm_users.id'), nullable=False) diff --git a/acmsite/templates/admin/officer_photo.html b/acmsite/templates/admin/officer_photo.html new file mode 100644 index 0000000..ea3cdf6 --- /dev/null +++ b/acmsite/templates/admin/officer_photo.html @@ -0,0 +1,103 @@ +{% extends "admin/admin-layout.html" %} + +{% block app_content %} + + +

Update Officer Photo

+ +
+ +
+ +
+
+
+ +
+ + +
+ +
+
+ + +
+ + + + Download +
+ + + +{% endblock %} diff --git a/acmsite/templates/admin/officers.html b/acmsite/templates/admin/officers.html new file mode 100644 index 0000000..0c4123e --- /dev/null +++ b/acmsite/templates/admin/officers.html @@ -0,0 +1,173 @@ +{% extends "admin/admin-layout.html" %} + +{% block app_content %} + +

Officer Positions for {{ current_user.first_name}} {{ current_user.last_name + }}

+

Update Photo: Here + + + + + + + + + + + {% for o in position_list %} + + + + + + + {% endfor %} + +
PositionTerm StartTerm End
{{ o.position }}{{ o.term_start }}{{ o.term_end }} + +
+ +

+ + + + + + + + + + + + + +{% endblock %} diff --git a/acmsite/templates/admin/users.html b/acmsite/templates/admin/users.html index bd15541..0917009 100644 --- a/acmsite/templates/admin/users.html +++ b/acmsite/templates/admin/users.html @@ -29,10 +29,14 @@ class="caret"> @@ -40,7 +44,7 @@ {% endfor %} - - + + {% endblock app_content %} diff --git a/acmsite/tmp/members.csv b/acmsite/tmp/members.csv new file mode 100644 index 0000000..19ce160 --- /dev/null +++ b/acmsite/tmp/members.csv @@ -0,0 +1 @@ +Salter,Cara,csalter2@wpi.edu diff --git a/requirements.txt b/requirements.txt index dd6257e..8d334e4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -27,3 +27,4 @@ urllib3==2.2.1 Werkzeug==2.3.7 WTForms==3.1.2 flask_wtf +pillow