Compare commits
3 Commits
12d8401fa3
...
5783c3888d
| Author | SHA1 | Date | |
|---|---|---|---|
| 5783c3888d | |||
| feb360afd8 | |||
| 1bd9fe7e88 |
@ -3,6 +3,12 @@ import logging
|
||||
from typing import Any
|
||||
from datetime import datetime, timezone
|
||||
from fastapi.responses import RedirectResponse
|
||||
from devplacepy.attachments import (
|
||||
delete_attachments_for,
|
||||
delete_inline_image,
|
||||
get_attachments,
|
||||
link_attachments,
|
||||
)
|
||||
from devplacepy.database import (
|
||||
get_table,
|
||||
resolve_by_slug,
|
||||
@ -16,15 +22,6 @@ from devplacepy.database import (
|
||||
load_comments,
|
||||
db,
|
||||
)
|
||||
|
||||
BOOKMARKABLE_TYPES = {"post", "gist", "project", "news"}
|
||||
REACTABLE_TYPES = {"post", "comment", "gist", "project"}
|
||||
from devplacepy.attachments import (
|
||||
delete_attachments_for,
|
||||
delete_inline_image,
|
||||
get_attachments,
|
||||
link_attachments,
|
||||
)
|
||||
from devplacepy.utils import (
|
||||
time_ago,
|
||||
generate_uid,
|
||||
@ -33,6 +30,9 @@ from devplacepy.utils import (
|
||||
create_mention_notifications,
|
||||
)
|
||||
|
||||
BOOKMARKABLE_TYPES = {"post", "gist", "project", "news"}
|
||||
REACTABLE_TYPES = {"post", "comment", "gist", "project"}
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
||||
@ -555,7 +555,6 @@ def get_user_votes(user_uid, target_uids):
|
||||
|
||||
|
||||
def get_reactions_by_targets(target_type, target_uids, user=None):
|
||||
empty = {"counts": {}, "mine": []}
|
||||
if not target_uids or "reactions" not in db.tables:
|
||||
return {}
|
||||
placeholders, params = _in_clause(target_uids)
|
||||
@ -1047,7 +1046,7 @@ def get_platform_analytics(top_n: int = 10) -> dict:
|
||||
):
|
||||
signed_in_now = row["n"] or 0
|
||||
|
||||
def _new_since(cutoff: str) -> int:
|
||||
def _users_created_since(cutoff: str) -> int:
|
||||
return (
|
||||
db["users"].count(created_at={">=": cutoff}) if "users" in db.tables else 0
|
||||
)
|
||||
@ -1071,9 +1070,9 @@ def get_platform_analytics(top_n: int = 10) -> dict:
|
||||
"active_7d": active_7d,
|
||||
"active_30d": active_30d,
|
||||
"signed_in_now": signed_in_now,
|
||||
"new_24h": _new_since(d1),
|
||||
"new_7d": _new_since(d7),
|
||||
"new_30d": _new_since(d30),
|
||||
"new_24h": _users_created_since(d1),
|
||||
"new_7d": _users_created_since(d7),
|
||||
"new_30d": _users_created_since(d30),
|
||||
"posts_today": site["posts_today"],
|
||||
"totals": totals,
|
||||
"top_authors": top_authors,
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
from devplacepy import schemas
|
||||
from devplacepy.constants import TOPICS, REACTION_EMOJI
|
||||
from devplacepy.docs_examples import schema_example, action_example
|
||||
|
||||
VOTE_TARGETS = ["post", "comment", "gist", "project"]
|
||||
REACTION_TARGETS = ["post", "comment", "gist", "project"]
|
||||
@ -3133,9 +3135,6 @@ four ways to sign requests.
|
||||
},
|
||||
]
|
||||
|
||||
from devplacepy import schemas
|
||||
from devplacepy.docs_examples import schema_example, action_example
|
||||
|
||||
_PAGE_RESPONSES = {
|
||||
"feed-list": schemas.FeedOut,
|
||||
"posts-detail": schemas.PostDetailOut,
|
||||
|
||||
@ -98,7 +98,7 @@ def build_markdown(
|
||||
slugs = {p["slug"] for p in pages}
|
||||
|
||||
out = [
|
||||
f"# DevPlace Documentation",
|
||||
"# DevPlace Documentation",
|
||||
"",
|
||||
f"Complete developer documentation for `{base}`.",
|
||||
"",
|
||||
|
||||
@ -2,7 +2,7 @@ import logging
|
||||
from typing import Annotated
|
||||
from fastapi import APIRouter, Request, Form
|
||||
from devplacepy.models import AdminRoleForm, AdminPasswordForm, AdminSettingsForm
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse
|
||||
from fastapi.responses import HTMLResponse, JSONResponse
|
||||
from devplacepy.database import (
|
||||
get_table,
|
||||
build_pagination,
|
||||
@ -11,7 +11,6 @@ from devplacepy.database import (
|
||||
clear_settings_cache,
|
||||
get_platform_analytics,
|
||||
)
|
||||
from devplacepy.templating import templates
|
||||
from devplacepy.utils import (
|
||||
require_admin,
|
||||
hash_password,
|
||||
|
||||
@ -4,7 +4,7 @@ import logging
|
||||
from typing import Annotated
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from fastapi import APIRouter, Request, Form
|
||||
from fastapi.responses import RedirectResponse, HTMLResponse, JSONResponse
|
||||
from fastapi.responses import HTMLResponse, JSONResponse
|
||||
from devplacepy.database import get_table, get_setting, get_int_setting
|
||||
from devplacepy.templating import templates
|
||||
from devplacepy.utils import (
|
||||
|
||||
@ -4,7 +4,6 @@ from datetime import datetime, timezone
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.responses import RedirectResponse, JSONResponse, HTMLResponse
|
||||
from devplacepy.database import get_table, db, paginate, resolve_object_url
|
||||
from devplacepy.templating import templates
|
||||
from devplacepy.utils import generate_uid, require_user, time_ago
|
||||
from devplacepy.seo import base_seo_context
|
||||
from devplacepy.responses import respond
|
||||
|
||||
@ -2,11 +2,10 @@ import logging
|
||||
from typing import Annotated
|
||||
from datetime import datetime, timezone
|
||||
from fastapi import APIRouter, Request, Form
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
from fastapi.responses import HTMLResponse
|
||||
from devplacepy.models import BugForm
|
||||
from devplacepy.database import get_table, load_comments
|
||||
from devplacepy.attachments import get_attachments_batch, link_attachments
|
||||
from devplacepy.templating import templates
|
||||
from devplacepy.utils import (
|
||||
generate_uid,
|
||||
require_user,
|
||||
|
||||
@ -213,8 +213,6 @@ async def instance_logs(request: Request, project_slug: str, uid: str, tail: int
|
||||
@router.get("/{project_slug}/containers/instances/{uid}/metrics")
|
||||
async def instance_metrics(request: Request, project_slug: str, uid: str):
|
||||
require_admin(request)
|
||||
project = _project(project_slug)
|
||||
inst = _instance(project, uid)
|
||||
return JSONResponse(
|
||||
{"metrics": store.recent_metrics(uid), "stats": api.instance_stats(uid)}
|
||||
)
|
||||
|
||||
@ -16,7 +16,6 @@ from devplacepy.database import (
|
||||
)
|
||||
from devplacepy.attachments import get_attachments_batch
|
||||
from devplacepy.content import enrich_items
|
||||
from devplacepy.templating import templates
|
||||
from devplacepy.utils import get_current_user
|
||||
from devplacepy.seo import list_page_seo, next_page_url
|
||||
from devplacepy.responses import respond
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.responses import RedirectResponse
|
||||
from devplacepy.database import get_table
|
||||
from devplacepy.utils import (
|
||||
generate_uid,
|
||||
|
||||
@ -2,7 +2,7 @@ import logging
|
||||
from typing import Annotated
|
||||
from fastapi import APIRouter, Request, Form
|
||||
from devplacepy.models import GistForm, GistEditForm
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
from fastapi.responses import HTMLResponse
|
||||
from devplacepy.database import (
|
||||
get_table,
|
||||
get_users_by_uids,
|
||||
@ -19,7 +19,6 @@ from devplacepy.content import (
|
||||
canonical_redirect,
|
||||
first_image_url,
|
||||
)
|
||||
from devplacepy.templating import templates
|
||||
from devplacepy.utils import get_current_user, require_user, not_found, XP_GIST
|
||||
from devplacepy.seo import (
|
||||
base_seo_context,
|
||||
@ -173,7 +172,7 @@ async def create_gist(request: Request, data: Annotated[GistForm, Form()]):
|
||||
source_code = data.source_code.strip()
|
||||
language = data.language
|
||||
|
||||
valid_languages = {l[0] for l in LANGUAGES}
|
||||
valid_languages = {lang[0] for lang in LANGUAGES}
|
||||
if language not in valid_languages:
|
||||
language = "plaintext"
|
||||
|
||||
@ -203,7 +202,7 @@ async def edit_gist(
|
||||
):
|
||||
user = require_user(request)
|
||||
language = data.language
|
||||
if language not in {l[0] for l in LANGUAGES}:
|
||||
if language not in {lang[0] for lang in LANGUAGES}:
|
||||
language = "plaintext"
|
||||
return edit_content_item(
|
||||
request,
|
||||
|
||||
@ -8,7 +8,6 @@ from devplacepy.database import (
|
||||
get_top_authors,
|
||||
get_featured_news,
|
||||
)
|
||||
from devplacepy.templating import templates
|
||||
from devplacepy.utils import get_current_user
|
||||
from devplacepy.seo import list_page_seo
|
||||
from devplacepy.responses import respond
|
||||
|
||||
@ -3,10 +3,10 @@ from typing import Annotated
|
||||
from datetime import datetime, timezone
|
||||
from fastapi import APIRouter, Request, Form
|
||||
from devplacepy.models import MessageForm
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse
|
||||
from fastapi.responses import HTMLResponse, JSONResponse
|
||||
from devplacepy.database import get_table, db
|
||||
from devplacepy.attachments import get_attachments_batch
|
||||
from devplacepy.templating import templates, clear_unread_cache, clear_messages_cache
|
||||
from devplacepy.templating import clear_unread_cache, clear_messages_cache
|
||||
from devplacepy.utils import (
|
||||
generate_uid,
|
||||
require_user,
|
||||
|
||||
@ -11,7 +11,6 @@ from devplacepy.database import (
|
||||
get_user_bookmarks,
|
||||
paginate,
|
||||
)
|
||||
from devplacepy.templating import templates
|
||||
from devplacepy.utils import get_current_user, time_ago, not_found
|
||||
from devplacepy.content import canonical_redirect
|
||||
from devplacepy.seo import (
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
import logging
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse
|
||||
from fastapi.responses import HTMLResponse, JSONResponse
|
||||
from devplacepy.database import get_table, db
|
||||
from devplacepy.templating import (
|
||||
templates,
|
||||
clear_unread_cache,
|
||||
jinja_unread_count,
|
||||
jinja_unread_messages,
|
||||
|
||||
@ -2,10 +2,9 @@ import logging
|
||||
from typing import Annotated
|
||||
from datetime import datetime, timezone
|
||||
from fastapi import APIRouter, Request, Form
|
||||
from fastapi.responses import RedirectResponse, HTMLResponse
|
||||
from fastapi.responses import HTMLResponse
|
||||
from devplacepy.constants import TOPICS
|
||||
from devplacepy.database import db, get_table, resolve_by_slug
|
||||
from devplacepy.templating import templates
|
||||
from devplacepy.utils import (
|
||||
get_current_user,
|
||||
require_user,
|
||||
|
||||
@ -2,7 +2,7 @@ import logging
|
||||
from typing import Annotated
|
||||
from fastapi import APIRouter, Request, Form
|
||||
from devplacepy.models import ProfileForm
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse
|
||||
from fastapi.responses import HTMLResponse, JSONResponse
|
||||
from devplacepy.database import (
|
||||
get_table,
|
||||
db,
|
||||
@ -20,7 +20,6 @@ from devplacepy.database import (
|
||||
get_following_among,
|
||||
)
|
||||
from devplacepy.content import enrich_items
|
||||
from devplacepy.templating import templates
|
||||
from devplacepy.utils import (
|
||||
get_current_user,
|
||||
require_user,
|
||||
|
||||
@ -25,7 +25,6 @@ from devplacepy.content import (
|
||||
is_owner,
|
||||
can_view_project,
|
||||
)
|
||||
from devplacepy.templating import templates
|
||||
from devplacepy.utils import (
|
||||
get_current_user,
|
||||
require_user,
|
||||
@ -75,7 +74,7 @@ def get_projects_list(
|
||||
or_(columns.title.ilike(like), columns.description.ilike(like))
|
||||
)
|
||||
if not is_admin(viewer) and "is_private" in columns:
|
||||
visible = or_(columns.is_private == None, columns.is_private == 0)
|
||||
visible = or_(columns.is_private.is_(None), columns.is_private == 0)
|
||||
if viewer:
|
||||
visible = or_(visible, columns.user_uid == viewer["uid"])
|
||||
clauses.append(visible)
|
||||
|
||||
@ -11,7 +11,6 @@ from faker import Faker
|
||||
|
||||
from devplacepy.services.bot.browser import BotBrowser
|
||||
from devplacepy.services.bot.config import (
|
||||
CATEGORIES,
|
||||
FEED_TOPICS,
|
||||
GIST_LANGUAGES,
|
||||
PERSONAS,
|
||||
@ -1828,7 +1827,6 @@ class DevPlaceBot:
|
||||
on_news_detail = "/news/" in curl
|
||||
on_news_list = bool(re.search(r"/news(\?|$)", curl))
|
||||
on_notifications = "/notifications" in curl
|
||||
on_feed = "/feed" in curl or curl.strip("/") == self.base_url
|
||||
|
||||
actions = 0
|
||||
did_post = session_posted
|
||||
@ -1846,7 +1844,6 @@ class DevPlaceBot:
|
||||
actions += 1
|
||||
curl = await b.url()
|
||||
page_text = (await b.html()).lower()
|
||||
on_feed = "/feed" in curl
|
||||
on_post = "/posts/" in curl
|
||||
continue
|
||||
|
||||
|
||||
@ -14,10 +14,8 @@ from devplacepy.services.containers.backend.base import Mount, PortMapping, RunS
|
||||
from devplacepy.services.containers.runtime import get_backend
|
||||
from devplacepy.services.devii.tasks.schedule import (
|
||||
Schedule,
|
||||
next_run,
|
||||
now_utc,
|
||||
to_iso,
|
||||
from_iso,
|
||||
)
|
||||
|
||||
IMAGE_NAME_RE = re.compile(r"^[a-z0-9][a-z0-9._-]{0,62}$")
|
||||
|
||||
@ -2,7 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import Any, Optional
|
||||
|
||||
|
||||
@ -17,7 +17,6 @@ from .client import ClientController
|
||||
from .config import Settings
|
||||
from .cost import CostTracker
|
||||
from .cost.tracker import Pricing
|
||||
from .errors import AuthRequiredError
|
||||
from .http_client import PlatformClient
|
||||
from .llm import LLMClient
|
||||
from .registry import CATALOG
|
||||
|
||||
@ -3,7 +3,6 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
from devplacepy.config import DATA_DIR
|
||||
from devplacepy import project_files
|
||||
|
||||
@ -6,7 +6,7 @@ import logging
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Optional
|
||||
|
||||
from devplacepy.database import db, get_table
|
||||
from devplacepy.database import db
|
||||
from devplacepy.services.openai_gateway.reliability import percentile
|
||||
from devplacepy.services.openai_gateway.usage import (
|
||||
GATEWAY_CONCURRENCY,
|
||||
|
||||
@ -413,3 +413,19 @@
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 360px) {
|
||||
.admin-table th,
|
||||
.admin-table td {
|
||||
padding: 0.25rem 0.375rem;
|
||||
}
|
||||
|
||||
.admin-table {
|
||||
font-size: 0.6875rem;
|
||||
}
|
||||
|
||||
.admin-btn {
|
||||
font-size: 0.6875rem;
|
||||
padding: 0.25rem 0.375rem;
|
||||
}
|
||||
}
|
||||
|
||||
@ -130,3 +130,13 @@
|
||||
height: 80px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
.attachment-lightbox-close {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
top: 0.75rem;
|
||||
right: 0.75rem;
|
||||
font-size: 1.375rem;
|
||||
}
|
||||
}
|
||||
|
||||
@ -539,7 +539,7 @@ img {
|
||||
.topnav-icon { position: relative; padding: 0.375rem; color: var(--text-secondary); font-size: 1.25rem; background: none; border: none; cursor: pointer; font-family: inherit; line-height: 1; }
|
||||
.topnav-icon:hover { color: var(--text-primary); }
|
||||
.nav-badge {
|
||||
position: absolute; top: 0; right: 0; min-width: 16px; height: 16px;
|
||||
position: absolute; top: 0; right: 0; z-index: 1; min-width: 16px; height: 16px;
|
||||
padding: 0 0.25rem; border-radius: 8px; background: var(--accent); color: var(--white);
|
||||
font-size: 0.6875rem; font-weight: 700;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
@ -568,7 +568,8 @@ img {
|
||||
padding: 0.25rem 0;
|
||||
z-index: 1001;
|
||||
}
|
||||
.topnav-user-dropdown:hover .dropdown-menu { display: block; }
|
||||
.topnav-user-dropdown:hover .dropdown-menu,
|
||||
.topnav-user-dropdown.open .dropdown-menu { display: block; }
|
||||
.dropdown-item {
|
||||
display: block;
|
||||
padding: 0.5rem 0.875rem;
|
||||
|
||||
@ -240,8 +240,8 @@ dp-upload[hidden] {
|
||||
|
||||
.dp-toast-host {
|
||||
position: fixed;
|
||||
bottom: 1rem;
|
||||
right: 1rem;
|
||||
bottom: max(1rem, env(safe-area-inset-bottom));
|
||||
right: max(1rem, env(safe-area-inset-right));
|
||||
z-index: 1100;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -251,7 +251,7 @@ dp-upload[hidden] {
|
||||
|
||||
.dp-toast {
|
||||
min-width: 12rem;
|
||||
max-width: 22rem;
|
||||
max-width: min(22rem, calc(100vw - 2rem));
|
||||
padding: 0.625rem 0.875rem;
|
||||
border-radius: var(--radius);
|
||||
background: var(--bg-card);
|
||||
|
||||
@ -468,6 +468,8 @@ devii-terminal .devii-agent .md-img {
|
||||
}
|
||||
|
||||
/* Mobile: only close is available; window is forced fullscreen by JS */
|
||||
devii-terminal.devii-mobile .devii-winctl button[data-win="minimize"],
|
||||
devii-terminal.devii-mobile .devii-winctl button[data-win="normalize"],
|
||||
devii-terminal.devii-mobile .devii-winctl button[data-win="maximize"],
|
||||
devii-terminal.devii-mobile .devii-winctl button[data-win="fullscreen"] {
|
||||
display: none;
|
||||
|
||||
@ -65,7 +65,7 @@
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 1rem;
|
||||
padding: var(--space-xl);
|
||||
}
|
||||
|
||||
.post-card:hover {
|
||||
@ -375,7 +375,7 @@
|
||||
}
|
||||
|
||||
.post-card {
|
||||
padding: 0.75rem;
|
||||
padding: var(--space-lg);
|
||||
}
|
||||
|
||||
.post-header {
|
||||
@ -416,6 +416,19 @@
|
||||
padding-left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.feed-nav {
|
||||
gap: 0.125rem;
|
||||
overflow-x: auto;
|
||||
flex-wrap: nowrap;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.feed-nav-btn {
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.leaderboard-page {
|
||||
|
||||
@ -163,6 +163,14 @@
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Mobile: only close is available; window is forced fullscreen by JS */
|
||||
.fw-host.fw-mobile .fw-winctl button[data-win="minimize"],
|
||||
.fw-host.fw-mobile .fw-winctl button[data-win="normalize"],
|
||||
.fw-host.fw-mobile .fw-winctl button[data-win="maximize"],
|
||||
.fw-host.fw-mobile .fw-winctl button[data-win="fullscreen"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Container terminal body */
|
||||
.ct-body {
|
||||
background: #000000;
|
||||
|
||||
@ -16,42 +16,6 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.landing-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1rem 2rem;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.landing-logo {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 800;
|
||||
color: var(--text-primary);
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.landing-logo span {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.landing-nav-links {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.landing-nav-links a {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.landing-nav-links a:hover {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.landing-hero {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -304,9 +268,6 @@
|
||||
.landing-features {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
.landing-nav {
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
.landing-section {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
@ -265,6 +265,22 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 360px) {
|
||||
.messages-thread {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.messages-input-area {
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
|
||||
.message-bubble {
|
||||
max-width: 85%;
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
.messages-send-btn {
|
||||
width: 44px;
|
||||
|
||||
@ -55,7 +55,7 @@
|
||||
}
|
||||
|
||||
.news-card-body {
|
||||
padding: 1rem;
|
||||
padding: var(--space-xl);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.625rem;
|
||||
@ -282,7 +282,7 @@
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.news-card-body {
|
||||
padding: 0.875rem;
|
||||
padding: var(--space-lg);
|
||||
}
|
||||
|
||||
.news-card-title {
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 1rem;
|
||||
padding: var(--space-xl);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
@ -64,7 +64,7 @@
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 1rem;
|
||||
padding: var(--space-xl);
|
||||
}
|
||||
|
||||
.comments-section h3 {
|
||||
@ -198,7 +198,7 @@
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.post-detail {
|
||||
padding: 0.75rem;
|
||||
padding: var(--space-lg);
|
||||
}
|
||||
|
||||
.post-detail-header {
|
||||
@ -211,7 +211,7 @@
|
||||
}
|
||||
|
||||
.comments-section {
|
||||
padding: 0.75rem;
|
||||
padding: var(--space-lg);
|
||||
}
|
||||
|
||||
.comment {
|
||||
@ -227,3 +227,13 @@
|
||||
padding-left: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 360px) {
|
||||
.post-detail {
|
||||
padding: var(--space-md);
|
||||
}
|
||||
|
||||
.comments-section {
|
||||
padding: var(--space-md);
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ export class MobileNav {
|
||||
this.initMobileNav();
|
||||
this.initMessagesResponsive();
|
||||
this.initMessageThread();
|
||||
this.initProfileDropdown();
|
||||
}
|
||||
|
||||
initMobileNav() {
|
||||
@ -92,4 +93,28 @@ export class MobileNav {
|
||||
const thread = document.querySelector(".messages-thread");
|
||||
if (thread) thread.scrollTop = thread.scrollHeight;
|
||||
}
|
||||
|
||||
initProfileDropdown() {
|
||||
const dropdown = document.querySelector(".topnav-user-dropdown");
|
||||
if (!dropdown) return;
|
||||
const btn = dropdown.querySelector(".topnav-user");
|
||||
if (!btn) return;
|
||||
|
||||
btn.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
dropdown.classList.toggle("open");
|
||||
});
|
||||
|
||||
document.addEventListener("click", (e) => {
|
||||
if (!dropdown.contains(e.target)) {
|
||||
dropdown.classList.remove("open");
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Escape") {
|
||||
dropdown.classList.remove("open");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -81,14 +81,15 @@
|
||||
<span class="nav-badge" data-counter-badge {% if unread_count == 0 %}hidden{% endif %}>{{ unread_count }}</span>
|
||||
</a>
|
||||
<div class="topnav-user-dropdown">
|
||||
<a href="/profile/{{ user['username'] }}" class="topnav-user">
|
||||
<button type="button" class="topnav-user" aria-label="User menu">
|
||||
<img src="{{ avatar_url('multiavatar', user['username'], 32) }}" class="avatar-img avatar-sm" alt="{{ user['username'] }}" loading="lazy">
|
||||
<div class="topnav-user-info">
|
||||
<span class="topnav-user-name">{{ user['username'] }}</span>
|
||||
{% if is_admin(user) %}<span class="topnav-user-role">{{ user['role'] }}</span>{% endif %}
|
||||
</div>
|
||||
</a>
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
<a href="/profile/{{ user['username'] }}" class="dropdown-item"><span class="icon">👤</span> Profile</a>
|
||||
<a href="/devii/" class="dropdown-item" data-devii-open><span class="icon">🤖</span> Devii</a>
|
||||
<a href="/bookmarks/saved" class="dropdown-item"><span class="icon">đź”–</span> Saved</a>
|
||||
<a href="/auth/logout" class="dropdown-item"><span class="icon">🚪</span> Logout</a>
|
||||
|
||||
@ -3,11 +3,13 @@ from markupsafe import Markup
|
||||
from devplacepy.cache import TTLCache
|
||||
from devplacepy.config import TEMPLATES_DIR
|
||||
from devplacepy.constants import TOPICS, REACTION_EMOJI
|
||||
from devplacepy.database import get_table
|
||||
from devplacepy.database import get_int_setting, get_setting, get_table
|
||||
from devplacepy.avatar import avatar_url
|
||||
from devplacepy.utils import format_date as _format_date
|
||||
from devplacepy.utils import badge_info, is_admin
|
||||
from devplacepy.attachments import format_file_size, file_icon_emoji
|
||||
from devplacepy.content import is_owner as _owns
|
||||
from devplacepy.customization import custom_css_tag, custom_js_tag, page_type_for
|
||||
|
||||
templates = Jinja2Templates(directory=str(TEMPLATES_DIR))
|
||||
|
||||
@ -46,24 +48,22 @@ def clear_messages_cache(user_uid: str) -> None:
|
||||
_messages_cache.pop(user_uid)
|
||||
|
||||
|
||||
def jinja_unread_count(user_uid: str) -> int:
|
||||
cached = _unread_cache.get(user_uid)
|
||||
def _cached_count(cache: TTLCache, table_name: str, cache_key: str, **filters) -> int:
|
||||
cached = cache.get(cache_key)
|
||||
if cached is not None:
|
||||
return cached
|
||||
notifs = get_table("notifications")
|
||||
count = notifs.count(user_uid=user_uid, read=False)
|
||||
_unread_cache.set(user_uid, count)
|
||||
table = get_table(table_name)
|
||||
count = table.count(**filters)
|
||||
cache.set(cache_key, count)
|
||||
return count
|
||||
|
||||
|
||||
def jinja_unread_count(user_uid: str) -> int:
|
||||
return _cached_count(_unread_cache, "notifications", user_uid, user_uid=user_uid, read=False)
|
||||
|
||||
|
||||
def jinja_unread_messages(user_uid: str) -> int:
|
||||
cached = _messages_cache.get(user_uid)
|
||||
if cached is not None:
|
||||
return cached
|
||||
messages = get_table("messages")
|
||||
count = messages.count(receiver_uid=user_uid, read=False)
|
||||
_messages_cache.set(user_uid, count)
|
||||
return count
|
||||
return _cached_count(_messages_cache, "messages", user_uid, receiver_uid=user_uid, read=False)
|
||||
|
||||
|
||||
def jinja_user_projects(user_uid: str) -> list:
|
||||
@ -82,20 +82,14 @@ templates.env.globals["REACTION_EMOJI"] = REACTION_EMOJI
|
||||
|
||||
|
||||
def jinja_max_upload_size_mb():
|
||||
from devplacepy.database import get_int_setting
|
||||
|
||||
return get_int_setting("max_upload_size_mb", 10)
|
||||
|
||||
|
||||
def jinja_max_attachments():
|
||||
from devplacepy.database import get_int_setting
|
||||
|
||||
return get_int_setting("max_attachments_per_resource", 10)
|
||||
|
||||
|
||||
def jinja_allowed_file_types():
|
||||
from devplacepy.database import get_setting
|
||||
|
||||
return get_setting("allowed_file_types", "")
|
||||
|
||||
|
||||
@ -140,13 +134,9 @@ def jinja_language_name(code: str) -> str:
|
||||
|
||||
templates.env.globals["language_name"] = jinja_language_name
|
||||
|
||||
from devplacepy.attachments import format_file_size, file_icon_emoji
|
||||
|
||||
templates.env.globals["format_file_size"] = format_file_size
|
||||
templates.env.globals["file_icon_emoji"] = file_icon_emoji
|
||||
|
||||
from devplacepy.customization import custom_css_tag, custom_js_tag, page_type_for
|
||||
|
||||
templates.env.globals["custom_css_tag"] = custom_css_tag
|
||||
templates.env.globals["custom_js_tag"] = custom_js_tag
|
||||
templates.env.globals["page_type_for"] = page_type_for
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
import uuid
|
||||
|
||||
@ -1,4 +1,10 @@
|
||||
import os, sys, tempfile, subprocess, time, asyncio, threading
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import subprocess
|
||||
import time
|
||||
import asyncio
|
||||
import threading
|
||||
from pathlib import Path
|
||||
import pytest
|
||||
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import pytest
|
||||
from uuid import uuid4
|
||||
from datetime import datetime, timezone
|
||||
from tests.conftest import BASE_URL
|
||||
@ -65,7 +64,8 @@ def test_admin_users_loads(alice):
|
||||
|
||||
def test_admin_users_pagination(alice):
|
||||
page, _ = alice
|
||||
import urllib.request, urllib.parse
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
|
||||
for i in range(30):
|
||||
data = urllib.parse.urlencode(
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import argparse
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
from tests.conftest import BASE_URL, login_user, run_async
|
||||
from tests.conftest import BASE_URL, run_async
|
||||
from devplacepy.database import db, get_table, init_db, refresh_snapshot
|
||||
from devplacepy import config, project_files
|
||||
from devplacepy.services.containers import api, store, runtime
|
||||
@ -13,7 +12,7 @@ from devplacepy.services.containers.backend.docker_cli import build_run_argv, pa
|
||||
from devplacepy.services.containers.backend.fake import FakeBackend
|
||||
from devplacepy.services.containers.service import ContainerService
|
||||
from devplacepy.services.containers.api import INSTANCE_LABEL
|
||||
from devplacepy.services.devii.tasks.schedule import Schedule, now_utc, to_iso
|
||||
from devplacepy.services.devii.tasks.schedule import Schedule, now_utc
|
||||
|
||||
_CONTAINER_TABLES = (
|
||||
"instances",
|
||||
|
||||
@ -1,9 +1,5 @@
|
||||
import pytest
|
||||
from uuid import uuid4
|
||||
from datetime import datetime
|
||||
from tests.conftest import BASE_URL
|
||||
from devplacepy.database import get_table
|
||||
from devplacepy.utils import make_combined_slug
|
||||
|
||||
|
||||
def test_landing_page_loads(page, app_server):
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import pytest
|
||||
import requests
|
||||
from tests.conftest import BASE_URL
|
||||
|
||||
|
||||
@ -1,11 +1,19 @@
|
||||
from devplacepy.database import get_table
|
||||
from devplacepy.utils import (
|
||||
hash_password,
|
||||
verify_password,
|
||||
generate_uid,
|
||||
slugify,
|
||||
time_ago,
|
||||
level_for_xp,
|
||||
award_badge,
|
||||
award_xp,
|
||||
badge_info,
|
||||
check_milestone_badges,
|
||||
create_mention_notifications,
|
||||
extract_mentions,
|
||||
generate_uid,
|
||||
hash_password,
|
||||
level_for_xp,
|
||||
safe_next,
|
||||
slugify,
|
||||
strip_html,
|
||||
time_ago,
|
||||
verify_password,
|
||||
)
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
@ -114,18 +122,6 @@ def test_badge_info_unknown_fallback():
|
||||
assert meta["description"] == "Nonexistent Badge"
|
||||
|
||||
|
||||
from devplacepy.utils import (
|
||||
safe_next,
|
||||
strip_html,
|
||||
extract_mentions,
|
||||
award_badge,
|
||||
award_xp,
|
||||
check_milestone_badges,
|
||||
create_mention_notifications,
|
||||
)
|
||||
from devplacepy.database import get_table
|
||||
|
||||
|
||||
def _seed_user(role="Member", xp=0, level=1):
|
||||
uid = generate_uid()
|
||||
username = f"ut_{uid[:8]}"
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
from tests.conftest import BASE_URL
|
||||
from tests.test_post import create_post
|
||||
|
||||
POST_PAYLOAD = (
|
||||
|
||||
Loading…
Reference in New Issue
Block a user