diff --git a/alembic/versions/74e63e0c8e89_add_name_and_year_to_users_table.py b/alembic/versions/74e63e0c8e89_add_name_and_year_to_users_table.py new file mode 100644 index 0000000..21ce25b --- /dev/null +++ b/alembic/versions/74e63e0c8e89_add_name_and_year_to_users_table.py @@ -0,0 +1,36 @@ +"""add name and year to users table + +Revision ID: 74e63e0c8e89 +Revises: 2df915a8c6ec +Create Date: 2025-10-27 20:46:29.628712 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '74e63e0c8e89' +down_revision: Union[str, None] = '2df915a8c6ec' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('users', sa.Column('display_name', sa.String(length=255), nullable=True)) + op.add_column('users', sa.Column('year', sa.String(length=255), nullable=True)) + op.add_column('usersdaily', sa.Column('display_name', sa.String(length=255), nullable=True)) + op.add_column('usersdaily', sa.Column('year', sa.String(length=255), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('usersdaily', 'year') + op.drop_column('usersdaily', 'display_name') + op.drop_column('users', 'year') + op.drop_column('users', 'display_name') + # ### end Alembic commands ### diff --git a/app.py b/app.py index 3d65cf8..8e03688 100644 --- a/app.py +++ b/app.py @@ -79,9 +79,16 @@ def index(): # Home page after user logs in through Princeton's CAS @app.route("/menu", methods=["GET"]) def menu(): - username = auth.authenticate() - user_insert = user_database.insert_player(username) - daily_insert = daily_user_database.insert_player_daily(username) + + # YUBI: update authenticate to return dictionary of user information + user_info = auth.authenticate() + username = user_info["username"] + display_name = user_info["displayName"] + year = user_info["year"] + + # YUBI: update database functions!!! + user_insert = user_database.insert_player(username, display_name, year) + daily_insert = daily_user_database.insert_player_daily(username, display_name, year) played_date = daily_user_database.get_last_played_date(username) current_date = pictures_database.get_current_date() @@ -111,7 +118,11 @@ def menu(): @app.route("/requests", methods=["GET"]) def requests(): username = flask.request.args.get("username") - username_auth = auth.authenticate() + user_info = auth.authenticate() + username_auth = user_info["username"] + display_name = user_info["displayName"] + year = user_info["year"] + last_date = daily_user_database.get_last_versus_date(username_auth) current_date = pictures_database.get_current_date() @@ -157,7 +168,10 @@ def game(): id = pictures_database.pic_of_day() - username = auth.authenticate() + user_info = auth.authenticate() + username = user_info["username"] + display_name = user_info["displayName"] + year = user_info["year"] user_played = daily_user_database.player_played(username) today_points = daily_user_database.get_daily_points(username) @@ -198,7 +212,10 @@ def game(): @app.route("/submit", methods=["POST"]) def submit(): id = pictures_database.pic_of_day() - username = auth.authenticate() + user_info = auth.authenticate() + username = user_info["username"] + display_name = user_info["displayName"] + year = user_info["year"] user_played = daily_user_database.player_played(username) today_points = daily_user_database.get_daily_points(username) @@ -262,7 +279,10 @@ def submit(): @app.route("/rules", methods=["GET"]) def rules(): # user must be logged in to access page - auth.authenticate() + user_info = auth.authenticate() + username = user_info["username"] + display_name = user_info["displayName"] + year = user_info["year"] html_code = flask.render_template("rules.html") response = flask.make_response(html_code) return response @@ -274,7 +294,10 @@ def rules(): # Congratulations page easter egg @app.route("/congrats", methods=["GET"]) def congrats(): - username = auth.authenticate() + user_info = auth.authenticate() + username = user_info["username"] + display_name = user_info["displayName"] + year = user_info["year"] top_player = user_database.get_top_player() check = database_check([top_player]) @@ -309,7 +332,10 @@ def congrats(): @app.route("/team", methods=["GET"]) def team(): # user must be logged in to access page - username = auth.authenticate() + user_info = auth.authenticate() + username = user_info["username"] + display_name = user_info["displayName"] + year = user_info["year"] top_player = user_database.get_top_player() check = database_check([top_player]) @@ -333,7 +359,11 @@ def team(): @app.route("/totalboard", methods=["GET"]) def leaderboard(): top_players = user_database.get_top_players() - username = auth.authenticate() + user_info = auth.authenticate() + username = user_info["username"] + display_name = user_info["displayName"] + year = user_info["year"] + points = user_database.get_points(username) daily_points = daily_user_database.get_daily_points(username) rank = user_database.get_rank(username) @@ -368,7 +398,11 @@ def leaderboard(): @app.route("/leaderboard", methods=["GET"]) def totalleaderboard(): top_players = daily_user_database.get_daily_top_players() - username = auth.authenticate() + user_info = auth.authenticate() + username = user_info["username"] + display_name = user_info["displayName"] + year = user_info["year"] + points = user_database.get_points(username) daily_points = daily_user_database.get_daily_points(username) rank = user_database.get_rank(username) @@ -434,7 +468,7 @@ def create_challenge_route(): if ( challengee_id == None or challengee_id not in users - or challengee_id == auth.authenticate() + or challengee_id == (auth.authenticate()["username"]) ): response = { "status": "error", @@ -442,8 +476,10 @@ def create_challenge_route(): } return flask.jsonify(response), 400 # Including a 400 Bad Request status code else: + user_info = auth.authenticate() + username = user_info["username"] result = challenges_database.create_challenge( - auth.authenticate(), challengee_id + username, challengee_id ) check = database_check([result]) @@ -521,7 +557,9 @@ def decline_challenge_route(): @app.route("/play_button", methods=["POST"]) def play_button(): challenge_id = flask.request.form.get("challenge_id") - user = auth.authenticate() + user_info = auth.authenticate() + user = user_info["username"] + status = challenges_database.get_playbutton_status(challenge_id, user) check = database_check([status]) if check is False: @@ -625,7 +663,9 @@ def start_challenge(challenge_id=None, index=None): @app.route("/end_challenge", methods=["POST"]) def end_challenge(): challenge_id = flask.request.form.get("challenge_id") - user = auth.authenticate() + user_info = auth.authenticate() + user = user_info["username"] + finish = challenges_database.update_finish_status(challenge_id, user) check = database_check([finish]) if check is False: @@ -675,7 +715,7 @@ def submit2(): return flask.make_response(html_code) if not currLat or not currLon: pic_status = versus_database.get_versus_pic_status( - challenge_id, auth.authenticate(), index + 1 + challenge_id, (auth.authenticate()["username"]), index + 1 ) check = database_check([pic_status]) if check is False: @@ -685,17 +725,17 @@ def submit2(): return flask.redirect(flask.url_for("requests")) if pic_status is False: fin1 = versus_database.update_versus_pic_status( - challenge_id, auth.authenticate(), index + 1 + challenge_id, (auth.authenticate()["username"]), index + 1 ) if fin1 is None: return flask.redirect(flask.url_for("requests")) fin2 = versus_database.store_versus_pic_points( - challenge_id, auth.authenticate(), index + 1, points + challenge_id, (auth.authenticate()["username"]), index + 1, points ) if fin2 is None: return flask.redirect(flask.url_for("requests")) fin3 = versus_database.update_versus_points( - challenge_id, auth.authenticate(), points + challenge_id, (auth.authenticate()["username"]), points ) if fin3 is None: return flask.redirect(flask.url_for("requests")) @@ -725,7 +765,7 @@ def submit2(): return flask.redirect(flask.url_for("requests")) distance = round(distance_func.calc_distance(currLat, currLon, coor)) pic_status = versus_database.get_versus_pic_status( - challenge_id, auth.authenticate(), index + 1 + challenge_id, (auth.authenticate()["username"]), index + 1 ) check = database_check([pic_status]) if check is False: @@ -736,17 +776,17 @@ def submit2(): if pic_status is False: points = round(versus_database.calculate_versus(distance, time)) fin1 = versus_database.store_versus_pic_points( - challenge_id, auth.authenticate(), index + 1, points + challenge_id, (auth.authenticate()["username"]), index + 1, points ) if fin1 is None: return flask.redirect(flask.url_for("requests")) fin2 = versus_database.update_versus_points( - challenge_id, auth.authenticate(), points + challenge_id, (auth.authenticate()["username"]), points ) if fin2 is None: return flask.redirect(flask.url_for("requests")) fin3 = versus_database.update_versus_pic_status( - challenge_id, auth.authenticate(), index + 1 + challenge_id, (auth.authenticate()["username"]), index + 1 ) if fin3 is None: return flask.redirect(flask.url_for("requests")) diff --git a/src/CAS/auth.py b/src/CAS/auth.py index 07f308b..933fea3 100644 --- a/src/CAS/auth.py +++ b/src/CAS/auth.py @@ -6,6 +6,7 @@ import urllib.parse import re import flask +import json # ----------------------------------------------------------------------- @@ -32,24 +33,49 @@ def strip_ticket(url): def validate(ticket): + + # YUBI: updated using v3 val_url = ( _CAS_URL - + "validate" + + "p3/serviceValidate" + "?service=" + urllib.parse.quote(strip_ticket(flask.request.url)) + "&ticket=" + urllib.parse.quote(ticket) + + "&format=json" ) - lines = [] - with urllib.request.urlopen(val_url) as flo: - lines = flo.readlines() # Should return 2 lines. - if len(lines) != 2: - return None - first_line = lines[0].decode("utf-8") - second_line = lines[1].decode("utf-8") - if not first_line.startswith("yes"): + + with urllib.request.urlopen(val_url) as response: + data = json.load(response) + + # Check if authentication was successful + service_response = data.get("serviceResponse", {}) + auth_success = service_response.get("authenticationSuccess") + if not auth_success: return None - return second_line + + username = auth_success.get("user", "").strip() + attributes = auth_success.get("attributes", {}) + + # Extract displayName + display_name = "" + if "displayName" in attributes: + # Could be a list + if isinstance(attributes["displayName"], list): + display_name = attributes["displayName"][0] + else: + display_name = attributes["displayName"] + + # Extract class year from grouperGroups + year = "Graduate" + grouper_groups = attributes.get("grouperGroups", []) + if isinstance(grouper_groups, list): + for g in grouper_groups: + if "PU:basis:classyear:" in g: + year = g.split(":")[-1] + break + + return {"username": username, "displayName": display_name or username, "year": year} # ----------------------------------------------------------------------- @@ -59,23 +85,19 @@ def validate(ticket): def authenticate(): + # If already authenticated, return cached info + if "user_info" in flask.session: + return flask.session.get("user_info") - # If the username is in the session, then the user was - # authenticated previously. So return the username. - if "username" in flask.session: - return flask.session.get("username") - - # If the request does not contain a login ticket, then redirect - # the browser to the login page to get one. + # If no ticket, redirect to CAS login ticket = flask.request.args.get("ticket") if ticket is None: login_url = _CAS_URL + "login?service=" + urllib.parse.quote(flask.request.url) flask.abort(flask.redirect(login_url)) - # If the login ticket is invalid, then redirect the browser - # to the login page to get a new one. - username = validate(ticket) - if username is None: + # Validate ticket + user_info = validate(ticket) + if user_info is None: login_url = ( _CAS_URL + "login?service=" @@ -83,11 +105,9 @@ def authenticate(): ) flask.abort(flask.redirect(login_url)) - # The user is authenticated, so store the username in - # the session. - username = username.strip() - flask.session["username"] = username - return username + # Store in session + flask.session["user_info"] = user_info + return user_info # ----------------------------------------------------------------------- @@ -107,6 +127,7 @@ def logoutapp(): def logoutcas(): + # YUBI: ASK, does this correctly logout of cas? # Log out of the CAS session, and then the application. logout_url = ( _CAS_URL diff --git a/src/Databases/daily_user_database.py b/src/Databases/daily_user_database.py index b725b96..3451289 100644 --- a/src/Databases/daily_user_database.py +++ b/src/Databases/daily_user_database.py @@ -12,7 +12,7 @@ # Inserts username into usersDaily table. -def insert_player_daily(username): +def insert_player_daily(username, display_name, year): try: with get_session() as session: existing = session.query(UserDaily).filter_by(username=username).first() @@ -21,6 +21,8 @@ def insert_player_daily(username): new_user = UserDaily( username=username, points=0, + display_name=display_name, + year=year, distance=0, played=False, last_played=None, @@ -287,7 +289,9 @@ def get_daily_top_players(): ) for user in users: - player_stats = {"username": user.username, "points": user.points} + # YUBI: update player_stats to show display_name concatenated with year + # I have replaced username with the name, but kept the json reference as username for now + player_stats = {"username": user.display_name + " (" + user.year + ")", "points": user.points} daily_top_players.append(player_stats) return daily_top_players diff --git a/src/Databases/user_database.py b/src/Databases/user_database.py index d594785..3657948 100644 --- a/src/Databases/user_database.py +++ b/src/Databases/user_database.py @@ -11,15 +11,15 @@ # Inserts username into users table. - -def insert_player(username): +# YUBI: update function to enable addition of displayName and year as well +def insert_player(username, display_name, year): try: with get_session() as session: # Check if username exists existing = session.query(User).filter_by(username=username).first() if existing is None: - new_user = User(username=username, points=0) + new_user = User(username=username, points=0, display_name=display_name, year=year) session.add(new_user) return "success" @@ -71,8 +71,8 @@ def reset_all_players_total_points(): # ----------------------------------------------------------------------- # Updates username's total points with points. - - +# YUBI ASK: in this function, do I need to update the full user table or just the points? +# I don't want to lose the other columns in the user table when I update points def update_player(username, points): try: with get_session() as session: @@ -151,9 +151,12 @@ def get_top_players(): .limit(10) .all() ) + # YUBI, ASK: does users return the whole row of information including display name and year? for user in users: - player_stats = {"username": user.username, "points": user.points} + # YUBI: replace username with display name and year + # Keep reference as username, but want to change this to name later + player_stats = {"username": user.display_name + " (" + user.year + ")", "points": user.points} top_players.append(player_stats) return top_players @@ -216,7 +219,8 @@ def get_top_player(): if user is None: return {"username": None, "points": 0} - player_stats = {"username": user.username, "points": user.points} + # YUBI: replace username with display name and year + player_stats = {"username": user.display_name + " (" + user.year + ")", "points": user.points} return player_stats diff --git a/src/models.py b/src/models.py index 25930c8..ce42658 100644 --- a/src/models.py +++ b/src/models.py @@ -18,9 +18,12 @@ class User(Base): username = Column(String(255), primary_key=True) points = Column(Integer, default=0) + display_name = Column(String(255), default="") + year = Column(String(255), default="") def __repr__(self): - return f"" + # YUBI: add display name and year to user table + return f"" # ----------------------------------------------------------------------- @@ -31,8 +34,11 @@ class UserDaily(Base): __tablename__ = "usersdaily" + # YUBI: add display name and year to user table username = Column(String(255), primary_key=True) points = Column(Integer, default=0) + display_name = Column(String(255), default="") + year = Column(String(255), default="") distance = Column(Integer, default=0) played = Column(Boolean, default=False) last_played = Column(Date, nullable=True) @@ -40,7 +46,7 @@ class UserDaily(Base): current_streak = Column(Integer, default=0) def __repr__(self): - return f"" + return f"" # ----------------------------------------------------------------------- @@ -98,7 +104,7 @@ def __repr__(self): # ----------------------------------------------------------------------- - +# YUBI ASK: do we want to show displayName instead of netID in matches as well? class Match(Base): """Model for matches table - stores completed versus mode match results"""