diff --git a/app.py b/app.py index 5b5f6b2..b27881f 100755 --- a/app.py +++ b/app.py @@ -26,6 +26,16 @@ migrate = Migrate() db.init_app(app) migrate.init_app(app, db) +# Helper Methods + + +def object_as_dict(obj): + return {c.key: getattr(obj, c.key) + for c in inspect(obj).mapper.column_attrs} + +# Model Forms + + BaseModelForm = model_form_factory(FlaskForm) @@ -35,10 +45,19 @@ class ModelForm(BaseModelForm): return db.session -class Admin(db.Model): +class User(db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String, unique=True, nullable=False) + email = db.Column(db.String, unique=True, nullable=False) + name = db.Column(db.String, unique=True, nullable=False) password = db.Column(db.String, nullable=False) + + institution = db.Column(db.String, unique=True, nullable=False) + position = db.Column(db.String, unique=True, nullable=False) + + admin = db.Column(db.Boolean) + + # for type annotations query: db.Query @classmethod @@ -47,31 +66,28 @@ class Admin(db.Model): @classmethod def authenticate(cls, username: str, pw: str): - user = Admin.query.filter_by(username=username).one_or_none() + user = User.query.filter_by(username=username).one_or_none() if user and bcrypt.checkpw(pw, user.password): - session['admin'] = user.username + session['user'] = user.username + if user.admin: + session['admin'] = user.username return user else: return None @classmethod - def exists(cls): - user = Admin.query.first() + def admin_exists(cls): + user = User.query.filter_by(admin=True).first() return True if user else False @classmethod - def authorize(cls): - if "admin" not in session: - return redirect(url_for("admin_login")) + def authorize_or_redirect(cls, admin=True): + if (admin and "admin" not in session) or "user" not in session: + return redirect(url_for("accounts_create")) else: return None -def object_as_dict(obj): - return {c.key: getattr(obj, c.key) - for c in inspect(obj).mapper.column_attrs} - - class Chemical(db.Model): query: db.Query id = db.Column(db.Integer, primary_key=True) @@ -126,15 +142,15 @@ def handler_403(msg): # Admin routes @app.route('/admin') def admin_root(): - if login := Admin.authorize(): + if login := User.authorize_or_redirect(): return login return render_template("admin.html", user=session.get("admin")) -@app.route('/admin/create', methods=['GET', 'POST']) -def admin_create(): - if Admin.exists(): - if login := Admin.authorize(): +@app.route('/accounts/create', methods=['GET', 'POST']) +def accounts_create(): + if User.admin_exists(): + if login := User.authorize_or_redirect(): return login if request.method == "GET": return render_template("register.html") @@ -143,21 +159,51 @@ def admin_create(): 'username'), request.form.get('password') if username is None or pw is None: return render_template("register.html", fail="Invalid Input.") - elif db.session.execute(db.select(Admin).filter_by(username=username)).fetchone(): + elif db.session.execute(db.select(User).filter_by(username=username)).fetchone(): return render_template("register.html", fail="Username already exists.") else: - db.session.add( - Admin(username=username, password=Admin.generate_password(pw))) + # because the IDE complains about type mismatches + form = {} | request.form + form['password'] = User.generate_password(pw) + form['admin'] = (True if form['admin'] == 'y' else False) + form.pop('reconfirm') + user = User(**form) + db.session.add(user) db.session.commit() return render_template("register.html", success=True) -@app.route('/admin/login', methods=['GET', 'POST']) -def admin_login(): +@app.route('/accounts/edit', methods=['GET', 'POST']) +def accounts_edit(): + if login := User.authorize_or_redirect(admin=False): + return login + user = User.query.filter_by(username=session.get('user')).one_or_404() + if request.method == "GET": + return render_template("account_edit.html", user=object_as_dict(user)) + else: + dct = object_as_dict(user) + # update all of the changes + for key in request.form: + if key in dct: + setattr(user, key, request.form[key]) + db.session.commit() + return render_template("account_edit.html", user=object_as_dict(user), success=True) + + +@app.route('/accounts/view/') +def accounts_edit_admin(id): + if login := User.authorize_or_redirect(admin=True): + return login + user = User.query.filter_by(id=id).one_or_404() + return render_template("account_view.html", user=object_as_dict(user)) + + +@app.route('/accounts/login', methods=['GET', 'POST']) +def login(): if request.method == "POST": username, pw = request.form.get( 'username', ''), request.form.get('password', '') - if Admin.authenticate(username, pw): + if User.authenticate(username, pw): return render_template("login.html", success=True) else: return render_template("login.html", fail="Could not authenticate.") @@ -165,19 +211,21 @@ def admin_login(): return render_template("login.html") -@app.route('/admin/logout', methods=['GET']) -def admin_logout(): +@app.route('/accounts/logout', methods=['GET']) +def logout(): if "admin" in session: session.pop('admin') + if "user" in session: + session.pop('user') return redirect(url_for('home')) @app.route("/") def home(): - if Admin.exists(): + if User.admin_exists(): return render_template("index.html") else: - return redirect(url_for("admin_create")) + return redirect(url_for("accounts_create")) # Routes for CRUD operations on chemicals @@ -318,9 +366,10 @@ def batch_add_request(): return render_template("batchadd.html") +# regular users can batch search. @app.route("/chemical/batch", methods=["GET", "POST"]) def batch_query_request(): - if not session.get('admin'): + if not session.get('user'): abort(403) if request.method == "POST": if "input" not in request.files or request.files["input"].filename == '': diff --git a/migrations/versions/282564545160_changed_user_schema.py b/migrations/versions/282564545160_changed_user_schema.py new file mode 100644 index 0000000..4219b8b --- /dev/null +++ b/migrations/versions/282564545160_changed_user_schema.py @@ -0,0 +1,51 @@ +"""changed user schema + +Revision ID: 282564545160 +Revises: 1a0a82192181 +Create Date: 2023-05-20 16:07:52.967576 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '282564545160' +down_revision = '1a0a82192181' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('user', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('username', sa.String(), nullable=False), + sa.Column('email', sa.String(), nullable=False), + sa.Column('name', sa.String(), nullable=False), + sa.Column('password', sa.String(), nullable=False), + sa.Column('institution', sa.String(), nullable=False), + sa.Column('position', sa.String(), nullable=False), + sa.Column('admin', sa.Boolean(), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('email'), + sa.UniqueConstraint('institution'), + sa.UniqueConstraint('name'), + sa.UniqueConstraint('position'), + sa.UniqueConstraint('username') + ) + op.drop_table('admin') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('admin', + sa.Column('id', sa.INTEGER(), nullable=False), + sa.Column('username', sa.VARCHAR(), nullable=False), + sa.Column('password', sa.VARCHAR(), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('username') + ) + op.drop_table('user') + # ### end Alembic commands ### diff --git a/templates/account_edit.html b/templates/account_edit.html new file mode 100644 index 0000000..02d379c --- /dev/null +++ b/templates/account_edit.html @@ -0,0 +1,51 @@ +{% extends "base.html" %} +{% block content %} +

Edit Account Profile

+ + + + + + + + + + + + + +
Id{{user.id}}
Username{{user.username}}
Admin{% if user.admin %} Yes {% else %} False {% endif %}
+
+ + + +
+ + + +
+ + + +
+ + + +
+ +
+ +
+ {% if success %} +

Edits Successful!

+ {% elif fail %} +

Edits Failed. Please try again. {{fail}}

+ {% endif %} +{% endblock %} diff --git a/templates/account_view.html b/templates/account_view.html new file mode 100644 index 0000000..e3d2d73 --- /dev/null +++ b/templates/account_view.html @@ -0,0 +1,34 @@ +{% extends "base.html" %} +{% block content %} +

View User

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Id{{user.id}}
Username{{user.username | safe}}
Admin{% if user.admin %} Yes {% else %} False {% endif %}
Name{{user.name | safe}}
Email{{user.email | safe}}
Institution{{user.institution | safe}}
Position{{user.position | safe}}
+{% endblock %} diff --git a/templates/base.html b/templates/base.html index 21219cf..4028ce4 100644 --- a/templates/base.html +++ b/templates/base.html @@ -18,9 +18,9 @@ Search {% if session.admin %} Admin - Logout + Logout {% else %} - Login + Login {% endif %} diff --git a/templates/register.html b/templates/register.html index bec6059..f6a0565 100644 --- a/templates/register.html +++ b/templates/register.html @@ -28,6 +28,27 @@
+ + + +
+ + + +
+ + + +
+ + + +
+ + + +
+
@@ -41,4 +62,4 @@ {% elif fail %}

Login Failed. Please try again. {{fail}}

{% endif %} -{% endblock %} \ No newline at end of file +{% endblock %}