2026-05-10 09:08:12 +02:00
import dataset
import logging
2026-05-11 00:41:41 +02:00
from datetime import datetime
2026-05-10 09:08:12 +02:00
from devplacepy . config import DATABASE_URL
logger = logging . getLogger ( __name__ )
2026-05-11 03:14:43 +02:00
db = dataset . connect (
DATABASE_URL ,
engine_kwargs = {
" connect_args " : {
" timeout " : 30 ,
" check_same_thread " : False ,
} ,
} ,
on_connect_statements = [
" PRAGMA journal_mode=WAL " ,
" PRAGMA synchronous=NORMAL " ,
" PRAGMA busy_timeout=30000 " ,
" PRAGMA cache_size=-8000 " ,
" PRAGMA temp_store=MEMORY " ,
" PRAGMA mmap_size=268435456 " ,
] ,
)
def _index ( db , table , name , columns ) :
try :
if table in db . tables :
cols = " , " . join ( columns )
db . query ( f " CREATE INDEX IF NOT EXISTS { name } ON { table } ( { cols } ) " )
except Exception as e :
logger . warning ( f " Could not create index { name } on { table } : { e } " )
2026-05-10 09:08:12 +02:00
def init_db ( ) :
tables = db . tables
2026-05-11 03:14:43 +02:00
_index ( db , " users " , " idx_users_username " , [ " username " ] )
_index ( db , " users " , " idx_users_email " , [ " email " ] )
_index ( db , " posts " , " idx_posts_user_uid " , [ " user_uid " ] )
_index ( db , " posts " , " idx_posts_created_at " , [ " created_at " ] )
_index ( db , " posts " , " idx_posts_topic " , [ " topic " ] )
_index ( db , " comments " , " idx_comments_post_uid " , [ " post_uid " ] )
2026-05-11 20:49:45 +02:00
_index ( db , " comments " , " idx_comments_target " , [ " target_type " , " target_uid " ] )
2026-05-11 03:14:43 +02:00
_index ( db , " comments " , " idx_comments_user_uid " , [ " user_uid " ] )
2026-05-13 21:17:57 +02:00
_index ( db , " comments " , " idx_comments_created_at " , [ " created_at " ] )
2026-05-11 03:14:43 +02:00
_index ( db , " votes " , " idx_votes_target " , [ " target_uid " , " target_type " ] )
_index ( db , " messages " , " idx_messages_sender " , [ " sender_uid " ] )
_index ( db , " messages " , " idx_messages_receiver " , [ " receiver_uid " ] )
_index ( db , " notifications " , " idx_notifications_user " , [ " user_uid " ] )
_index ( db , " notifications " , " idx_notifications_user_read " , [ " user_uid " , " read " ] )
_index ( db , " sessions " , " idx_sessions_token " , [ " session_token " ] )
_index ( db , " projects " , " idx_projects_user " , [ " user_uid " ] )
_index ( db , " badges " , " idx_badges_user " , [ " user_uid " ] )
_index ( db , " follows " , " idx_follows_follower " , [ " follower_uid " ] )
_index ( db , " follows " , " idx_follows_following " , [ " following_uid " ] )
_index ( db , " password_resets " , " idx_password_resets_token " , [ " token " ] )
2026-05-12 15:07:34 +02:00
_index ( db , " gists " , " idx_gists_user_uid " , [ " user_uid " ] )
_index ( db , " attachments " , " idx_attachments_resource " , [ " resource_type " , " resource_uid " ] )
2026-05-13 21:17:57 +02:00
_index ( db , " attachments " , " idx_attachments_target " , [ " target_type " , " target_uid " ] )
2026-05-11 03:14:43 +02:00
2026-05-11 05:30:51 +02:00
if " site_settings " in tables :
defaults = { " site_name " : " DevPlace " , " site_description " : " The Developer Social Network " , " site_tagline " : " Track industry shifts. Discover bold releases. Share what you are building in an open, uncensored environment. " }
for key , value in defaults . items ( ) :
existing = db [ " site_settings " ] . find_one ( key = key )
if not existing :
db [ " site_settings " ] . insert ( { " uid " : f " default_ { key } " , " key " : key , " value " : value } )
2026-05-11 07:02:06 +02:00
_index ( db , " news " , " idx_news_external_id " , [ " external_id " ] )
_index ( db , " news " , " idx_news_synced_at " , [ " synced_at " ] )
2026-05-11 22:12:43 +02:00
_index ( db , " news " , " idx_news_status " , [ " status " ] )
2026-05-11 07:02:06 +02:00
_index ( db , " news_images " , " idx_news_images_news_uid " , [ " news_uid " ] )
_index ( db , " news_sync " , " idx_news_sync_external_id " , [ " external_id " ] )
2026-05-11 22:12:43 +02:00
if " news " in tables :
for article in db [ " news " ] . find ( status = None ) :
was_featured = article . get ( " featured " , 0 )
db [ " news " ] . update ( {
" uid " : article [ " uid " ] ,
" status " : " published " if was_featured else " draft " ,
} , [ " uid " ] )
for article in db [ " news " ] . find ( show_on_landing = None ) :
db [ " news " ] . update ( {
" uid " : article [ " uid " ] ,
" show_on_landing " : 0 ,
} , [ " uid " ] )
2026-05-12 12:45:52 +02:00
for article in db [ " news " ] . find ( slug = None ) :
from devplacepy . utils import make_combined_slug
slug = make_combined_slug ( article . get ( " title " , " " ) or " news " , article [ " uid " ] )
db [ " news " ] . update ( {
" uid " : article [ " uid " ] ,
" slug " : slug ,
} , [ " uid " ] )
2026-05-11 22:12:43 +02:00
if " news_sync " in tables :
for entry in db [ " news_sync " ] . find ( ) :
current = entry . get ( " status " , " " )
if current in ( " below_threshold " , " " ) :
db [ " news_sync " ] . update ( {
" id " : entry [ " id " ] ,
" status " : " graded " ,
} , [ " id " ] )
2026-05-11 07:02:06 +02:00
if " site_settings " in tables :
news_defaults = {
" news_grade_threshold " : " 7 " ,
" news_api_url " : " https://news.app.molodetz.nl/api " ,
" news_ai_url " : " https://openai.app.molodetz.nl/v1/chat/completions " ,
" news_ai_model " : " molodetz " ,
}
for key , value in news_defaults . items ( ) :
existing = db [ " site_settings " ] . find_one ( key = key )
if not existing :
db [ " site_settings " ] . insert ( { " uid " : f " default_ { key } " , " key " : key , " value " : value } )
2026-05-12 15:07:34 +02:00
upload_defaults = {
" max_upload_size_mb " : " 10 " ,
" allowed_file_types " : " " ,
" max_attachments_per_resource " : " 10 " ,
}
for key , value in upload_defaults . items ( ) :
existing = db [ " site_settings " ] . find_one ( key = key )
if not existing :
db [ " site_settings " ] . insert ( { " uid " : f " default_ { key } " , " key " : key , " value " : value } )
2026-05-10 09:08:12 +02:00
logger . info ( " Database initialized " )
def get_table ( name ) :
return db [ name ]
2026-05-11 00:41:41 +02:00
2026-05-11 03:14:43 +02:00
def get_users_by_uids ( uids ) :
if not uids :
return { }
seen = set ( )
unique = [ u for u in uids if u not in seen and not seen . add ( u ) ]
return { u [ " uid " ] : u for u in db [ " users " ] . find ( db [ " users " ] . table . columns . uid . in_ ( unique ) ) }
def get_comment_counts_by_post_uids ( post_uids ) :
if not post_uids or " comments " not in db . tables :
return { }
2026-05-12 12:45:52 +02:00
placeholders = " , " . join ( f " :p { i } " for i in range ( len ( post_uids ) ) )
params = { f " p { i } " : u for i , u in enumerate ( post_uids ) }
rows = db . query ( f " SELECT post_uid, COUNT(*) as c FROM comments WHERE post_uid IN ( { placeholders } ) GROUP BY post_uid " , * * params )
2026-05-11 03:14:43 +02:00
return { r [ " post_uid " ] : r [ " c " ] for r in rows }
def get_vote_counts ( target_uids ) :
if not target_uids or " votes " not in db . tables :
return { } , { }
2026-05-12 12:45:52 +02:00
placeholders = " , " . join ( f " :p { i } " for i in range ( len ( target_uids ) ) )
params = { f " p { i } " : u for i , u in enumerate ( target_uids ) }
rows = db . query ( f " SELECT target_uid, value, COUNT(*) as c FROM votes WHERE target_uid IN ( { placeholders } ) GROUP BY target_uid, value " , * * params )
2026-05-11 03:14:43 +02:00
ups = { }
downs = { }
for r in rows :
if r [ " value " ] == 1 :
ups [ r [ " target_uid " ] ] = r [ " c " ]
else :
downs [ r [ " target_uid " ] ] = r [ " c " ]
return ups , downs
2026-05-11 20:49:45 +02:00
def load_comments ( target_type , target_uid ) :
if " comments " not in db . tables :
return [ ]
comments_table = db [ " comments " ]
raw = list ( comments_table . find ( target_type = target_type , target_uid = target_uid , order_by = [ " created_at " ] ) )
if not raw and target_type == " post " :
raw = list ( comments_table . find ( post_uid = target_uid , order_by = [ " created_at " ] ) )
if not raw :
return [ ]
uids = [ c [ " user_uid " ] for c in raw ]
cids = [ c [ " uid " ] for c in raw ]
users = get_users_by_uids ( uids )
ups , downs = get_vote_counts ( cids )
from devplacepy . utils import time_ago
2026-05-13 21:17:57 +02:00
from devplacepy . attachments import get_attachments_batch as _gab
atts_map = _gab ( " comment " , cids ) if " attachments " in db . tables else { }
2026-05-11 20:49:45 +02:00
cmap = { }
for c in raw :
cmap [ c [ " uid " ] ] = {
" comment " : c ,
" author " : users . get ( c [ " user_uid " ] ) ,
" time_ago " : time_ago ( c [ " created_at " ] ) ,
" votes " : { " up " : ups . get ( c [ " uid " ] , 0 ) , " down " : downs . get ( c [ " uid " ] , 0 ) } ,
" children " : [ ] ,
2026-05-12 15:07:34 +02:00
" attachments " : atts_map . get ( c [ " uid " ] , [ ] ) ,
2026-05-11 20:49:45 +02:00
}
top = [ ]
for item in cmap . values ( ) :
parent = item [ " comment " ] . get ( " parent_uid " )
if parent and parent in cmap :
cmap [ parent ] [ " children " ] . append ( item )
else :
top . append ( item )
return top
2026-05-12 15:07:34 +02:00
def get_attachments ( resource_type : str , resource_uid : str ) - > list :
if " attachments " not in db . tables :
return [ ]
return list ( db [ " attachments " ] . find ( resource_type = resource_type , resource_uid = resource_uid , order_by = [ " created_at " ] ) )
def get_attachments_by_type ( resource_type : str , resource_uids : list ) - > dict :
if not resource_uids or " attachments " not in db . tables :
return { }
rows = list ( db [ " attachments " ] . find ( db [ " attachments " ] . table . columns . resource_uid . in_ ( resource_uids ) , resource_type = resource_type ) )
result = { }
for a in rows :
key = a [ " resource_uid " ]
if key not in result :
result [ key ] = [ ]
result [ key ] . append ( a )
return result
def delete_attachment_record ( uid : str ) - > None :
if " attachments " not in db . tables :
return
att = db [ " attachments " ] . find_one ( uid = uid )
if att :
_delete_attachment_file ( att . get ( " storage_path " , " " ) )
db [ " attachments " ] . delete ( id = att [ " id " ] )
def delete_attachments ( resource_type : str , resource_uid : str ) - > None :
if " attachments " not in db . tables :
return
for a in db [ " attachments " ] . find ( resource_type = resource_type , resource_uid = resource_uid ) :
_delete_attachment_file ( a . get ( " storage_path " , " " ) )
db [ " attachments " ] . delete ( resource_type = resource_type , resource_uid = resource_uid )
def _delete_attachment_file ( storage_path : str ) - > None :
if not storage_path :
return
from devplacepy . config import STATIC_DIR
file_path = STATIC_DIR / " uploads " / storage_path
try :
file_path . unlink ( missing_ok = True )
parent = file_path . parent
if parent . exists ( ) and not any ( parent . iterdir ( ) ) :
parent . rmdir ( )
grandparent = parent . parent
if grandparent . exists ( ) and not any ( grandparent . iterdir ( ) ) :
grandparent . rmdir ( )
except Exception as e :
logger . warning ( f " Failed to delete attachment file { storage_path } : { e } " )
def get_setting ( key : str , default : str = " " ) - > str :
if " site_settings " not in db . tables :
return default
entry = db [ " site_settings " ] . find_one ( key = key )
return entry [ " value " ] if entry else default
def resolve_by_slug ( table , slug ) :
entry = table . find_one ( slug = slug )
if not entry :
entry = table . find_one ( uid = slug )
return entry
def pagination_params ( page , per_page = 25 ) :
total = None
total_pages = None
return {
" page " : page ,
" per_page " : per_page ,
}
def build_pagination ( page , total , per_page = 25 ) :
total_pages = max ( 1 , __import__ ( " math " ) . ceil ( total / per_page ) )
page = max ( 1 , min ( page , total_pages ) )
return {
" page " : page ,
" per_page " : per_page ,
" total " : total ,
" total_pages " : total_pages ,
" has_prev " : page > 1 ,
" has_next " : page < total_pages ,
" prev_page " : page - 1 ,
" next_page " : page + 1 ,
}
2026-05-11 00:41:41 +02:00
def get_daily_topic ( ) :
2026-05-12 15:07:34 +02:00
if " news " in db . tables :
article = db [ " news " ] . find_one ( status = " published " , order_by = [ " -synced_at " ] )
if article :
desc = ( article . get ( " description " ) or " " ) [ : 200 ] or ( article . get ( " content " ) or " " ) [ : 200 ]
return {
" title " : article . get ( " title " , " " ) ,
" summary " : desc ,
" slug " : article . get ( " slug " , " " ) ,
" url " : article . get ( " url " , " " ) ,
}
return { " title " : " Welcome to DevPlace " , " summary " : " Stay tuned for the latest dev news. " }