diff --git a/app.py b/app.py index b53447a9c..83c0e4e70 100644 --- a/app.py +++ b/app.py @@ -17,6 +17,10 @@ if page_label == "Homepage": elif page_label == "BuffBot": pg.buffbot() + +elif page_label == "Letter Generator": + pg.letter_generator() + elif page_label == "Outstanding Members": pg.outstanding_members() diff --git a/images/02-05-2025-AI101.jpg b/images/02-05-2025-AI101.jpg new file mode 100644 index 000000000..ec01b658d Binary files /dev/null and b/images/02-05-2025-AI101.jpg differ diff --git a/images/04-08-2025-ModernWebDevelopment.jpg b/images/04-08-2025-ModernWebDevelopment.jpg new file mode 100644 index 000000000..7629ad1e0 Binary files /dev/null and b/images/04-08-2025-ModernWebDevelopment.jpg differ diff --git a/images/ButtTools-SampleTemplatePlaceHolder.jpg b/images/ButtTools-SampleTemplatePlaceHolder.jpg new file mode 100644 index 000000000..533485c20 Binary files /dev/null and b/images/ButtTools-SampleTemplatePlaceHolder.jpg differ diff --git a/webpages/__init__.py b/webpages/__init__.py index 30ecd37ad..63d9bdf67 100644 --- a/webpages/__init__.py +++ b/webpages/__init__.py @@ -8,6 +8,7 @@ from .reference import reference from .pythonx_lessons_pages.pythonx_homepage import pythonx_homepage from .pythonx_lessons_pages.pythonx_introduction import pythonx_introduction from .buff_bot import buffbot +from .bufftools_pages.letter_generator import letter_generator from .pythonx_lessons_pages.pythonx_finance import pythonx_finance from .pythonx_lessons_pages.pythonx_geomap import pythonx_geomap from .pythonx_lessons_pages.pythonx_wordcloud import pythonx_wordcloud diff --git a/webpages/__pycache__/SendEmail.cpython-312.pyc b/webpages/__pycache__/SendEmail.cpython-312.pyc index 81593896c..1eb5d565d 100644 Binary files a/webpages/__pycache__/SendEmail.cpython-312.pyc and b/webpages/__pycache__/SendEmail.cpython-312.pyc differ diff --git a/webpages/__pycache__/__init__.cpython-312.pyc b/webpages/__pycache__/__init__.cpython-312.pyc index 68e5d3dd1..0e97f747c 100644 Binary files a/webpages/__pycache__/__init__.cpython-312.pyc and b/webpages/__pycache__/__init__.cpython-312.pyc differ diff --git a/webpages/__pycache__/buff_bot.cpython-312.pyc b/webpages/__pycache__/buff_bot.cpython-312.pyc index 4428eec7b..6ab39c712 100644 Binary files a/webpages/__pycache__/buff_bot.cpython-312.pyc and b/webpages/__pycache__/buff_bot.cpython-312.pyc differ diff --git a/webpages/__pycache__/classroom.cpython-312.pyc b/webpages/__pycache__/classroom.cpython-312.pyc index 7f8856bb1..fb2ab5ecf 100644 Binary files a/webpages/__pycache__/classroom.cpython-312.pyc and b/webpages/__pycache__/classroom.cpython-312.pyc differ diff --git a/webpages/__pycache__/code_editor.cpython-312.pyc b/webpages/__pycache__/code_editor.cpython-312.pyc index 5d3d7d243..c964185a8 100644 Binary files a/webpages/__pycache__/code_editor.cpython-312.pyc and b/webpages/__pycache__/code_editor.cpython-312.pyc differ diff --git a/webpages/__pycache__/event.cpython-312.pyc b/webpages/__pycache__/event.cpython-312.pyc index 0b8a55394..908ed1932 100644 Binary files a/webpages/__pycache__/event.cpython-312.pyc and b/webpages/__pycache__/event.cpython-312.pyc differ diff --git a/webpages/__pycache__/home.cpython-312.pyc b/webpages/__pycache__/home.cpython-312.pyc index 6c599dd4e..9b14a31e1 100644 Binary files a/webpages/__pycache__/home.cpython-312.pyc and b/webpages/__pycache__/home.cpython-312.pyc differ diff --git a/webpages/__pycache__/join_us.cpython-312.pyc b/webpages/__pycache__/join_us.cpython-312.pyc index 2c8dd71b8..c09db43c9 100644 Binary files a/webpages/__pycache__/join_us.cpython-312.pyc and b/webpages/__pycache__/join_us.cpython-312.pyc differ diff --git a/webpages/__pycache__/navigation.cpython-312.pyc b/webpages/__pycache__/navigation.cpython-312.pyc index 9447b3b16..bc7ae91ac 100644 Binary files a/webpages/__pycache__/navigation.cpython-312.pyc and b/webpages/__pycache__/navigation.cpython-312.pyc differ diff --git a/webpages/__pycache__/outstanding_members.cpython-312.pyc b/webpages/__pycache__/outstanding_members.cpython-312.pyc index 916e506a0..29d136a08 100644 Binary files a/webpages/__pycache__/outstanding_members.cpython-312.pyc and b/webpages/__pycache__/outstanding_members.cpython-312.pyc differ diff --git a/webpages/__pycache__/project.cpython-312.pyc b/webpages/__pycache__/project.cpython-312.pyc index c4ae24010..8764ef24c 100644 Binary files a/webpages/__pycache__/project.cpython-312.pyc and b/webpages/__pycache__/project.cpython-312.pyc differ diff --git a/webpages/__pycache__/reference.cpython-312.pyc b/webpages/__pycache__/reference.cpython-312.pyc index 414db5c79..e68cca931 100644 Binary files a/webpages/__pycache__/reference.cpython-312.pyc and b/webpages/__pycache__/reference.cpython-312.pyc differ diff --git a/webpages/__pycache__/testing.cpython-312.pyc b/webpages/__pycache__/testing.cpython-312.pyc index 3a249e3c0..e6d7e3e85 100644 Binary files a/webpages/__pycache__/testing.cpython-312.pyc and b/webpages/__pycache__/testing.cpython-312.pyc differ diff --git a/webpages/bufftools_pages/__pycache__/letter_generator.cpython-312.pyc b/webpages/bufftools_pages/__pycache__/letter_generator.cpython-312.pyc new file mode 100644 index 000000000..059080901 Binary files /dev/null and b/webpages/bufftools_pages/__pycache__/letter_generator.cpython-312.pyc differ diff --git a/webpages/bufftools_pages/letter_generator.py b/webpages/bufftools_pages/letter_generator.py new file mode 100644 index 000000000..8fec55751 --- /dev/null +++ b/webpages/bufftools_pages/letter_generator.py @@ -0,0 +1,207 @@ +import os +import tempfile +import streamlit as st +from docx import Document +from io import BytesIO +import base64 +import re +from zipfile import ZipFile +import subprocess +import shutil + +def sanitize_filename(name): + """Convert name to a safe filename""" + name = re.sub(r'[^\w\s-]', '', str(name)).strip() + return re.sub(r'[-\s]+', '_', name) + +def check_pandoc_installed(): + try: + subprocess.run(["pandoc", "--version"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + return True + except (subprocess.CalledProcessError, FileNotFoundError): + return False + +def check_libreoffice_installed(): + try: + subprocess.run(["libreoffice", "--version"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + return True + except (subprocess.CalledProcessError, FileNotFoundError): + return False + +def convert_with_libreoffice(docx_path, pdf_path): + """Convert using LibreOffice (better for complex docs with images)""" + try: + cmd = [ + "libreoffice", + "--headless", + "--convert-to", "pdf", + "--outdir", os.path.dirname(pdf_path), + docx_path + ] + result = subprocess.run(cmd, check=True) + # LibreOffice names output as input file but with .pdf extension + expected_path = os.path.splitext(docx_path)[0] + ".pdf" + if os.path.exists(expected_path): + if expected_path != pdf_path: + shutil.move(expected_path, pdf_path) + return True + return False + except subprocess.CalledProcessError as e: + st.error(f"LibreOffice conversion failed: {str(e)}") + return False + +def convert_to_pdf(docx_path, pdf_path): + """Try multiple conversion methods to preserve images""" + # First try LibreOffice if available + if check_libreoffice_installed(): + if convert_with_libreoffice(docx_path, pdf_path): + return True + + # Fallback to Pandoc if LibreOffice fails or isn't available + if check_pandoc_installed(): + try: + cmd = [ + "pandoc", + docx_path, + "-o", pdf_path, + "--pdf-engine=xelatex", + "--resource-path", os.path.dirname(docx_path), + "--extract-media", os.path.dirname(docx_path) + ] + subprocess.run(cmd, check=True) + return True + except subprocess.CalledProcessError as e: + st.error(f"Pandoc conversion failed: {str(e)}") + + return False + +def letter_generator(): + st.title("📄 Generate Letters for Recipient") + st.markdown(""" + Generate documents based on recipient names in Word/PDF files. + + This is useful for creating personalized letters or certificates based on given templates. + """) + + # Check requirements + if not (check_pandoc_installed() or check_libreoffice_installed()): + st.error(""" + **Required tools missing**: + - Install [LibreOffice](https://www.libreoffice.org/) for best PDF conversion + - Or install [Pandoc](https://pandoc.org/installing.html) as fallback + """) + st.stop() + + st.image("./images/ButtTools-SampleTemplatePlaceHolder.jpg", width=400, caption="Sample Template with Placeholder") + + + # File upload + st.subheader("1. Upload Template") + template_file = st.file_uploader("Word template (.docx)", type=["docx"]) + + # Placeholder config + st.subheader("2. Configure Placeholders") + placeholder = st.text_input("Placeholder to replace (e.g., [NAME])", "[NAME]") + + # Data input + st.subheader("3. Enter Values (Names)") + names = st.text_area("List values (one per line)", height=150).split('\n') + + # Output options + st.subheader("4. Output Format") + output_format = st.radio("", ["Word", "PDF"], index=1) + + if st.button("✨ Generate Documents"): + if not template_file: + st.error("Please upload a template file") + return + + names = [n.strip() for n in names if n.strip()] + if not names: + st.error("Please enter at least one value") + return + + generate_documents(template_file, names, placeholder, output_format) + +def generate_documents(template_file, names, placeholder, output_format): + with st.spinner(f"Generating {len(names)} {output_format} files..."): + with tempfile.TemporaryDirectory() as temp_dir: + # Save template + template_path = os.path.join(temp_dir, "template.docx") + with open(template_path, "wb") as f: + f.write(template_file.getbuffer()) + + results = [] + progress_bar = st.progress(0) + + for i, name in enumerate(names): + progress = (i + 1) / len(names) + progress_bar.progress(progress) + + try: + # Customize document + doc = Document(template_path) + for p in doc.paragraphs: + if placeholder in p.text: + for r in p.runs: + if placeholder in r.text: + r.text = r.text.replace(placeholder, name) + + for table in doc.tables: + for row in table.rows: + for cell in row.cells: + if placeholder in cell.text: + for p in cell.paragraphs: + for r in p.runs: + if placeholder in r.text: + r.text = r.text.replace(placeholder, name) + + # Save output + safe_name = sanitize_filename(name) + if output_format == "Word": + output_path = os.path.join(temp_dir, f"output_{safe_name}.docx") + doc.save(output_path) + results.append(output_path) + else: + word_path = os.path.join(temp_dir, f"temp_{safe_name}.docx") + pdf_path = os.path.join(temp_dir, f"output_{safe_name}.pdf") + doc.save(word_path) + + if convert_to_pdf(word_path, pdf_path): + results.append(pdf_path) + os.unlink(word_path) + + except Exception as e: + st.error(f"Error processing {name}: {str(e)}") + + progress_bar.empty() + + if results: + # Create download package + zip_buffer = BytesIO() + with ZipFile(zip_buffer, "w") as zipf: + for file_path in results: + with open(file_path, "rb") as f: + ext = ".pdf" if output_format == "PDF" else ".docx" + name = os.path.basename(file_path).replace("temp_", "").replace("output_", "") + zipf.writestr(f"document_{name}{ext}", f.read()) + + # Show results + st.success(f"Generated {len(results)} documents") + st.download_button( + "📥 Download All", + data=zip_buffer.getvalue(), + file_name=f"documents_{output_format.lower()}.zip", + mime="application/zip" + ) + + # Preview first PDF + if output_format == "PDF" and results: + with open(results[0], "rb") as f: + st.subheader("First Document Preview") + base64_pdf = base64.b64encode(f.read()).decode('utf-8') + st.markdown( + f'', + unsafe_allow_html=True + ) diff --git a/webpages/cis_tech_challenge_pages/__pycache__/cis_tech_challenge_homepage.cpython-312.pyc b/webpages/cis_tech_challenge_pages/__pycache__/cis_tech_challenge_homepage.cpython-312.pyc index 483d219db..aa96f2fe4 100644 Binary files a/webpages/cis_tech_challenge_pages/__pycache__/cis_tech_challenge_homepage.cpython-312.pyc and b/webpages/cis_tech_challenge_pages/__pycache__/cis_tech_challenge_homepage.cpython-312.pyc differ diff --git a/webpages/coreteks_pages/__pycache__/coreteks_homepage.cpython-312.pyc b/webpages/coreteks_pages/__pycache__/coreteks_homepage.cpython-312.pyc index 25790cb96..0721cee8d 100644 Binary files a/webpages/coreteks_pages/__pycache__/coreteks_homepage.cpython-312.pyc and b/webpages/coreteks_pages/__pycache__/coreteks_homepage.cpython-312.pyc differ diff --git a/webpages/coreteks_pages/coreteks_homepage.py b/webpages/coreteks_pages/coreteks_homepage.py index d24545bc7..7a522cafe 100644 --- a/webpages/coreteks_pages/coreteks_homepage.py +++ b/webpages/coreteks_pages/coreteks_homepage.py @@ -2,9 +2,9 @@ import streamlit as st def coreteks_homepage(): # load pythonx logo - st.image("./images/CoreTeksPicture.png") + st.image("./images/CoreTeksPicture.png", use_container_width =True) - st.header(" :clap: Welcome to CoreTeks") + st.subheader(" :clap: Welcome to CoreTeks") st.markdown( """ @@ -18,18 +18,30 @@ def coreteks_homepage(): st.divider() - st.header(" :paperclip: CoreTeks Presentations") - # for streamlit talk - st.subheader(" :one: Introduction to Streamlit") - st.image("./images/11-05-2024-Streamlit.jpg", width= 700) - st.link_button(label="Watch Video Recording", use_container_width=True, type="primary", url="https://wtamu0-my.sharepoint.com/:v:/g/personal/czhang_wtamu_edu/EZBXalcLWaxHquMkZxOWXz8BV2yBo_A1OURZin0ZM0XliQ?nav=eyJyZWZlcnJhbEluZm8iOnsicmVmZXJyYWxBcHAiOiJPbmVEcml2ZUZvckJ1c2luZXNzIiwicmVmZXJyYWxBcHBQbGF0Zm9ybSI6IldlYiIsInJlZmVycmFsTW9kZSI6InZpZXciLCJyZWZlcnJhbFZpZXciOiJNeUZpbGVzTGlua0NvcHkifX0&e=SJD0A4", ) - st.divider() + st.subheader(" :paperclip: CoreTeks Presentations") + with st.expander("**:one: 11/05/2024: Introduction to Streamlit**"): + # for streamlit talk + + st.image("./images/11-05-2024-Streamlit.jpg", use_container_width =True) + st.link_button(label="Watch Video Recording", use_container_width=True, type="primary", url="https://wtamu0-my.sharepoint.com/:v:/g/personal/czhang_wtamu_edu/EZBXalcLWaxHquMkZxOWXz8BV2yBo_A1OURZin0ZM0XliQ?nav=eyJyZWZlcnJhbEluZm8iOnsicmVmZXJyYWxBcHAiOiJPbmVEcml2ZUZvckJ1c2luZXNzIiwicmVmZXJyYWxBcHBQbGF0Zm9ybSI6IldlYiIsInJlZmVycmFsTW9kZSI6InZpZXciLCJyZWZlcnJhbFZpZXciOiJNeUZpbGVzTGlua0NvcHkifX0&e=SJD0A4", ) + st.divider() + + with st.expander("**:two: 11/19/2024: Linux Security and Web Server Setup**"): # for linux talk - st.subheader(" :two: Linux Security and Web Server Setup") - st.image("./images/11-19-2024-CaddyAndLinux.jpg", width= 700) - st.link_button(label="Watch Video Recording", use_container_width=True, type="primary", url="https://wtamu0-my.sharepoint.com/:v:/g/personal/czhang_wtamu_edu/EdgZHrnkqihNrOZGYAT6O2MBSRBOBMv3czD_uIE21KgsWw?nav=eyJyZWZlcnJhbEluZm8iOnsicmVmZXJyYWxBcHAiOiJPbmVEcml2ZUZvckJ1c2luZXNzIiwicmVmZXJyYWxBcHBQbGF0Zm9ybSI6IldlYiIsInJlZmVycmFsTW9kZSI6InZpZXciLCJyZWZlcnJhbFZpZXciOiJNeUZpbGVzTGlua0NvcHkifX0&e=obIoyY", ) - st.link_button(label="Download PDF Instruction", use_container_width=True, type="primary", url="https://wtamu0-my.sharepoint.com/:b:/g/personal/czhang_wtamu_edu/Eaygqoc_FqxNgYj3BlEOXhsBzOj-BWTbNKwkpJEYSDBwgA?e=K7qxSU", ) - st.divider() + # st.subheader(" :two: Linux Security and Web Server Setup") + st.image("./images/11-19-2024-CaddyAndLinux.jpg", use_container_width =True) + st.link_button(label="Watch Video Recording", use_container_width=True, type="primary", url="https://wtamu0-my.sharepoint.com/:v:/g/personal/czhang_wtamu_edu/EdgZHrnkqihNrOZGYAT6O2MBSRBOBMv3czD_uIE21KgsWw?nav=eyJyZWZlcnJhbEluZm8iOnsicmVmZXJyYWxBcHAiOiJPbmVEcml2ZUZvckJ1c2luZXNzIiwicmVmZXJyYWxBcHBQbGF0Zm9ybSI6IldlYiIsInJlZmVycmFsTW9kZSI6InZpZXciLCJyZWZlcnJhbFZpZXciOiJNeUZpbGVzTGlua0NvcHkifX0&e=obIoyY", ) + st.link_button(label="Download PDF Instruction", use_container_width=True, type="primary", url="https://wtamu0-my.sharepoint.com/:b:/g/personal/czhang_wtamu_edu/Eaygqoc_FqxNgYj3BlEOXhsBzOj-BWTbNKwkpJEYSDBwgA?e=K7qxSU", ) + st.divider() + + with st.expander("**:three: 02/05/2025: AI101: An Introduction to AI in Business**"): + st.image("./images/02-05-2025-AI101.jpg", use_container_width =True) + st.link_button(label="Watch Video Recording", use_container_width=True, type="primary", url="https://discord.com/channels/1015379966780780655/1151982114825318400/1337853863176179843", ) + st.link_button(label="Download PDF Instruction", use_container_width=True, type="primary", url="https://discord.com/channels/1015379966780780655/1151982114825318400/1337853863176179843", ) + st.divider() + + with st.expander("**:four: 04/08/2025: The State of Web Development in 2025​**"): + st.image("./images/04-08-2025-ModernWebDevelopment.jpg", use_container_width =True) diff --git a/webpages/navigation.py b/webpages/navigation.py index 9242b6ede..bb35daf81 100644 --- a/webpages/navigation.py +++ b/webpages/navigation.py @@ -14,6 +14,9 @@ def navigation_bar(): page_label = sac.menu([ sac.MenuItem('Homepage', icon='house'), sac.MenuItem('BuffBot', icon='robot'), + sac.MenuItem('BuffTools', icon='boxes', children=[ + sac.MenuItem('Letter Generator', icon='bi bi-file-word'), + ]), sac.MenuItem('Outstanding Members', icon='award'), sac.MenuItem("Join Us", icon='person-add'), sac.MenuItem('BuffTeks Project', icon='bi bi-laptop'), diff --git a/webpages/pythonx_lessons_pages/__pycache__/pythonx_finance.cpython-312.pyc b/webpages/pythonx_lessons_pages/__pycache__/pythonx_finance.cpython-312.pyc index d64ef1566..e3edc773d 100644 Binary files a/webpages/pythonx_lessons_pages/__pycache__/pythonx_finance.cpython-312.pyc and b/webpages/pythonx_lessons_pages/__pycache__/pythonx_finance.cpython-312.pyc differ diff --git a/webpages/pythonx_lessons_pages/__pycache__/pythonx_geomap.cpython-312.pyc b/webpages/pythonx_lessons_pages/__pycache__/pythonx_geomap.cpython-312.pyc index 6bd89b323..bfb4326ec 100644 Binary files a/webpages/pythonx_lessons_pages/__pycache__/pythonx_geomap.cpython-312.pyc and b/webpages/pythonx_lessons_pages/__pycache__/pythonx_geomap.cpython-312.pyc differ diff --git a/webpages/pythonx_lessons_pages/__pycache__/pythonx_homepage.cpython-312.pyc b/webpages/pythonx_lessons_pages/__pycache__/pythonx_homepage.cpython-312.pyc index 07a2d5286..a16872bf9 100644 Binary files a/webpages/pythonx_lessons_pages/__pycache__/pythonx_homepage.cpython-312.pyc and b/webpages/pythonx_lessons_pages/__pycache__/pythonx_homepage.cpython-312.pyc differ diff --git a/webpages/pythonx_lessons_pages/__pycache__/pythonx_introduction.cpython-312.pyc b/webpages/pythonx_lessons_pages/__pycache__/pythonx_introduction.cpython-312.pyc index 33e0416c4..fb869cb00 100644 Binary files a/webpages/pythonx_lessons_pages/__pycache__/pythonx_introduction.cpython-312.pyc and b/webpages/pythonx_lessons_pages/__pycache__/pythonx_introduction.cpython-312.pyc differ diff --git a/webpages/pythonx_lessons_pages/__pycache__/pythonx_wordcloud.cpython-312.pyc b/webpages/pythonx_lessons_pages/__pycache__/pythonx_wordcloud.cpython-312.pyc index 3b3d00e29..cd22346ee 100644 Binary files a/webpages/pythonx_lessons_pages/__pycache__/pythonx_wordcloud.cpython-312.pyc and b/webpages/pythonx_lessons_pages/__pycache__/pythonx_wordcloud.cpython-312.pyc differ