Add support for voting on Question

This commit is contained in:
Thomas Sileo 2022-07-23 19:02:06 +02:00
parent 4046fa0506
commit d67a44bb59
8 changed files with 187 additions and 6 deletions

View file

@ -0,0 +1,32 @@
"""Keep track of poll answers
Revision ID: c8cbfccf885d
Revises: c9f204f5611d
Create Date: 2022-07-23 19:01:16.289953
"""
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision = 'c8cbfccf885d'
down_revision = 'c9f204f5611d'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('inbox', schema=None) as batch_op:
batch_op.add_column(sa.Column('voted_for_answers', sa.JSON(), nullable=True))
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('inbox', schema=None) as batch_op:
batch_op.drop_column('voted_for_answers')
# ### end Alembic commands ###

View file

@ -35,6 +35,7 @@ AS_EXTENDED_CTX = [
"featured": {"@id": "toot:featured", "@type": "@id"},
"Emoji": "toot:Emoji",
"blurhash": "toot:blurhash",
"votersCount": "toot:votersCount",
# schema
"schema": "http://schema.org#",
"PropertyValue": "schema:PropertyValue",
@ -281,7 +282,7 @@ def wrap_object(activity: RawObject) -> RawObject:
def wrap_object_if_needed(raw_object: RawObject) -> RawObject:
if raw_object["type"] in ["Note"]:
if raw_object["type"] in ["Note", "Article", "Question"]:
return wrap_object(raw_object)
return raw_object

View file

@ -6,6 +6,7 @@ from fastapi import Request
from fastapi import UploadFile
from fastapi.exceptions import HTTPException
from fastapi.responses import RedirectResponse
from loguru import logger
from sqlalchemy import func
from sqlalchemy import select
from sqlalchemy.orm import joinedload
@ -683,6 +684,26 @@ async def admin_actions_new(
)
@router.post("/actions/vote")
async def admin_actions_vote(
request: Request,
redirect_url: str = Form(),
in_reply_to: str = Form(),
csrf_check: None = Depends(verify_csrf_token),
db_session: AsyncSession = Depends(get_db_session),
) -> RedirectResponse:
form_data = await request.form()
names = form_data.getlist("name")
logger.info(f"{names=}")
for name in names:
await boxes.send_vote(
db_session,
in_reply_to=in_reply_to,
name=name,
)
return RedirectResponse(redirect_url, status_code=302)
@unauthenticated_router.get("/login")
async def login(
request: Request,

View file

@ -398,6 +398,112 @@ async def send_create(
return note_id
async def send_vote(
db_session: AsyncSession,
in_reply_to: str,
name: str,
) -> str:
logger.info(f"Send vote {name}")
vote_id = allocate_outbox_id()
published = now().replace(microsecond=0).isoformat().replace("+00:00", "Z")
in_reply_to_object = await get_anybox_object_by_ap_id(db_session, in_reply_to)
if not in_reply_to_object:
raise ValueError(f"Invalid in reply to {in_reply_to=}")
if not in_reply_to_object.ap_context:
raise ValueError("Object has no context")
context = in_reply_to_object.ap_context
to = [in_reply_to_object.actor.ap_id]
note = {
"@context": ap.AS_EXTENDED_CTX,
"type": "Note",
"id": outbox_object_id(vote_id),
"attributedTo": ID,
"name": name,
"to": to,
"cc": [],
"published": published,
"context": context,
"conversation": context,
"url": outbox_object_id(vote_id),
"inReplyTo": in_reply_to,
}
outbox_object = await save_outbox_object(db_session, vote_id, note)
if not outbox_object.id:
raise ValueError("Should never happen")
recipients = await _compute_recipients(db_session, note)
for rcp in recipients:
await new_outgoing_activity(db_session, rcp, outbox_object.id)
await db_session.commit()
return vote_id
async def send_question(
db_session: AsyncSession,
source: str,
) -> str:
note_id = allocate_outbox_id()
published = now().replace(microsecond=0).isoformat().replace("+00:00", "Z")
context = f"{ID}/contexts/" + uuid.uuid4().hex
content, tags, mentioned_actors = await markdownify(db_session, source)
to = [ap.AS_PUBLIC]
cc = [f"{BASE_URL}/followers"]
note = {
"@context": ap.AS_EXTENDED_CTX,
"type": "Question",
"id": outbox_object_id(note_id),
"attributedTo": ID,
"content": content,
"to": to,
"cc": cc,
"published": published,
"context": context,
"conversation": context,
"url": outbox_object_id(note_id),
"tag": tags,
"votersCount": 0,
"endTime": (now() + timedelta(minutes=5)).isoformat().replace("+00:00", "Z"),
"anyOf": [
{
"type": "Note",
"name": "A",
"replies": {"type": "Collection", "totalItems": 0},
},
{
"type": "Note",
"name": "B",
"replies": {"type": "Collection", "totalItems": 0},
},
],
"summary": None,
"sensitive": False,
}
outbox_object = await save_outbox_object(db_session, note_id, note, source=source)
if not outbox_object.id:
raise ValueError("Should never happen")
for tag in tags:
if tag["type"] == "Hashtag":
tagged_object = models.TaggedOutboxObject(
tag=tag["name"][1:],
outbox_object_id=outbox_object.id,
)
db_session.add(tagged_object)
recipients = await _compute_recipients(db_session, note)
for rcp in recipients:
await new_outgoing_activity(db_session, rcp, outbox_object.id)
await db_session.commit()
return note_id
async def send_update(
db_session: AsyncSession,
ap_id: str,

View file

@ -76,7 +76,7 @@ _RESIZED_CACHE: MutableMapping[tuple[str, int], tuple[bytes, str, Any]] = LFUCac
# TODO(ts):
#
# Next:
# - show pending follow request (and prevent double follow?)
# - prevent double accept/double follow
# - UI support for updating posts
# - Article support
# - Fix tests

View file

@ -108,6 +108,7 @@ class InboxObject(Base, BaseObject):
# Link the oubox AP ID to allow undo without any extra query
liked_via_outbox_object_ap_id = Column(String, nullable=True)
announced_via_outbox_object_ap_id = Column(String, nullable=True)
voted_for_answers: Mapped[list[str] | None] = Column(JSON, nullable=True)
is_bookmarked = Column(Boolean, nullable=False, default=False)

View file

@ -24,7 +24,7 @@
{% if inbox_object.ap_type == "Announce" %}
{{ actor_action(inbox_object, "shared") }}
{{ utils.display_object(inbox_object.relates_to_anybox_object) }}
{% elif inbox_object.ap_type in ["Article", "Note", "Video"] %}
{% elif inbox_object.ap_type in ["Article", "Note", "Video", "Page", "Question"] %}
{{ utils.display_object(inbox_object) }}
{% elif inbox_object.ap_type == "Follow" %}
{{ actor_action(inbox_object, "followed you") }}

View file

@ -292,6 +292,12 @@
{% endif %}
{% if object.ap_type == "Question" %}
{% if object.is_from_inbox %}
<form action="{{ request.url_for("admin_actions_vote") }}" method="POST">
{{ embed_csrf_token() }}
{{ embed_redirect_url(object.permalink_id) }}
<input type="hidden" name="in_reply_to" value="{{ object.ap_id }}">
{% endif %}
{% if object.ap_object.oneOf %}
<ul style="list-style-type: none;padding:0;">
@ -299,15 +305,28 @@
{% for item in object.ap_object.oneOf %}
<li style="display:block;">
{% set pct = item | poll_item_pct(object.ap_object.votersCount) %}
<p style="margin:20px 0 10px 0;">{{ item.name | clean_html(object) | safe }} <span style="float:right;">{{ pct }}% <span class="muted">({{ item.replies.totalItems }} votes)</span></span></p>
<p style="margin:20px 0 10px 0;">
{% if object.is_from_inbox %}
<input type="radio" name="name" value="{{ item.name }}">
{% endif %}
{{ item.name | clean_html(object) | safe }} <span style="float:right;">{{ pct }}% <span class="muted">({{ item.replies.totalItems }} votes)</span></span>
</p>
<svg class="poll-bar">
<line x1="0" y1="10px" x2="{{ pct }}%" y2="10px" style="stroke-width: 20px;"></line>
<line x1="0" y1="10px" x2="{{ pct or 1 }}%" y2="10px" style="stroke-width: 20px;"></line>
</svg>
</li>
{% endfor %}
</ul>
{% endif %}
{% if object.is_from_inbox %}
<p class="form">
<input type="submit" value="vote">
</p>
</form>
{% endif %}
{% endif %}
{{ display_og_meta(object) }}
@ -325,7 +344,8 @@
<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>
{% if object.ap_type == "Question" %}
<li>ends {{ object.ap_object.endTime | parse_datetime | timeago }}</li>
{% set endAt = object.ap_object.endTime | parse_datetime %}
<li>ends <time title="{{ endAt.replace(microsecond=0).isoformat() }}">{{ endAt | timeago }}</time></li>
<li>{{ object.ap_object.votersCount }} voters</li>
{% endif %}
{% if is_admin %}