Files
balanceboard/models.py
chelsea 94ffa69d21 Fix Issue #18: Community settings now match admin panel configuration
## Problem Fixed:
Community selection in settings was using hardcoded list that didn't match the actual enabled communities in the admin panel's collection_targets configuration.

## Root Cause:
The settings_communities() function had a hardcoded list of only 6 communities, while platform_config.json defines many more communities and collection_targets specifies which ones are actually enabled.

## Solution:
- **Dynamic community loading** - Reads from platform_config.json instead of hardcoded list
- **Collection target filtering** - Only shows communities that are in collection_targets (actually being crawled)
- **Complete community data** - Includes display_name, icon, and description from platform config
- **Platform consistency** - Ensures settings match what's configured in admin panel

The community settings now perfectly reflect what's enabled in the admin panel\!

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-12 03:26:50 -05:00

249 lines
8.9 KiB
Python

"""
Database Models
SQLAlchemy models for the application.
"""
import uuid
from datetime import datetime
from flask_login import UserMixin
from flask_bcrypt import Bcrypt
from database import db
# Initialize bcrypt
bcrypt = Bcrypt()
class User(UserMixin, db.Model):
"""User model with bcrypt password hashing"""
__tablename__ = 'users'
# Primary fields
id = db.Column(db.String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
username = db.Column(db.String(80), unique=True, nullable=False, index=True)
email = db.Column(db.String(120), unique=True, nullable=False, index=True)
password_hash = db.Column(db.String(128), nullable=True) # Nullable for OAuth users
# OAuth fields
auth0_id = db.Column(db.String(255), unique=True, nullable=True, index=True)
# User attributes
is_admin = db.Column(db.Boolean, default=False, nullable=False)
is_active = db.Column(db.Boolean, default=True, nullable=False)
# Profile
profile_picture_url = db.Column(db.String(255), nullable=True)
# Timestamps
created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
last_login = db.Column(db.DateTime, nullable=True)
# User settings (JSON stored as text)
settings = db.Column(db.Text, default='{}')
# Password reset
reset_token = db.Column(db.String(100), nullable=True, unique=True, index=True)
reset_token_expiry = db.Column(db.DateTime, nullable=True)
def __init__(self, username, email, password=None, is_admin=False, auth0_id=None):
"""
Initialize a new user.
Args:
username: Unique username
email: Unique email address
password: Plain text password (will be hashed) - optional for OAuth users
is_admin: Whether user is admin (default False)
auth0_id: Auth0 user ID for OAuth users (optional)
"""
# Validate inputs
if not username or not isinstance(username, str) or len(username) > 80:
raise ValueError("Invalid username")
if not email or not isinstance(email, str) or len(email) > 120:
raise ValueError("Invalid email")
if password is not None and (not isinstance(password, str) or len(password) < 1):
raise ValueError("Invalid password")
if password is None and auth0_id is None:
raise ValueError("Either password or auth0_id must be provided")
self.id = str(uuid.uuid4())
self.username = username.strip()
self.email = email.strip().lower()
self.auth0_id = auth0_id
if password:
self.set_password(password)
else:
self.password_hash = None # OAuth users don't have passwords
self.is_admin = bool(is_admin)
self.is_active = True
self.created_at = datetime.utcnow()
def set_password(self, password):
"""
Hash and set user password using bcrypt.
Args:
password: Plain text password
"""
self.password_hash = bcrypt.generate_password_hash(password).decode('utf-8')
def check_password(self, password):
"""
Verify password against stored hash.
Args:
password: Plain text password to check
Returns:
True if password matches, False otherwise
"""
return bcrypt.check_password_hash(self.password_hash, password)
def update_last_login(self):
"""Update the last login timestamp"""
self.last_login = datetime.utcnow()
db.session.commit()
def generate_reset_token(self):
"""Generate a password reset token that expires in 1 hour"""
import secrets
from datetime import timedelta
self.reset_token = secrets.token_urlsafe(32)
self.reset_token_expiry = datetime.utcnow() + timedelta(hours=1)
db.session.commit()
return self.reset_token
def verify_reset_token(self, token):
"""Verify if the provided reset token is valid and not expired"""
if not self.reset_token or not self.reset_token_expiry:
return False
if self.reset_token != token:
return False
if datetime.utcnow() > self.reset_token_expiry:
return False
return True
def clear_reset_token(self):
"""Clear the reset token after use"""
self.reset_token = None
self.reset_token_expiry = None
db.session.commit()
def get_id(self):
"""Required by Flask-Login"""
return self.id
def __repr__(self):
return f'<User {self.username}>'
class Session(db.Model):
"""User session model for tracking active sessions"""
__tablename__ = 'user_sessions'
session_id = db.Column(db.String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
user_id = db.Column(db.String(36), db.ForeignKey('users.id'), nullable=False)
created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
expires_at = db.Column(db.DateTime, nullable=False)
# Relationship
user = db.relationship('User', backref=db.backref('sessions', lazy=True))
def __repr__(self):
return f'<Session {self.session_id} for user {self.user_id}>'
class PollSource(db.Model):
"""Source polling configuration"""
__tablename__ = 'poll_sources'
id = db.Column(db.String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
platform = db.Column(db.String(50), nullable=False, index=True) # reddit, hackernews, etc.
source_id = db.Column(db.String(100), nullable=False) # programming, python, etc.
display_name = db.Column(db.String(200), nullable=False)
# Polling configuration
enabled = db.Column(db.Boolean, default=True, nullable=False)
poll_interval_minutes = db.Column(db.Integer, default=60, nullable=False) # How often to poll
max_posts = db.Column(db.Integer, default=100, nullable=False) # Max posts per poll
fetch_comments = db.Column(db.Boolean, default=True, nullable=False) # Whether to fetch comments
priority = db.Column(db.String(20), default='medium', nullable=False) # low, medium, high
# Status tracking
last_poll_time = db.Column(db.DateTime, nullable=True)
last_poll_status = db.Column(db.String(50), nullable=True) # success, error, etc.
last_poll_error = db.Column(db.Text, nullable=True)
posts_collected = db.Column(db.Integer, default=0, nullable=False)
# Metadata
created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow, onupdate=datetime.utcnow)
created_by = db.Column(db.String(36), db.ForeignKey('users.id'), nullable=True)
# Unique constraint on platform + source_id
__table_args__ = (
db.UniqueConstraint('platform', 'source_id', name='unique_platform_source'),
)
def __repr__(self):
return f'<PollSource {self.platform}:{self.source_id}>'
class PollLog(db.Model):
"""Log of polling activities"""
__tablename__ = 'poll_logs'
id = db.Column(db.String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
source_id = db.Column(db.String(36), db.ForeignKey('poll_sources.id'), nullable=False, index=True)
started_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
completed_at = db.Column(db.DateTime, nullable=True)
status = db.Column(db.String(50), nullable=False) # running, success, error
posts_found = db.Column(db.Integer, default=0)
posts_new = db.Column(db.Integer, default=0)
posts_updated = db.Column(db.Integer, default=0)
error_message = db.Column(db.Text, nullable=True)
# Relationship
source = db.relationship('PollSource', backref=db.backref('logs', lazy='dynamic', order_by='PollLog.started_at.desc()'))
def __repr__(self):
return f'<PollLog {self.id} for source {self.source_id}>'
class Bookmark(db.Model):
"""User bookmarks for posts"""
__tablename__ = 'bookmarks'
id = db.Column(db.String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
user_id = db.Column(db.String(36), db.ForeignKey('users.id'), nullable=False, index=True)
post_uuid = db.Column(db.String(255), nullable=False, index=True) # UUID of the bookmarked post
# Optional metadata
title = db.Column(db.String(500), nullable=True) # Cached post title
platform = db.Column(db.String(50), nullable=True) # Cached platform info
source = db.Column(db.String(100), nullable=True) # Cached source info
# Timestamps
created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
# Relationships
user = db.relationship('User', backref=db.backref('bookmarks', lazy='dynamic', order_by='Bookmark.created_at.desc()'))
# Unique constraint - user can only bookmark a post once
__table_args__ = (
db.UniqueConstraint('user_id', 'post_uuid', name='unique_user_bookmark'),
)
def __repr__(self):
return f'<Bookmark {self.post_uuid} by user {self.user_id}>'