# Project export: VoteSmart

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: TreeHacks 2025
- Tagline: Empowering your voice with AI-driven voting guidance that aligns with your values.
- Devpost: https://devpost.com/software/votesmart-548672
- GitHub: https://github.com/yaycobparker/VoteSmart.git
- Video: https://www.youtube.com/embed/6LmEOHNUUVA?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 1 GitHub contributor(s) — Jacob Parker (1 commits)

## Devpost submission (written by the team)

### Inspiration

In 2024, during the General Election, thousands of Santa Clara County residents (including us!) received a ballot and the Santa Clara County Voter Information Guide—a 44-page long rundown of each candidate and ballot measure. Critical to making informed voting decisions, this guide presents lengthy, densely worded explanations of ballot initiatives—many of which have significant implications for local communities. As students who are now voters, we have heard from friends and other adults that their struggles with the voter guide can lead to confusion or distress, a lack of clarity on what to vote for, and decreased enthusiasm for civic engagement and the election process. Furthermore, the phenomenon of “undervoting” can be exacerbated by this confusion. In the 2024 General Election, counties in the United States had up to 9% undervoting on various propositions, from tiny Alpine County in California to the much more sizable Harris County in Texas. In each of these cases, the outcomes of various races may have changed had there been fewer ‘undervoters’. For this project, we have decided to begin our focus right here in Santa Clara County, as we already have a young and inexperienced (and frankly, impatient) voter base yet to be tapped. We also zeroed in on one piece of voting often neglected by ‘undervoters’: ballot measures. These pieces of legislation are key sticking points for voters, and their complexity often leads to disengagement—yet they shape policies that directly impact communities. By addressing this critical gap, we aim to empower voters with the knowledge and confidence needed to navigate ballot measures effectively, recognizing that their participation in these decisions is fundamental to a truly representative democracy.

### What it does

VoteSmart is an AI companion built to understand a user’s political leanings and assist in the voting process. Our mission is to increase voter awareness with a transparent and comprehensive AI that eliminates the verbose terminology often found in a Voter Handbook. By leveraging AI to cultivate a culture of deeper engagement in the democratic process, we aim to empower voters with greater autonomy while restoring trust among those disillusioned by an often needlessly complex voting system. Once their profile is saved with their political preferences, a user can immediately view their customized ballot, featuring propositions relevant to their city. Most importantly, artificial intelligence tailors ballot measure descriptions to their political leanings, offering a straightforward Yes or No voting recommendation. No more falling prey to circular language, dense jargon, or skipping votes out of frustration. VoteSmart encourages participation while giving users the autonomy to learn more about what their vote really means.

### How we built it

We used TreeHacks' website starter repo as a lifting-off point, then added our own functionality and style. We recognized immediately that a website would be the most accessible manner to reach our users and create an effective tool to serve them. We used Windsurf to fill gaps in our technical skills, and develop a functional base we could build off of. We also drew inspiration from the Depolarizing GPT framework, which seeks to reduce political polarization by offering a multi-perspective approach to political issues.

### Challenges we ran into

As first time hackathon attendees, building our own application (from scratch!) was extremely intimidating, and we had to teach ourselves many complex coding concepts as we built our site. Using Windsurf was also a learning curve, as we discovered how to tailor our prompts and find the most efficient ways to use the program, while still incorporating our own technical skills and stylistic choices.

### What we learned

We now know how to create a website from scratch, run it locally with Flask, and iteratively improve our UI to provide the best user experience. We also learned about NLTK for language processing, as well as how to use Github to streamline our code commits.

### What's next

Our plan is to expand our application to counties across the country, include political candidates in addition to propositions, and offer a wider variety of languages. We currently have a version of VoteSmart that uses OpenAI to more comprehensively offer voting recommendations to users, and would like to obtain an API key to fully implement this in the future. It’s all about inclusion, education, and engagement! And until then, we hope you VoteSmart!

## README (from the GitHub repository)

# VoteSmartV2

An enhanced version of VoteSmart that helps voters make informed decisions about ballot propositions using AI analysis.

## Features

- User authentication and profile management
- Political stance configuration (Economic, Social, Environmental, Foreign Policy)
- AI-powered ballot proposition analysis
- Modern, responsive UI
- Enhanced security and error handling
- OpenAI GPT-4 integration

## Setup

1. Create a virtual environment:
```bash
python -m venv venv
```

2. Activate the virtual environment:
- Windows: `.\venv\Scripts\activate`
- Unix/MacOS: `source venv/bin/activate`

3. Install dependencies:
```bash
pip install -r requirements.txt
```

4. Set up environment variables in `.env`:
```
FLASK_APP=app.py
FLASK_ENV=development
FLASK_DEBUG=1
SECRET_KEY=your-secret-key
OPENAI_API_KEY=your-openai-api-key
```

5. Initialize the database:
```bash
flask db upgrade
```

6. Run the application:
```bash
python app.py
```

## Usage

1. Register for an account
2. Configure your political stances in your profile
3. Submit ballot propositions for AI analysis
4. Receive personalized voting recommendations based on your political profile

## Security Note

Never commit your `.env` file or expose your API keys. Make sure to keep your SECRET_KEY and OPENAI_API_KEY secure.

## Project Structure

- `/app` - Main application directory
  - `/models` - Database models
  - `/routes` - Application routes
  - `/templates` - HTML templates
  - `/static` - CSS, JavaScript, and other static files


## Detected evidence (automated analysis)

Indexed codebase: 36 recognized source files, 135 KB.
- CSS (language) — detected in the code
- Flask (technology) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- Python (language) — detected in the code
- OpenAI (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (51 of 51)

```
.env
app.log
app.py
babel.cfg
extensions.py
init_db.py
instance/votesmart.db
messages.pot
migrations/alembic.ini
migrations/env.py
migrations/README
migrations/revert_address_fields.py
migrations/sample_propositions.py
migrations/script.py.mako
migrations/versions/25f093f93ec7_add_status_fields_to_propositions.py
migrations/versions/4580f9093b7a_add_address_fields_to_user_model.py
migrations/versions/4d7bd25d8dcd_add_city_field_to_user_model.py
migrations/versions/80c68dbc9230_add_source_url_to_propositions.py
migrations/versions/acf349871474_add_is_admin_field_to_user_model.py
models/__init__.py
models/political_compass.py
models/proposition.py
models/user.py
populate_measures.py
README.md
requirements.txt
routes/__init__.py
routes/auth.py
routes/ballot.py
routes/main.py
scrapers/__init__.py
scrapers/ballot_measures.py
services/ballot_matcher.py
stance_calculation.log
static/css/style.css
static/js/profile.js
templates/404.html
templates/500.html
templates/auth/login.html
templates/auth/register.html
templates/ballot.html
templates/base.html
templates/index.html
templates/profile.html
translations/es/LC_MESSAGES/messages.mo
translations/es/LC_MESSAGES/messages.po
translations/messages.pot
translations/zh/LC_MESSAGES/messages.mo
translations/zh/LC_MESSAGES/messages.po
update_measures.py
update_propositions.py
```

### Dependencies

- requirements.txt: email-validator@==2.1.0.post1, flask@==3.0.0, flask-babel@==4.0.0, flask-login@==0.6.3, flask-migrate@==4.0.5, flask-sqlalchemy@==3.1.1, flask-wtf@==1.2.1, nltk@==3.8.1, python-dotenv@==1.0.0, sqlalchemy@==2.0.38, werkzeug@==3.0.1

### Recent commits (newest first)

- Initial commit

## Key source files (fetched from GitHub, selected and truncated for size)

### requirements.txt

```
flask==3.0.0
flask-sqlalchemy==3.1.1
flask-login==0.6.3
flask-migrate==4.0.5
flask-wtf==1.2.1
flask-babel==4.0.0
python-dotenv==1.0.0
email-validator==2.1.0.post1
werkzeug==3.0.1
nltk==3.8.1
sqlalchemy==2.0.38

```

### app.py

```python
from flask import Flask, render_template, request, session, g
from dotenv import load_dotenv
from extensions import db, migrate, login_manager
from models.user import User
from flask_babel import Babel
import os
import logging
import sys

# Configure logging
logging.basicConfig(
    level=logging.DEBUG,
    format='%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]',
    handlers=[
        logging.StreamHandler(sys.stdout),
        logging.FileHandler('app.log')
    ]
)
logger = logging.getLogger(__name__)

# Load environment variables
load_dotenv()

babel = Babel()

def get_locale():
    # Get locale from user preference in session, fallback to browser settings
    if 'language' in session:
        return session['language']
    return request.accept_languages.best_match(['en', 'es', 'zh'])

def create_app():
    try:
        app = Flask(__name__)
        
        # Set up logging to show all output
        app.logger.setLevel(logging.DEBUG)
        # Add a stream handler to show logs in the console
        console_handler = logging.StreamHandler()
        console_handler.setLevel(logging.DEBUG)
        formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
        console_handler.setFormatter(formatter)
        app.logger.addHandler(console_handler)
        
        # Basic configuration
        app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', 'dev-key-change-this')
        app.config['DEBUG'] = os.getenv('FLASK_DEBUG', '0') == '1'
        
        # Babel configuration
        app.config['BABEL_DEFAULT_LOCALE'] = 'en'
        app.config['BABEL_SUPPORTED_LOCALES'] = ['en', 'es', 'zh']
        babel.init_app(app, locale_selector=get_locale)
        
        @app.before_request
        def before_request():
            g.lang_code = get_locale()
        
        # Ensure instance directory exists
        instance_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'instance')
        os.makedirs(instance_path, exist_ok=True)
        logger.info(f'Instance path: {instance_path}')
        
        # Database configuration
        db_path = os.path.join(instance_path, "votesmart.db")
        app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{db_path}'
        app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
        logger.info(f'Database path: {db_path}')

        # Initialize extensions
        db.init_app(app)
        migrate.init_app(app, db)
        
        # Configure login
        login_manager.init_app(app)
        login_manager.login_view = 'auth.login'
        login_manager.login_message_category = 'info'

        @login_manager.user_loader
        def load_user(id):
            try:
                return User.query.get(int(id))
            except Exception as e:
                logger.error(f'Error loading user: {str(e)}')
                return None

        # Register error handlers
        @app.errorhandler(404)
        def not_found_error(error):
            return render_template('404.html'), 404

        @app.errorhandler(500)
        def internal_error(error):
            db.session.rollback()
            logger.error(f'Internal server error: {str(error)}')
            return render_template('500.html'), 500

        with app.app_context():
            try:
                # Register blueprints
                from routes.auth import auth_bp
                from routes.main import main_bp
                from routes.ballot import bp as ballot_bp

                app.register_blueprint(auth_bp)
                app.register_blueprint(main_bp)
                app.register_blueprint(ballot_bp)

                # Create database tables
                db.create_all()
                logger.info('Database tables created successfully')
                
            except Exception as e:
                logger.error(f'Error during app initialization: {str(e)}')
                raise

        return app

    except Exception as e:
        logger.error(f'Error creating app: {str(e)}')
        raise

app = create_app()

if __name__ == '__main__':
    logger.info('Starting server on port 8000...')
    app.run(host='0.0.0.0', port=8000, debug=True)

```

### routes/main.py

```python
from flask import Blueprint, render_template, request, jsonify, redirect, url_for, current_app, flash, session
from flask_login import login_required, current_user
from models.user import User, SANTA_CLARA_CITIES
from models.political_compass import POLITICAL_COMPASS_QUESTIONS, calculate_stance
from models.proposition import Proposition
from extensions import db
import nltk
from nltk.tokenize import word_tokenize, sent_tokenize
from nltk.corpus import stopwords
from collections import defaultdict
import re
import os
import traceback
from flask_babel import _
from datetime import datetime

main_bp = Blueprint('main', __name__)

# Political stance keywords
POLITICAL_KEYWORDS = {
    'economic': {
        'liberal': ['tax', 'spending', 'welfare', 'regulation', 'subsidy', 'public funding'],
        'conservative': ['tax cut', 'private sector', 'free market', 'deregulation', 'fiscal responsibility']
    },
    'social': {
        'liberal': ['equality', 'rights', 'diversity', 'inclusion', 'reform'],
        'conservative': ['traditional', 'family values', 'law and order', 'personal responsibility']
    },
    'environmental': {
        'liberal': ['climate change', 'renewable', 'environmental protection', 'emissions', 'conservation'],
        'conservative': ['energy independence', 'nuclear', 'balanced approach', 'economic impact']
    },
    'foreign_policy': {
        'liberal': ['diplomacy', 'international cooperation', 'humanitarian', 'peace'],
        'conservative': ['strong defense', 'national security', 'sovereignty', 'military']
    }
}

def ensure_nltk_data():
    """Ensure NLTK data is downloaded to a writable location."""
    try:
        # Set NLTK data path to a writable location
        nltk_data_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'nltk_data')
        os.makedirs(nltk_data_dir, exist_ok=True)
        nltk.data.path.append(nltk_data_dir)
        
        # Download required NLTK data
        for resource in ['punkt', 'stopwords']:
            try:
                nltk.data.find(f'tokenizers/{resource}' if resource == 'punkt' else f'corpora/{resource}')
                current_app.logger.info(f'Found NLTK resource: {resource}')
            except LookupError:
                current_app.logger.info(f'Downloading NLTK resource: {resource}')
                nltk.download(resource, download_dir=nltk_data_dir)
    except Exception as e:
        current_app.logger.error(f'Error downloading NLTK data: {str(e)}\n{traceback.format_exc()}')

def analyze_text(text, user_stances):
    try:
        current_app.logger.info('Starting text analysis')
        current_app.logger.debug(f'Input text: {text[:100]}...')
        
        # Tokenize the text
        sentences = sent_tokenize(text.lower())
        current_app.logger.debug(f'Tokenized into {len(sentences)} sentences')
        
        # Initialize stance scores
        stance_scores = defaultdict(lambda: {'liberal': 0, 'conservative': 0})
        
        # Analyze each sentence for keyword matches
        for sentence in sentences:
            for stance_type, keywords in POLITICAL_KEYWORDS.items():
                for ideology, terms in keywords.items():
                    for term in terms:
                        if term.lower() in sentence:
                            stance_scores[stance_type][ideology] += 1
                            current_app.logger.debug(f'Found {ideology} {stance_type} keyword: {term}')
        
        current_app.logger.debug(f'Stance scores: {dict(stance_scores)}')
        
        # Generate recommendation based on user's stances and text analysis
        recommendation = generate_recommendation(stance_scores, user_stances)
        current_app.logger.info('Analysis completed successfully')
        
        return recommendation
    except Exception as e:
        error_msg = f'Error in analyze_text: {str(e)}\n{traceback.format_exc()}'
        current_app.logger.error(error_msg)
        return error_msg

def generate_recommendation(stance_scores, user_stances):
    try:
        current_app.logger.info('Generating recommendation')
        overall_alignment = 0
        explanations = []
        
        # Map user stance scores (1-10) to a liberal-conservative spectrum
        stance_weights = {
            'economic': user_stances.economic_stance,
            'social': user_stances.social_stance,
            'environmental': user_stances.environmental_stance,
            'foreign_policy': user_stances.foreign_policy_stance
        }
        current_app.logger.debug(f'User stances: {stance_weights}')
        
        for stance_type, scores in stance_scores.items():
            user_score = stance_weights.get(stance_type, 5)
            user_leaning = 'conservative' if user_score > 5 else 'liberal'
            
            # Calculate which ideology has more keyword matches
            text_leaning = 'conservative' if scores['conservative'] > scores['liberal'] else 'liberal'
            
            current_app.logger.debug(f'{stance_type}: user={user_leaning}, text={text_leaning}')
            
            # Add to overall alignment if the text's leaning matches the user's leaning
            if user_leaning == text_leaning:
                overall_alignment += 1
            
            # Generate explanation for this stance type
            if scores['liberal'] > 0 or scores['conservative'] > 0:
                explanation = f"\n{stance_type.replace('_', ' ').title()} Analysis:"
                explanation += f"\n- Found {scores['liberal']} liberal and {scores['conservative']} conservative keywords"
                explanation += f"\n- This aligns with your {user_leaning} stance" if user_leaning == text_leaning else \
                              f"\n- This differs from your {user_leaning} stance"
                explanations.append(explanation)
        
        # Calculate final recommendation
        recommendation = "Yes" if overall_alignment >= 2 else "No"
        
      
[truncated — 28840 more characters]
```

### extensions.py

```python
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager
from flask_migrate import Migrate

db = SQLAlchemy()
migrate = Migrate()
login_manager = LoginManager()
login_manager.login_view = 'auth.login'

```

### update_measures.py

```python
from app import create_app
from scrapers.ballot_measures import BallotMeasureScraper

app = create_app()
with app.app_context():
    scraper = BallotMeasureScraper()
    success = scraper.update_all_measures()
    print(f"Update {'succeeded' if success else 'failed'}")

```

### init_db.py

```python
from app import app, db
from migrations.sample_propositions import add_sample_propositions

def init_db():
    with app.app_context():
        # Create all tables
        db.create_all()
        
        # Add sample propositions
        add_sample_propositions()
        
        print("Database initialized with sample propositions!")

if __name__ == '__main__':
    init_db()

```

### update_propositions.py

```python
from app import app
from scrapers.ballot_measures import BallotMeasureScraper
import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

def update_propositions():
    """Update ballot propositions from official sources."""
    with app.app_context():
        scraper = BallotMeasureScraper()
        success = scraper.update_all_measures()
        
        if success:
            logger.info("Successfully updated ballot propositions")
        else:
            logger.error("Failed to update ballot propositions")

if __name__ == '__main__':
    update_propositions()

```

### populate_measures.py

```python
from flask import Flask
from extensions import db
from scrapers.ballot_measures import BallotMeasureScraper
import logging
import os

# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

def create_minimal_app():
    app = Flask(__name__)
    
    # Ensure instance directory exists
    instance_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'instance')
    os.makedirs(instance_path, exist_ok=True)
    
    # Database configuration
    db_path = os.path.join(instance_path, "votesmart.db")
    app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{db_path}'
    app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
    
    db.init_app(app)
    return app

if __name__ == '__main__':
    app = create_minimal_app()
    with app.app_context():
        # Create database tables
        db.create_all()
        
        logger.info("Starting ballot measure scraper...")
        scraper = BallotMeasureScraper()
        success = scraper.update_all_measures()
        if success:
            logger.info("Successfully updated ballot measures")
        else:
            logger.error("Failed to update ballot measures")

```

### models/__init__.py

```python


```

### routes/__init__.py

```python


```

[26 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]