2026-05-10 09:08:12 +02:00
import dataset
import logging
2026-05-30 20:16:39 +02:00
from collections import defaultdict
2026-05-23 06:20:27 +02:00
from datetime import datetime , timezone
from devplacepy . cache import TTLCache
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 " ] )
2026-05-23 10:03:27 +02:00
_index ( db , " push_registration " , " idx_push_registration_user " , [ " user_uid " ] )
2026-05-11 03:14:43 +02:00
_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 " ] )
2026-05-23 09:00:52 +02:00
_index ( db , " gists " , " idx_gists_language " , [ " language " ] )
2026-05-12 15:07:34 +02:00
_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-30 20:16:39 +02:00
_backfill_gamification ( )
2026-05-10 09:08:12 +02:00
logger . info ( " Database initialized " )
2026-05-30 20:16:39 +02:00
def _backfill_gamification ( ) :
if " users " not in db . tables :
return
from devplacepy . utils import (
level_for_xp , check_milestone_badges ,
XP_POST , XP_COMMENT , XP_PROJECT , XP_GIST , XP_UPVOTE , XP_FOLLOW ,
)
pending = list ( db [ " users " ] . find ( xp = 0 ) )
if not pending :
return
xp_by_user = defaultdict ( int )
def add_counts ( table , column , points ) :
if table not in db . tables :
return
for row in db . query ( f " SELECT { column } AS uid, COUNT(*) AS c FROM { table } GROUP BY { column } " ) :
if row [ " uid " ] :
xp_by_user [ row [ " uid " ] ] + = row [ " c " ] * points
add_counts ( " posts " , " user_uid " , XP_POST )
add_counts ( " comments " , " user_uid " , XP_COMMENT )
add_counts ( " projects " , " user_uid " , XP_PROJECT )
add_counts ( " gists " , " user_uid " , XP_GIST )
add_counts ( " follows " , " following_uid " , XP_FOLLOW )
if " votes " in db . tables :
for content_table , target_type in ( ( " posts " , " post " ) , ( " projects " , " project " ) , ( " gists " , " gist " ) , ( " comments " , " comment " ) ) :
if content_table not in db . tables :
continue
rows = db . query (
f " SELECT c.user_uid AS uid, COUNT(*) AS c "
f " FROM votes v JOIN { content_table } c ON v.target_uid = c.uid "
f " WHERE v.target_type = :t AND v.value = 1 GROUP BY c.user_uid " ,
t = target_type ,
)
for row in rows :
if row [ " uid " ] :
xp_by_user [ row [ " uid " ] ] + = row [ " c " ] * XP_UPVOTE
for user in pending :
xp = xp_by_user . get ( user [ " uid " ] , 0 )
if xp < = 0 :
continue
db [ " users " ] . update ( { " uid " : user [ " uid " ] , " xp " : xp , " level " : level_for_xp ( xp ) } , [ " uid " ] )
_authors_cache . clear ( )
for user in pending :
check_milestone_badges ( user [ " uid " ] )
logger . info ( f " Gamification backfill processed { len ( pending ) } users " )
2026-05-10 09:08:12 +02:00
def get_table ( name ) :
return db [ name ]
2026-05-11 00:41:41 +02:00
2026-05-30 20:16:39 +02:00
def _in_clause ( uids , prefix = " p " ) :
placeholders = " , " . join ( f " : { prefix } { i } " for i in range ( len ( uids ) ) )
params = { f " { prefix } { i } " : uid for i , uid in enumerate ( uids ) }
return placeholders , params
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-30 20:16:39 +02:00
placeholders , params = _in_clause ( post_uids )
2026-05-14 04:12:19 +02:00
rows = db . query ( f " SELECT target_uid, COUNT(*) as c FROM comments WHERE target_type= ' post ' AND target_uid IN ( { placeholders } ) GROUP BY target_uid " , * * params )
return { r [ " target_uid " ] : r [ " c " ] for r in rows }
2026-05-11 03:14:43 +02:00
2026-05-23 06:20:27 +02:00
def get_post_counts_by_user_uids ( user_uids ) :
if not user_uids or " posts " not in db . tables :
return { }
2026-05-30 20:16:39 +02:00
placeholders , params = _in_clause ( user_uids )
2026-05-23 06:20:27 +02:00
rows = db . query ( f " SELECT user_uid, COUNT(*) as c FROM posts WHERE user_uid IN ( { placeholders } ) GROUP BY user_uid " , * * params )
return { r [ " user_uid " ] : r [ " c " ] for r in rows }
2026-05-11 03:14:43 +02:00
def get_vote_counts ( target_uids ) :
if not target_uids or " votes " not in db . tables :
return { } , { }
2026-05-30 20:16:39 +02:00
placeholders , params = _in_clause ( target_uids )
2026-05-12 12:45:52 +02:00
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-30 20:16:39 +02:00
def get_user_votes ( user_uid , target_uids ) :
if not user_uid or not target_uids or " votes " not in db . tables :
return { }
placeholders , params = _in_clause ( target_uids )
params [ " uid " ] = user_uid
rows = db . query ( f " SELECT target_uid, value FROM votes WHERE user_uid = :uid AND target_uid IN ( { placeholders } ) " , * * params )
return { r [ " target_uid " ] : r [ " value " ] for r in rows }
def load_comments ( target_type , target_uid , user = None ) :
2026-05-11 20:49:45 +02:00
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 )
2026-05-30 20:16:39 +02:00
my_votes = get_user_votes ( user [ " uid " ] , cids ) if user else { }
2026-05-11 20:49:45 +02:00
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 ) } ,
2026-05-30 20:16:39 +02:00
" my_vote " : my_votes . get ( c [ " uid " ] , 0 ) ,
2026-05-11 20:49:45 +02:00
" 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
2026-05-23 06:20:27 +02:00
def get_news_images_by_uids ( news_uids : list ) - > dict :
if not news_uids or " news_images " not in db . tables :
return { }
images_table = db [ " news_images " ]
rows = images_table . find ( images_table . table . columns . news_uid . in_ ( news_uids ) , order_by = [ " uid " ] )
result = { }
for r in rows :
result . setdefault ( r [ " news_uid " ] , r [ " url " ] )
return result
2026-05-12 15:07:34 +02:00
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 } " )
2026-05-23 06:20:27 +02:00
_settings_cache = TTLCache ( ttl = 60 )
2026-05-12 15:07:34 +02:00
def get_setting ( key : str , default : str = " " ) - > str :
2026-05-23 06:20:27 +02:00
cached = _settings_cache . get ( key )
if cached is not None :
return cached
2026-05-12 15:07:34 +02:00
if " site_settings " not in db . tables :
return default
entry = db [ " site_settings " ] . find_one ( key = key )
2026-05-23 06:20:27 +02:00
if entry is None :
return default
_settings_cache . set ( key , entry [ " value " ] )
return entry [ " value " ]
2026-05-23 06:55:11 +02:00
def get_int_setting ( key : str , default : int ) - > int :
raw = get_setting ( key , str ( default ) )
try :
return int ( raw )
except ( TypeError , ValueError ) :
return default
2026-05-23 06:20:27 +02:00
def clear_settings_cache ( ) - > None :
_settings_cache . clear ( )
_stats_cache = TTLCache ( ttl = 30 )
def get_site_stats ( ) - > dict :
cached = _stats_cache . get ( " site " )
if cached is not None :
return cached
today_start = datetime . now ( timezone . utc ) . replace ( hour = 0 , minute = 0 , second = 0 , microsecond = 0 ) . isoformat ( )
stats = {
" total_members " : db [ " users " ] . count ( ) if " users " in db . tables else 0 ,
" posts_today " : db [ " posts " ] . count ( created_at = { " >= " : today_start } ) if " posts " in db . tables else 0 ,
" total_projects " : db [ " projects " ] . count ( ) if " projects " in db . tables else 0 ,
" total_gists " : db [ " gists " ] . count ( ) if " gists " in db . tables else 0 ,
}
_stats_cache . set ( " site " , stats )
return stats
2026-05-12 15:07:34 +02:00
2026-05-23 09:00:52 +02:00
_gist_languages_cache = TTLCache ( ttl = 60 )
def get_gist_languages ( ) - > set [ str ] :
cached = _gist_languages_cache . get ( " codes " )
if cached is not None :
return cached
codes : set [ str ] = set ( )
if " gists " in db . tables :
for row in db . query ( " SELECT DISTINCT language FROM gists " ) :
language = row . get ( " language " )
if language :
codes . add ( language )
_gist_languages_cache . set ( " codes " , codes )
return codes
2026-05-23 10:54:45 +02:00
_STARRED_CONTENT_TABLES = ( " posts " , " projects " , " gists " )
2026-05-30 20:16:39 +02:00
_authors_cache = TTLCache ( ttl = 60 )
2026-05-23 10:54:45 +02:00
2026-05-30 20:16:39 +02:00
def _ranked_authors ( ) - > list :
cached = _authors_cache . get ( " ranked " )
2026-05-23 10:54:45 +02:00
if cached is not None :
return cached
sources = [ table for table in _STARRED_CONTENT_TABLES if table in db . tables ]
if not sources :
2026-05-30 20:16:39 +02:00
_authors_cache . set ( " ranked " , [ ] )
2026-05-23 10:54:45 +02:00
return [ ]
union = " UNION ALL " . join ( f " SELECT user_uid, stars FROM { table } " for table in sources )
rows = db . query (
f " SELECT user_uid, SUM(stars) AS total FROM ( { union } ) "
2026-05-30 20:16:39 +02:00
f " GROUP BY user_uid HAVING total > 0 ORDER BY total DESC "
2026-05-23 10:54:45 +02:00
)
ranked = [ ( row [ " user_uid " ] , row [ " total " ] ) for row in rows ]
users_map = get_users_by_uids ( [ uid for uid , _ in ranked ] )
authors = [ ]
for uid , total in ranked :
user = users_map . get ( uid )
if user :
author = dict ( user )
author [ " stars " ] = total
authors . append ( author )
2026-05-30 20:16:39 +02:00
_authors_cache . set ( " ranked " , authors )
2026-05-23 10:54:45 +02:00
return authors
2026-05-30 20:16:39 +02:00
def get_top_authors ( limit : int = 5 ) - > list :
return _ranked_authors ( ) [ : limit ]
def get_leaderboard ( limit : int = 50 , offset : int = 0 ) - > list :
sliced = _ranked_authors ( ) [ offset : offset + limit ]
leaderboard = [ ]
for position , author in enumerate ( sliced , start = offset + 1 ) :
entry = dict ( author )
entry [ " rank " ] = position
leaderboard . append ( entry )
return leaderboard
def get_user_rank ( user_uid : str ) :
for position , author in enumerate ( _ranked_authors ( ) , start = 1 ) :
if author [ " uid " ] == user_uid :
return position
return None
2026-05-23 10:54:45 +02:00
def get_user_stars ( user_uid : str ) - > int :
total = 0
for table in _STARRED_CONTENT_TABLES :
if table in db . tables :
for row in db . query ( f " SELECT COALESCE(SUM(stars), 0) AS s FROM { table } WHERE user_uid = :u " , u = user_uid ) :
total + = row [ " s " ] or 0
return total
2026-05-12 15:07:34 +02:00
def resolve_by_slug ( table , slug ) :
entry = table . find_one ( slug = slug )
if not entry :
entry = table . find_one ( uid = slug )
return entry
2026-05-25 16:16:53 +02:00
def resolve_object_url ( target_type : str , target_uid : str ) - > str :
if target_type == " post " :
post = resolve_by_slug ( get_table ( " posts " ) , target_uid )
return f " /posts/ { post [ ' slug ' ] or post [ ' uid ' ] } " if post else " /feed "
if target_type == " project " :
project = resolve_by_slug ( get_table ( " projects " ) , target_uid )
return f " /projects/ { project [ ' slug ' ] or project [ ' uid ' ] } " if project else " /projects "
if target_type == " news " :
article = resolve_by_slug ( get_table ( " news " ) , target_uid )
if article :
return f " /news/ { article . get ( ' slug ' , ' ' ) or article [ ' uid ' ] } "
return " /news "
if target_type == " bug " :
return f " /bugs?highlight= { target_uid } "
if target_type == " gist " :
gist = resolve_by_slug ( get_table ( " gists " ) , target_uid )
return f " /gists/ { gist [ ' slug ' ] or gist [ ' uid ' ] } " if gist else " /gists "
if target_type == " comment " :
comment = get_table ( " comments " ) . find_one ( uid = target_uid )
if not comment :
return " /feed "
parent_url = resolve_object_url ( comment . get ( " target_type " , " post " ) , comment . get ( " target_uid " ) or comment . get ( " post_uid " , " " ) )
return f " { parent_url } #comment- { target_uid } "
return " /feed "
2026-05-23 08:34:13 +02:00
VOTABLE_TARGETS : dict [ str , str ] = {
" post " : " posts " ,
" project " : " projects " ,
" gist " : " gists " ,
" comment " : " comments " ,
}
STAR_TARGETS : set [ str ] = { " post " , " project " , " gist " }
def update_target_stars ( target_type : str , target_uid : str , net_stars : int ) - > None :
table_name = VOTABLE_TARGETS . get ( target_type )
if not table_name or target_type not in STAR_TARGETS :
return
get_table ( table_name ) . update ( { " uid " : target_uid , " stars " : net_stars } , [ " uid " ] )
2026-05-30 20:16:39 +02:00
_authors_cache . clear ( )
2026-05-23 08:34:13 +02:00
def get_target_owner_uid ( target_type : str , target_uid : str ) - > str | None :
table_name = VOTABLE_TARGETS . get ( target_type )
if not table_name :
return None
row = get_table ( table_name ) . find_one ( uid = target_uid )
return row [ " user_uid " ] if row else None
2026-05-12 15:07:34 +02:00
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. " }
2026-05-30 20:16:39 +02:00
def get_featured_news ( limit = 5 ) :
if " news " not in db . tables :
return [ ]
from devplacepy . utils import time_ago
rows = list ( db [ " news " ] . find ( show_on_landing = 1 , order_by = [ " -synced_at " ] , _limit = limit ) )
articles = [ ]
for article in rows :
summary = ( article . get ( " description " ) or " " ) [ : 120 ] or ( article . get ( " content " ) or " " ) [ : 120 ]
articles . append ( {
" title " : article . get ( " title " , " " ) ,
" summary " : summary ,
" slug " : article . get ( " slug " , " " ) ,
" url " : article . get ( " url " , " " ) ,
" source_name " : article . get ( " source_name " , " " ) ,
" time_ago " : time_ago ( article [ " synced_at " ] ) if article . get ( " synced_at " ) else " " ,
} )
return articles