diff --git a/.gitignore b/.gitignore
index 52778a37e..114734176 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,4 @@ files/student_locations.db
__pycache__/
*.pyc
*.cpython-312.pyc
+venv/
\ No newline at end of file
diff --git a/app.py b/app.py
index b446f030e..aed583d9e 100644
--- a/app.py
+++ b/app.py
@@ -2,13 +2,12 @@ import streamlit as st
import webpages as pg
from webpages.navigation import navigation_bar as nv
+
st.set_page_config(page_title="BuffTeks Student Organization",
page_icon="./images/BuffTeksLogo.png",
layout = "centered"
)
-
-
page_label = nv()
# block for main pages
@@ -64,4 +63,7 @@ elif page_label == "CoreTeks":
elif page_label == "Testing":
pg.testing()
elif page_label == "Reference":
- pg.reference()
\ No newline at end of file
+ pg.reference()
+
+elif page_label == "Admin":
+ pg.admin()
\ No newline at end of file
diff --git a/webpages/__init__.py b/webpages/__init__.py
index 4c0ea71af..bfdc49641 100644
--- a/webpages/__init__.py
+++ b/webpages/__init__.py
@@ -19,3 +19,4 @@ from .outstanding_members import outstanding_members
from .cis_tech_challenge_pages.cis_tech_challenge_homepage import cis_tech_challenge_homepage
from .coreteks_pages.coreteks_homepage import coreteks_homepage
from .SendEmail import send_email
+from .admin import admin
\ No newline at end of file
diff --git a/webpages/admin.py b/webpages/admin.py
new file mode 100644
index 000000000..0f1a688f9
--- /dev/null
+++ b/webpages/admin.py
@@ -0,0 +1,34 @@
+import streamlit as st
+import pandas as pd
+import streamlit_authenticator as stauth
+
+from webpages.buffteks_authenticator import load_authenticator
+
+from webpages.adminpages.helloween_image import camera_photo
+
+def admin():
+
+ try:
+ authenticator = load_authenticator()
+ authenticator.login()
+
+ except Exception as e:
+ st.error(e)
+
+
+ if st.session_state.get('authentication_status') is False:
+ st.error('Username/password is incorrect')
+ elif st.session_state.get('authentication_status') is None:
+ st.warning('Please enter your username and password')
+
+ if st.session_state.get('authentication_status'):
+ authenticator.logout()
+ st.header(f'Welcome *{st.session_state.get("name")}*')
+
+ st.divider()
+ with st.expander("Check Memberships"):
+ new_members = pd.read_json("new_members.json")
+ st.dataframe(new_members)
+
+ st.divider()
+ camera_photo()
diff --git a/webpages/adminpages/email_helloween_image.py b/webpages/adminpages/email_helloween_image.py
new file mode 100644
index 000000000..6ed6b7492
--- /dev/null
+++ b/webpages/adminpages/email_helloween_image.py
@@ -0,0 +1,68 @@
+import smtplib
+import io
+from email.mime.text import MIMEText
+from email.mime.multipart import MIMEMultipart
+from email.mime.image import MIMEImage
+
+def send_email_with_attachment(sender_email, password, to_email, subject, image_bytes):
+ """
+ Sends an email with an image attachment.
+
+ Args:
+ sender_email (str): The sender's email address.
+ password (str): The sender's email password.
+ to_email (str): The recipient's email address.
+ subject (str): The subject of the email.
+ image_bytes (bytes): The image content as a byte string.
+ """
+ message = MIMEMultipart('alternative')
+ message['Subject'] = subject
+ message['From'] = sender_email
+ message['To'] = to_email
+
+ html_content = """
+
+
+ Happy Halloween!
+
+
+
+
๐ Happy Halloween! ๐
+
+ Thank you for your interest in the
+ BuffTeks
+ student organization!
+
+
+ Wishing you a spooktacular Halloween!
+ Here is your special holiday photo generated just for you.
+ Enjoy and have a frightfully fun day!
+
+
+
+
+ """
+ message.attach(MIMEText(html_content, 'html'))
+
+ if image_bytes:
+ # The image object is converted to bytes before attaching.
+ img_byte_arr = io.BytesIO()
+ # Assuming image_bytes is a PIL Image object.
+ # Convert to RGB if necessary for JPEG format.
+ if image_bytes.mode in ('RGBA', 'P'):
+ image_bytes = image_bytes.convert('RGB')
+ image_bytes.save(img_byte_arr, format='JPEG')
+ img_byte_arr = img_byte_arr.getvalue()
+
+ image_part = MIMEImage(img_byte_arr, _subtype="jpeg")
+ image_part.add_header('Content-Disposition', 'attachment', filename="halloween.jpg")
+ message.attach(image_part)
+
+ try:
+ with smtplib.SMTP('mail.privateemail.com', 587) as server:
+ server.starttls()
+ server.login(sender_email, password)
+ server.send_message(message)
+ print("Email sent successfully!")
+ except Exception as e:
+ print(f"Failed to send email: {str(e)}")
\ No newline at end of file
diff --git a/webpages/adminpages/helloween_image.py b/webpages/adminpages/helloween_image.py
new file mode 100644
index 000000000..1b2c53f9d
--- /dev/null
+++ b/webpages/adminpages/helloween_image.py
@@ -0,0 +1,123 @@
+import streamlit as st
+from PIL import Image
+from google import genai
+import json
+from io import BytesIO
+from .email_helloween_image import send_email_with_attachment
+
+
+def camera_photo():
+ st.markdown("๐ Halloween Photo Booth ๐ธ
", unsafe_allow_html=True)
+ st.markdown(" Capture your Halloween spirit with a festive photo! ๐ป ", unsafe_allow_html=True)
+
+ if 'image_response' not in st.session_state:
+ st.session_state.image_response = None
+
+ enable_camera = st.checkbox("Enable Camera")
+ if enable_camera:
+ st.info("๐Note: No photos are stored. The image is processed in-memory and deleted after refresh app.")
+ picture = st.camera_input("Take a Halloween-themed photo! ๐๐ป๐ธ๏ธ")
+
+ if picture is not None:
+ image = Image.open(picture)
+
+ keep_dressing_style = st.checkbox("Keep original dressing style", value=False)
+ keep_body_pose = st.checkbox("Keep original body pose", value=False)
+
+ theme = st.selectbox(
+ "Select or Input a Halloween theme for your photo:",
+ [ "๐ Classic Halloween",
+ "๐ป Haunted House",
+ "โ๏ธ Science Experiment Gone Wrong",
+ "๐ฅ Horror Film Inspired",
+ "๐ฏ๏ธ Gothic",
+ "๐งโโ๏ธ Harry Potter",
+ "โฐ๏ธ Graveyard",
+ "๐ฎ Cult Horror Theme"
+ ],
+ index=0,
+ key="theme_selector"
+ )
+
+ if st.button("AI Editing"):
+ edit_prompt = f"""
+ Retain original facial features (eyes, nose, mouth etc.) of any persons in the photo. The goal is to achieve a realistic, edited look, not an AI-generated or overly smoothed/perfected appearance. Avoid any artificial alterations to these features.
+ Keep the original dressing style: {str(keep_dressing_style)}
+ Keep the original body pose: {str(keep_body_pose)}
+ Theme: {theme}
+ If a person is already in costume, enhance the costume and add more Halloween elements and background to the scene.
+ If multiple persons are present, ensure all are included in the Halloween theme.
+ - Overall Style: The final image should be photorealistic.
+ Ensure the final image is vibrant, festive, and captures the Halloween spirit!
+ """
+ with st.spinner("Editing image..."):
+ text_response, image_response = edit_image_with_ai(image, edit_prompt)
+ if text_response:
+ st.info(text_response)
+ if image_response:
+ st.session_state.image_response = image_response
+ # Rerun to display the generated image and send options
+ st.rerun()
+
+ if st.session_state.image_response:
+ st.image(st.session_state.image_response)
+ # Convert PIL image to bytes for download
+ img_byte_arr = BytesIO()
+ st.session_state.image_response.save(img_byte_arr, format="JPEG")
+ st.download_button(
+ label="Download Edited Image",
+ data=img_byte_arr.getvalue(),
+ file_name="edited_image.jpeg",
+ mime="image/jpeg"
+ )
+ send_image(st.session_state.image_response)
+
+def send_image(image_response):
+ st.divider()
+ to_email = st.text_input("Enter your email to receive the photo:", key="email_input")
+ if st.button("Send Email"):
+ with st.spinner("Sending email..."):
+ if to_email and image_response:
+ with open('app_config.json') as config_file:
+ config = json.load(config_file)
+ sender_email = config["send_email"]["sender_email"]
+ password = config["send_email"]["password"]
+ subject = "Your Spooktacular Halloween Photo! ๐๐ป"
+ send_email_with_attachment(sender_email, password, to_email, subject, image_response)
+ st.success("Email sent successfully!")
+ else:
+ st.error("Please enter a valid email address and ensure the image is generated.")
+
+
+
+
+def edit_image_with_ai(image, description):
+ with open('app_config.json') as config_file:
+ config = json.load(config_file)
+ api_key = config["nano-banana"]["api_key"]
+
+ client = genai.Client(api_key=api_key)
+
+ prompt = description
+
+ response = client.models.generate_content(
+ model="gemini-2.5-flash-image-preview",
+ contents=[prompt, image],
+ )
+
+ text_response = None
+ image_response = None
+
+ # AttributeError: 'NoneType' object has no attribute 'parts'
+ if response.candidates and len(response.candidates) <= 0:
+ st.error("No response from AI model. Please try again.")
+ return text_response, image_response
+
+ for part in response.candidates[0].content.parts:
+ if part.text is not None:
+ text_response = part.text
+
+ if part.inline_data is not None:
+ image_response = Image.open(BytesIO(part.inline_data.data))
+
+ return text_response, image_response
diff --git a/webpages/buffteks_authenticator.py b/webpages/buffteks_authenticator.py
new file mode 100644
index 000000000..491bace49
--- /dev/null
+++ b/webpages/buffteks_authenticator.py
@@ -0,0 +1,18 @@
+
+# for authentication
+import yaml
+from yaml.loader import SafeLoader
+import streamlit_authenticator as stauth
+
+
+def load_authenticator():
+ with open('webpages/users.yaml') as f:
+ users = yaml.load(f, Loader=SafeLoader)
+
+ authenticator = stauth.Authenticate(
+ users['credentials'],
+ users['cookie']['name'],
+ users['cookie']['key'],
+ users['cookie']['expiry_days']
+ )
+ return authenticator
\ No newline at end of file
diff --git a/webpages/navigation.py b/webpages/navigation.py
index cbcc2568e..d0af212d0 100644
--- a/webpages/navigation.py
+++ b/webpages/navigation.py
@@ -2,9 +2,13 @@ import streamlit as st
import streamlit_antd_components as sac
+from .buffteks_authenticator import load_authenticator
+
+
# This doc is used to set up the navigation bar
# basic structure is:
def navigation_bar():
+
# start page is the homepage
page_label = "Homepage"
@@ -44,6 +48,9 @@ def navigation_bar():
sac.MenuItem(type='divider'),
sac.MenuItem("Reference", icon='paperclip'),
+
+ sac.MenuItem(type='divider'),
+ sac.MenuItem("Admin", icon='lock'),
], open_all=True)
diff --git a/webpages/users.yaml b/webpages/users.yaml
new file mode 100644
index 000000000..b77ddf5b8
--- /dev/null
+++ b/webpages/users.yaml
@@ -0,0 +1,32 @@
+cookie:
+ expiry_days: 30
+ key: buffteks.org # To be filled with any string
+ name: buffteks.org # To be filled with any string
+credentials:
+ usernames:
+ czhang:
+ email: czhang@wtamu.edu
+ failed_login_attempts: 0 # Will be managed automatically
+ first_name: Carl
+ last_name: Zhang
+ logged_in: False # Will be managed automatically
+ password: 1992Carl@* # Will be hashed automatically
+ roles: # Optional
+ - admin
+ - editor
+ - viewer
+
+# oauth2: # Optional
+# google: # Follow instructions: https://developers.google.com/identity/protocols/oauth2
+# client_id: # To be filled
+# client_secret: # To be filled
+# redirect_uri: # URL to redirect to after OAuth2 authentication
+# microsoft: # Follow instructions: https://learn.microsoft.com/en-us/graph/auth-register-app-v2
+# client_id: # To be filled
+# client_secret: # To be filled
+# redirect_uri: # URL to redirect to after OAuth2 authentication
+# tenant_id: # To be filled
+# pre-authorized: # Optional
+# emails:
+# - melsby@gmail.com
+# api_key: # Optional - register to receive a free API key: https://stauthenticator.com
\ No newline at end of file