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__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
*.cpython-312.pyc
|
*.cpython-312.pyc
|
||||||
|
venv/
|
||||||
8
app.py
8
app.py
@@ -2,13 +2,12 @@ import streamlit as st
|
|||||||
import webpages as pg
|
import webpages as pg
|
||||||
from webpages.navigation import navigation_bar as nv
|
from webpages.navigation import navigation_bar as nv
|
||||||
|
|
||||||
|
|
||||||
st.set_page_config(page_title="BuffTeks Student Organization",
|
st.set_page_config(page_title="BuffTeks Student Organization",
|
||||||
page_icon="./images/BuffTeksLogo.png",
|
page_icon="./images/BuffTeksLogo.png",
|
||||||
layout = "centered"
|
layout = "centered"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
page_label = nv()
|
page_label = nv()
|
||||||
|
|
||||||
# block for main pages
|
# block for main pages
|
||||||
@@ -64,4 +63,7 @@ elif page_label == "CoreTeks":
|
|||||||
elif page_label == "Testing":
|
elif page_label == "Testing":
|
||||||
pg.testing()
|
pg.testing()
|
||||||
elif page_label == "Reference":
|
elif page_label == "Reference":
|
||||||
pg.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 .cis_tech_challenge_pages.cis_tech_challenge_homepage import cis_tech_challenge_homepage
|
||||||
from .coreteks_pages.coreteks_homepage import coreteks_homepage
|
from .coreteks_pages.coreteks_homepage import coreteks_homepage
|
||||||
from .SendEmail import send_email
|
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
|
import streamlit_antd_components as sac
|
||||||
|
|
||||||
|
from .buffteks_authenticator import load_authenticator
|
||||||
|
|
||||||
|
|
||||||
# This doc is used to set up the navigation bar
|
# This doc is used to set up the navigation bar
|
||||||
# basic structure is:
|
# basic structure is:
|
||||||
def navigation_bar():
|
def navigation_bar():
|
||||||
|
|
||||||
# start page is the homepage
|
# start page is the homepage
|
||||||
page_label = "Homepage"
|
page_label = "Homepage"
|
||||||
|
|
||||||
@@ -44,6 +48,9 @@ def navigation_bar():
|
|||||||
|
|
||||||
sac.MenuItem(type='divider'),
|
sac.MenuItem(type='divider'),
|
||||||
sac.MenuItem("Reference", icon='paperclip'),
|
sac.MenuItem("Reference", icon='paperclip'),
|
||||||
|
|
||||||
|
sac.MenuItem(type='divider'),
|
||||||
|
sac.MenuItem("Admin", icon='lock'),
|
||||||
|
|
||||||
], open_all=True)
|
], open_all=True)
|
||||||
|
|
||||||
|
|||||||
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