Start support for authoring articles
parent
e363ae2802
commit
24f3f94056
|
@ -661,6 +661,7 @@ async def admin_actions_new(
|
||||||
is_sensitive: bool = Form(False),
|
is_sensitive: bool = Form(False),
|
||||||
visibility: str = Form(),
|
visibility: str = Form(),
|
||||||
poll_type: str | None = Form(None),
|
poll_type: str | None = Form(None),
|
||||||
|
name: str | None = Form(None),
|
||||||
csrf_check: None = Depends(verify_csrf_token),
|
csrf_check: None = Depends(verify_csrf_token),
|
||||||
db_session: AsyncSession = Depends(get_db_session),
|
db_session: AsyncSession = Depends(get_db_session),
|
||||||
) -> RedirectResponse:
|
) -> RedirectResponse:
|
||||||
|
@ -687,6 +688,8 @@ async def admin_actions_new(
|
||||||
raise ValueError("Question must have at least 2 answers")
|
raise ValueError("Question must have at least 2 answers")
|
||||||
|
|
||||||
poll_duration_in_minutes = int(raw_form_data["poll_duration"])
|
poll_duration_in_minutes = int(raw_form_data["poll_duration"])
|
||||||
|
elif name:
|
||||||
|
ap_type = "Article"
|
||||||
|
|
||||||
public_id = await boxes.send_create(
|
public_id = await boxes.send_create(
|
||||||
db_session,
|
db_session,
|
||||||
|
@ -700,6 +703,7 @@ async def admin_actions_new(
|
||||||
poll_type=poll_type,
|
poll_type=poll_type,
|
||||||
poll_answers=poll_answers,
|
poll_answers=poll_answers,
|
||||||
poll_duration_in_minutes=poll_duration_in_minutes,
|
poll_duration_in_minutes=poll_duration_in_minutes,
|
||||||
|
name=name,
|
||||||
)
|
)
|
||||||
return RedirectResponse(
|
return RedirectResponse(
|
||||||
request.url_for("outbox_by_public_id", public_id=public_id),
|
request.url_for("outbox_by_public_id", public_id=public_id),
|
||||||
|
|
|
@ -297,6 +297,7 @@ async def send_create(
|
||||||
poll_type: str | None = None,
|
poll_type: str | None = None,
|
||||||
poll_answers: list[str] | None = None,
|
poll_answers: list[str] | None = None,
|
||||||
poll_duration_in_minutes: int | None = None,
|
poll_duration_in_minutes: int | None = None,
|
||||||
|
name: str | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
note_id = allocate_outbox_id()
|
note_id = allocate_outbox_id()
|
||||||
published = now().replace(microsecond=0).isoformat().replace("+00:00", "Z")
|
published = now().replace(microsecond=0).isoformat().replace("+00:00", "Z")
|
||||||
|
@ -367,6 +368,11 @@ async def send_create(
|
||||||
for answer in poll_answers
|
for answer in poll_answers
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
elif ap_type == "Article":
|
||||||
|
if not name:
|
||||||
|
raise ValueError("Article must have a name")
|
||||||
|
|
||||||
|
extra_obj_attrs = {"name": name}
|
||||||
|
|
||||||
obj = {
|
obj = {
|
||||||
"@context": ap.AS_EXTENDED_CTX,
|
"@context": ap.AS_EXTENDED_CTX,
|
||||||
|
|
46
app/main.py
46
app/main.py
|
@ -215,6 +215,7 @@ async def index(
|
||||||
models.OutboxObject.visibility == ap.VisibilityEnum.PUBLIC,
|
models.OutboxObject.visibility == ap.VisibilityEnum.PUBLIC,
|
||||||
models.OutboxObject.is_deleted.is_(False),
|
models.OutboxObject.is_deleted.is_(False),
|
||||||
models.OutboxObject.is_hidden_from_homepage.is_(False),
|
models.OutboxObject.is_hidden_from_homepage.is_(False),
|
||||||
|
models.OutboxObject.ap_type != "Article",
|
||||||
)
|
)
|
||||||
q = select(models.OutboxObject).where(*where)
|
q = select(models.OutboxObject).where(*where)
|
||||||
total_count = await db_session.scalar(
|
total_count = await db_session.scalar(
|
||||||
|
@ -257,6 +258,51 @@ async def index(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/articles")
|
||||||
|
async def articles(
|
||||||
|
request: Request,
|
||||||
|
db_session: AsyncSession = Depends(get_db_session),
|
||||||
|
_: httpsig.HTTPSigInfo = Depends(httpsig.httpsig_checker),
|
||||||
|
page: int | None = None,
|
||||||
|
) -> templates.TemplateResponse | ActivityPubResponse:
|
||||||
|
# TODO: special ActivityPub collection for Article
|
||||||
|
|
||||||
|
where = (
|
||||||
|
models.OutboxObject.visibility == ap.VisibilityEnum.PUBLIC,
|
||||||
|
models.OutboxObject.is_deleted.is_(False),
|
||||||
|
models.OutboxObject.is_hidden_from_homepage.is_(False),
|
||||||
|
models.OutboxObject.ap_type == "Article",
|
||||||
|
)
|
||||||
|
q = select(models.OutboxObject).where(*where)
|
||||||
|
|
||||||
|
outbox_objects_result = await db_session.scalars(
|
||||||
|
q.options(
|
||||||
|
joinedload(models.OutboxObject.outbox_object_attachments).options(
|
||||||
|
joinedload(models.OutboxObjectAttachment.upload)
|
||||||
|
),
|
||||||
|
joinedload(models.OutboxObject.relates_to_inbox_object).options(
|
||||||
|
joinedload(models.InboxObject.actor),
|
||||||
|
),
|
||||||
|
joinedload(models.OutboxObject.relates_to_outbox_object).options(
|
||||||
|
joinedload(models.OutboxObject.outbox_object_attachments).options(
|
||||||
|
joinedload(models.OutboxObjectAttachment.upload)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).order_by(models.OutboxObject.ap_published_at.desc())
|
||||||
|
)
|
||||||
|
outbox_objects = outbox_objects_result.unique().all()
|
||||||
|
|
||||||
|
return await templates.render_template(
|
||||||
|
db_session,
|
||||||
|
request,
|
||||||
|
"articles.html",
|
||||||
|
{
|
||||||
|
"request": request,
|
||||||
|
"objects": outbox_objects,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def _build_followx_collection(
|
async def _build_followx_collection(
|
||||||
db_session: AsyncSession,
|
db_session: AsyncSession,
|
||||||
model_cls: Type[models.Following | models.Follower],
|
model_cls: Type[models.Following | models.Follower],
|
||||||
|
|
|
@ -142,6 +142,15 @@ footer {
|
||||||
max-width: 50px;
|
max-width: 50px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#articles {
|
||||||
|
list-style-type: none;
|
||||||
|
margin: 30px 0;
|
||||||
|
padding: 0 20px;
|
||||||
|
li {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#notifications, #followers, #following {
|
#notifications, #followers, #following {
|
||||||
ul {
|
ul {
|
||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
|
|
|
@ -109,6 +109,14 @@ async def render_template(
|
||||||
)
|
)
|
||||||
if is_admin
|
if is_admin
|
||||||
else 0,
|
else 0,
|
||||||
|
"articles_count": await db_session.scalar(
|
||||||
|
select(func.count(models.OutboxObject.id)).where(
|
||||||
|
models.OutboxObject.visibility == ap.VisibilityEnum.PUBLIC,
|
||||||
|
models.OutboxObject.is_deleted.is_(False),
|
||||||
|
models.OutboxObject.is_hidden_from_homepage.is_(False),
|
||||||
|
models.OutboxObject.ap_type == "Article",
|
||||||
|
)
|
||||||
|
),
|
||||||
"local_actor": LOCAL_ACTOR,
|
"local_actor": LOCAL_ACTOR,
|
||||||
"followers_count": await db_session.scalar(
|
"followers_count": await db_session.scalar(
|
||||||
select(func.count(models.Follower.id))
|
select(func.count(models.Follower.id))
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<nav class="flexbox">
|
<nav class="flexbox">
|
||||||
<ul>
|
<ul>
|
||||||
{% for ap_type in ["Note", "Question"] %}
|
{% for ap_type in ["Note", "Article", "Question"] %}
|
||||||
<li><a href="?type={{ ap_type }}" {% if request.query_params.get("type", "Note") == ap_type %}class="active"{% endif %}>
|
<li><a href="?type={{ ap_type }}" {% if request.query_params.get("type", "Note") == ap_type %}class="active"{% endif %}>
|
||||||
{{ ap_type }}
|
{{ ap_type }}
|
||||||
</a>
|
</a>
|
||||||
|
@ -35,6 +35,13 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
{% if request.query_params.type == "Article" %}
|
||||||
|
<p>
|
||||||
|
<input type="text" style="width:95%" name="name" placeholder="Title">
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% for emoji in emojis %}
|
{% for emoji in emojis %}
|
||||||
<span class="ji">{{ emoji | emojify(True) | safe }}</span>
|
<span class="ji">{{ emoji | emojify(True) | safe }}</span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
{%- import "utils.html" as utils with context -%}
|
||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
|
<title>{{ local_actor.display_name }}'s articles</title>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% include "header.html" %}
|
||||||
|
|
||||||
|
<ul class="h-feed" id="articles">
|
||||||
|
<data class="p-name" value="{{ local_actor.display_name}}'s articles"></data>
|
||||||
|
{% for outbox_object in objects %}
|
||||||
|
<li>
|
||||||
|
<span class="muted" style="padding-right:10px;">{{ outbox_object.ap_published_at.strftime("%Y-%m-%d") }}</span> <a href="{{ outbox_object.url }}">{{ outbox_object.name }}</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{% endblock %}
|
|
@ -22,6 +22,9 @@
|
||||||
<nav class="flexbox">
|
<nav class="flexbox">
|
||||||
<ul>
|
<ul>
|
||||||
<li>{{ header_link("index", "Notes") }}</li>
|
<li>{{ header_link("index", "Notes") }}</li>
|
||||||
|
{% if articles_count %}
|
||||||
|
<li>{{ header_link("articles", "Articles") }}</li>
|
||||||
|
{% endif %}
|
||||||
<li>{{ header_link("followers", "Followers") }} <span class="counter">{{ followers_count }}</span></li>
|
<li>{{ header_link("followers", "Followers") }} <span class="counter">{{ followers_count }}</span></li>
|
||||||
<li>{{ header_link("following", "Following") }} <span class="counter">{{ following_count }}</span></li>
|
<li>{{ header_link("following", "Following") }} <span class="counter">{{ following_count }}</span></li>
|
||||||
<li>{{ header_link("get_remote_follow", "Remote follow") }}</li>
|
<li>{{ header_link("get_remote_follow", "Remote follow") }}</li>
|
||||||
|
|
|
@ -4,14 +4,14 @@
|
||||||
{% block head %}
|
{% block head %}
|
||||||
{% if outbox_object %}
|
{% if outbox_object %}
|
||||||
{% set excerpt = outbox_object.content | html2text | trim | truncate(50) %}
|
{% set excerpt = outbox_object.content | html2text | trim | truncate(50) %}
|
||||||
<title>{{ local_actor.display_name }}: "{{ excerpt }}"</title>
|
<title>{% if outbox_object.name %}{{ outbox_object.name }}{% else %}{{ local_actor.display_name }}: "{{ excerpt }}"{% endif %}</title>
|
||||||
<link rel="webmention" href="{{ url_for("webmention_endpoint") }}">
|
<link rel="webmention" href="{{ url_for("webmention_endpoint") }}">
|
||||||
<link rel="alternate" href="{{ request.url }}" type="application/activity+json">
|
<link rel="alternate" href="{{ request.url }}" type="application/activity+json">
|
||||||
<meta name="description" content="{{ excerpt }}">
|
<meta name="description" content="{{ excerpt }}">
|
||||||
<meta content="article" property="og:type" />
|
<meta content="article" property="og:type" />
|
||||||
<meta content="{{ outbox_object.url }}" property="og:url" />
|
<meta content="{{ outbox_object.url }}" property="og:url" />
|
||||||
<meta content="{{ local_actor.display_name }}'s microblog" property="og:site_name" />
|
<meta content="{{ local_actor.display_name }}'s microblog" property="og:site_name" />
|
||||||
<meta content="Note" property="og:title" />
|
<meta content="{% if outbox_object.name %}{{ name }}{% else %}Note{% endif %}" property="og:title" />
|
||||||
<meta content="{{ excerpt }}" property="og:description" />
|
<meta content="{{ excerpt }}" property="og:description" />
|
||||||
<meta content="{{ local_actor.icon_url }}" property="og:image" />
|
<meta content="{{ local_actor.icon_url }}" property="og:image" />
|
||||||
<meta content="summary" property="twitter:card" />
|
<meta content="summary" property="twitter:card" />
|
||||||
|
@ -24,7 +24,7 @@
|
||||||
{% macro display_replies_tree(replies_tree_node) %}
|
{% macro display_replies_tree(replies_tree_node) %}
|
||||||
|
|
||||||
{% if replies_tree_node.is_requested %}
|
{% if replies_tree_node.is_requested %}
|
||||||
{{ utils.display_object(replies_tree_node.ap_object, likes=likes, shares=shares, webmentions=webmentions, expanded=not replies_tree_node.is_root) }}
|
{{ utils.display_object(replies_tree_node.ap_object, likes=likes, shares=shares, webmentions=webmentions, expanded=not replies_tree_node.is_root, is_object_page=True) }}
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ utils.display_object(replies_tree_node.ap_object) }}
|
{{ utils.display_object(replies_tree_node.ap_object) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -263,10 +263,20 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro display_object(object, likes=[], shares=[], webmentions=[], expanded=False, actors_metadata={}) %}
|
{% macro display_object(object, likes=[], shares=[], webmentions=[], expanded=False, actors_metadata={}, is_object_page=False) %}
|
||||||
|
{% set is_article_mode = object.is_from_outbox and object.ap_type == "Article" and is_object_page %}
|
||||||
{% if object.ap_type in ["Note", "Article", "Video", "Page", "Question"] %}
|
{% if object.ap_type in ["Note", "Article", "Video", "Page", "Question"] %}
|
||||||
<div class="ap-object {% if expanded %}ap-object-expanded {% endif %}h-entry" id="{{ object.permalink_id }}">
|
<div class="ap-object {% if expanded %}ap-object-expanded {% endif %}h-entry" id="{{ object.permalink_id }}">
|
||||||
|
|
||||||
|
{% if is_article_mode %}
|
||||||
|
<data class="h-card">
|
||||||
|
<data class="u-photo" value="{{ local_actor.icon_url }}"></data>
|
||||||
|
<data class="u-url" value="{{ local_actor.url}}"></data>
|
||||||
|
<data class="p-name" value="{{ local_actor.handle }}"></data>
|
||||||
|
</data>
|
||||||
|
{% else %}
|
||||||
{{ display_actor(object.actor, actors_metadata, embedded=True) }}
|
{{ display_actor(object.actor, actors_metadata, embedded=True) }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if object.in_reply_to %}
|
{% if object.in_reply_to %}
|
||||||
<a href="{% if is_admin %}{{ url_for("get_lookup") }}?query={% endif %}{{ object.in_reply_to }}" title="{{ object.in_reply_to }}" class="in-reply-to" rel="nofollow">
|
<a href="{% if is_admin %}{{ url_for("get_lookup") }}?query={% endif %}{{ object.in_reply_to }}" title="{{ object.in_reply_to }}" class="in-reply-to" rel="nofollow">
|
||||||
|
@ -274,6 +284,15 @@
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if object.ap_type == "Article" %}
|
||||||
|
<h2 class="p-name" style="margin-top:0;">{{ object.name }}</h2>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if is_article_mode %}
|
||||||
|
<time class="dt-published muted" datetime="{{ object.ap_published_at.replace(microsecond=0).isoformat() }}" title="{{ object.ap_published_at.replace(microsecond=0).isoformat() }}">{{ object.ap_published_at.strftime("%b %d, %Y") }}</time>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
{% if object.summary %}
|
{% if object.summary %}
|
||||||
<p class="p-summary">{{ object.summary | clean_html(object) | safe }}</p>
|
<p class="p-summary">{{ object.summary | clean_html(object) | safe }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -352,9 +371,11 @@
|
||||||
<li>
|
<li>
|
||||||
<div><a href="{{ object.url }}"{% if object.is_from_inbox %} rel="nofollow"{% endif %} class="object-permalink u-url u-uid">permalink</a></div>
|
<div><a href="{{ object.url }}"{% if object.is_from_inbox %} rel="nofollow"{% endif %} class="object-permalink u-url u-uid">permalink</a></div>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
{% if not is_article_mode %}
|
||||||
<time class="dt-published" datetime="{{ object.ap_published_at.replace(microsecond=0).isoformat() }}" title="{{ object.ap_published_at.replace(microsecond=0).isoformat() }}">{{ object.ap_published_at | timeago }}</time>
|
<li>
|
||||||
</li>
|
<time class="dt-published" datetime="{{ object.ap_published_at.replace(microsecond=0).isoformat() }}" title="{{ object.ap_published_at.replace(microsecond=0).isoformat() }}">{{ object.ap_published_at | timeago }}</time>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
{% if object.ap_type == "Question" %}
|
{% if object.ap_type == "Question" %}
|
||||||
{% set endAt = object.ap_object.endTime | parse_datetime %}
|
{% set endAt = object.ap_object.endTime | parse_datetime %}
|
||||||
<li>
|
<li>
|
||||||
|
|
Loading…
Reference in New Issue