forked from forks/microblog.pub
Start support for authoring articles
This commit is contained in:
parent
e363ae2802
commit
24f3f94056
10 changed files with 132 additions and 8 deletions
|
@ -661,6 +661,7 @@ async def admin_actions_new(
|
|||
is_sensitive: bool = Form(False),
|
||||
visibility: str = Form(),
|
||||
poll_type: str | None = Form(None),
|
||||
name: str | None = Form(None),
|
||||
csrf_check: None = Depends(verify_csrf_token),
|
||||
db_session: AsyncSession = Depends(get_db_session),
|
||||
) -> RedirectResponse:
|
||||
|
@ -687,6 +688,8 @@ async def admin_actions_new(
|
|||
raise ValueError("Question must have at least 2 answers")
|
||||
|
||||
poll_duration_in_minutes = int(raw_form_data["poll_duration"])
|
||||
elif name:
|
||||
ap_type = "Article"
|
||||
|
||||
public_id = await boxes.send_create(
|
||||
db_session,
|
||||
|
@ -700,6 +703,7 @@ async def admin_actions_new(
|
|||
poll_type=poll_type,
|
||||
poll_answers=poll_answers,
|
||||
poll_duration_in_minutes=poll_duration_in_minutes,
|
||||
name=name,
|
||||
)
|
||||
return RedirectResponse(
|
||||
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_answers: list[str] | None = None,
|
||||
poll_duration_in_minutes: int | None = None,
|
||||
name: str | None = None,
|
||||
) -> str:
|
||||
note_id = allocate_outbox_id()
|
||||
published = now().replace(microsecond=0).isoformat().replace("+00:00", "Z")
|
||||
|
@ -367,6 +368,11 @@ async def send_create(
|
|||
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 = {
|
||||
"@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.is_deleted.is_(False),
|
||||
models.OutboxObject.is_hidden_from_homepage.is_(False),
|
||||
models.OutboxObject.ap_type != "Article",
|
||||
)
|
||||
q = select(models.OutboxObject).where(*where)
|
||||
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(
|
||||
db_session: AsyncSession,
|
||||
model_cls: Type[models.Following | models.Follower],
|
||||
|
|
|
@ -142,6 +142,15 @@ footer {
|
|||
max-width: 50px;
|
||||
}
|
||||
}
|
||||
#articles {
|
||||
list-style-type: none;
|
||||
margin: 30px 0;
|
||||
padding: 0 20px;
|
||||
li {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
#notifications, #followers, #following {
|
||||
ul {
|
||||
list-style-type: none;
|
||||
|
|
|
@ -109,6 +109,14 @@ async def render_template(
|
|||
)
|
||||
if is_admin
|
||||
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,
|
||||
"followers_count": await db_session.scalar(
|
||||
select(func.count(models.Follower.id))
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
<div class="box">
|
||||
<nav class="flexbox">
|
||||
<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 %}>
|
||||
{{ ap_type }}
|
||||
</a>
|
||||
|
@ -35,6 +35,13 @@
|
|||
{% endfor %}
|
||||
</select>
|
||||
</p>
|
||||
|
||||
{% if request.query_params.type == "Article" %}
|
||||
<p>
|
||||
<input type="text" style="width:95%" name="name" placeholder="Title">
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
{% for emoji in emojis %}
|
||||
<span class="ji">{{ emoji | emojify(True) | safe }}</span>
|
||||
{% endfor %}
|
||||
|
|
20
app/templates/articles.html
Normal file
20
app/templates/articles.html
Normal file
|
@ -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">
|
||||
<ul>
|
||||
<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("following", "Following") }} <span class="counter">{{ following_count }}</span></li>
|
||||
<li>{{ header_link("get_remote_follow", "Remote follow") }}</li>
|
||||
|
|
|
@ -4,14 +4,14 @@
|
|||
{% block head %}
|
||||
{% if outbox_object %}
|
||||
{% 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="alternate" href="{{ request.url }}" type="application/activity+json">
|
||||
<meta name="description" content="{{ excerpt }}">
|
||||
<meta content="article" property="og:type" />
|
||||
<meta content="{{ outbox_object.url }}" property="og:url" />
|
||||
<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="{{ local_actor.icon_url }}" property="og:image" />
|
||||
<meta content="summary" property="twitter:card" />
|
||||
|
@ -24,7 +24,7 @@
|
|||
{% macro display_replies_tree(replies_tree_node) %}
|
||||
|
||||
{% 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 %}
|
||||
{{ utils.display_object(replies_tree_node.ap_object) }}
|
||||
{% endif %}
|
||||
|
|
|
@ -263,10 +263,20 @@
|
|||
{% endif %}
|
||||
{% 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"] %}
|
||||
<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) }}
|
||||
{% endif %}
|
||||
|
||||
{% 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">
|
||||
|
@ -274,6 +284,15 @@
|
|||
</a>
|
||||
{% 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 %}
|
||||
<p class="p-summary">{{ object.summary | clean_html(object) | safe }}</p>
|
||||
{% endif %}
|
||||
|
@ -352,9 +371,11 @@
|
|||
<li>
|
||||
<div><a href="{{ object.url }}"{% if object.is_from_inbox %} rel="nofollow"{% endif %} class="object-permalink u-url u-uid">permalink</a></div>
|
||||
</li>
|
||||
{% if not is_article_mode %}
|
||||
<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" %}
|
||||
{% set endAt = object.ap_object.endTime | parse_datetime %}
|
||||
<li>
|
||||
|
|
Loading…
Reference in a new issue