add admin page to manage private data
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,3 +4,4 @@ files/student_locations.db
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.cpython-312.pyc
|
||||
venv/
|
||||
6
app.py
6
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
|
||||
@@ -65,3 +64,6 @@ elif page_label == "Testing":
|
||||
pg.testing()
|
||||
elif page_label == "Reference":
|
||||
pg.reference()
|
||||
|
||||
elif page_label == "Admin":
|
||||
pg.admin()
|
||||
@@ -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
|
||||
34
webpages/admin.py
Normal file
34
webpages/admin.py
Normal file
@@ -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()
|
||||
68
webpages/adminpages/email_helloween_image.py
Normal file
68
webpages/adminpages/email_helloween_image.py
Normal file
@@ -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 = """
|
||||
<html>
|
||||
<head>
|
||||
<title>Happy Halloween!</title>
|
||||
</head>
|
||||
<body style="font-family: Arial, sans-serif; background-color: #1a1a1a; color: #f0f0f0; margin: 0; padding: 20px; text-align: center;">
|
||||
<div style="max-width: 600px; margin: auto; background-color: #2c2c2c; padding: 30px; border-radius: 10px; box-shadow: 0 4px 8px rgba(0,0,0,0.5);">
|
||||
<h2 style="color: #ff6600;">🎃 Happy Halloween! 🎃</h2>
|
||||
<p style="font-size: 16px; line-height: 1.6;">
|
||||
Thank you for your interest in the
|
||||
<a href="https://buffteks.org" target="_blank" rel="noopener" style="color: #ff8533; text-decoration: none;" onmouseover="this.style.textDecoration='underline';" onmouseout="this.style.textDecoration='none';">BuffTeks</a>
|
||||
student organization!
|
||||
</p>
|
||||
<p style="font-size: 16px; line-height: 1.6;">
|
||||
Wishing you a spooktacular Halloween!<br>
|
||||
Here is your special holiday photo generated just for you.<br>
|
||||
Enjoy and have a frightfully fun day!
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
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)}")
|
||||
123
webpages/adminpages/helloween_image.py
Normal file
123
webpages/adminpages/helloween_image.py
Normal file
@@ -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("<h1 style='text-align: center; color: #451002;'>🎃 Halloween Photo Booth 📸</h1>", unsafe_allow_html=True)
|
||||
st.markdown("<h5 style='text-align: center;'> Capture your Halloween spirit with a festive photo! 👻 </h3>", 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
|
||||
18
webpages/buffteks_authenticator.py
Normal file
18
webpages/buffteks_authenticator.py
Normal file
@@ -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
|
||||
@@ -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"
|
||||
|
||||
@@ -45,6 +49,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)
|
||||
|
||||
return page_label
|
||||
|
||||
32
webpages/users.yaml
Normal file
32
webpages/users.yaml
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user