Improvements on polls support

main
Thomas Sileo 2022-07-23 23:06:30 +02:00
parent d67a44bb59
commit 31807233c4
4 changed files with 77 additions and 41 deletions

View File

@ -281,7 +281,7 @@ async def admin_inbox(
) -> templates.TemplateResponse: ) -> templates.TemplateResponse:
where = [ where = [
models.InboxObject.ap_type.not_in( models.InboxObject.ap_type.not_in(
["Accept", "Delete", "Create", "Update", "Undo", "Read"] ["Accept", "Delete", "Create", "Update", "Undo", "Read", "Add", "Remove"]
), ),
models.InboxObject.is_deleted.is_(False), models.InboxObject.is_deleted.is_(False),
] ]
@ -695,12 +695,11 @@ async def admin_actions_vote(
form_data = await request.form() form_data = await request.form()
names = form_data.getlist("name") names = form_data.getlist("name")
logger.info(f"{names=}") logger.info(f"{names=}")
for name in names: await boxes.send_vote(
await boxes.send_vote( db_session,
db_session, in_reply_to=in_reply_to,
in_reply_to=in_reply_to, names=names,
name=name, )
)
return RedirectResponse(redirect_url, status_code=302) return RedirectResponse(redirect_url, status_code=302)

View File

@ -11,6 +11,7 @@ from app.actor import LOCAL_ACTOR
from app.actor import Actor from app.actor import Actor
from app.actor import RemoteActor from app.actor import RemoteActor
from app.media import proxied_media_url from app.media import proxied_media_url
from app.utils.datetime import now
from app.utils.datetime import parse_isoformat from app.utils.datetime import parse_isoformat
@ -190,6 +191,20 @@ class Object:
def has_ld_signature(self) -> bool: def has_ld_signature(self) -> bool:
return bool(self.ap_object.get("signature")) return bool(self.ap_object.get("signature"))
@property
def is_poll_ended(self) -> bool:
if "endTime" in self.ap_object:
return now() > parse_isoformat(self.ap_object["endTime"])
return False
@cached_property
def poll_items(self) -> list[ap.RawObject] | None:
return self.ap_object.get("oneOf") or self.ap_object.get("anyOf")
@cached_property
def is_one_of_poll(self) -> bool:
return bool(self.ap_object.get("oneOf"))
def _to_camel(string: str) -> str: def _to_camel(string: str) -> str:
cased = "".join(word.capitalize() for word in string.split("_")) cased = "".join(word.capitalize() for word in string.split("_"))

View File

@ -401,42 +401,48 @@ async def send_create(
async def send_vote( async def send_vote(
db_session: AsyncSession, db_session: AsyncSession,
in_reply_to: str, in_reply_to: str,
name: str, names: list[str],
) -> str: ) -> str:
logger.info(f"Send vote {name}") logger.info(f"Send vote {names}")
vote_id = allocate_outbox_id()
published = now().replace(microsecond=0).isoformat().replace("+00:00", "Z") 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) in_reply_to_object = await get_inbox_object_by_ap_id(db_session, in_reply_to)
if not in_reply_to_object: if not in_reply_to_object:
raise ValueError(f"Invalid in reply to {in_reply_to=}") raise ValueError(f"Invalid in reply to {in_reply_to=}")
if not in_reply_to_object.ap_context: if not in_reply_to_object.ap_context:
raise ValueError("Object has no context") raise ValueError("Object has no context")
context = in_reply_to_object.ap_context context = in_reply_to_object.ap_context
# TODO: ensure the name are valid?
# Save the answers
in_reply_to_object.voted_for_answers = names
to = [in_reply_to_object.actor.ap_id] to = [in_reply_to_object.actor.ap_id]
note = { for name in names:
"@context": ap.AS_EXTENDED_CTX, vote_id = allocate_outbox_id()
"type": "Note", note = {
"id": outbox_object_id(vote_id), "@context": ap.AS_EXTENDED_CTX,
"attributedTo": ID, "type": "Note",
"name": name, "id": outbox_object_id(vote_id),
"to": to, "attributedTo": ID,
"cc": [], "name": name,
"published": published, "to": to,
"context": context, "cc": [],
"conversation": context, "published": published,
"url": outbox_object_id(vote_id), "context": context,
"inReplyTo": in_reply_to, "conversation": context,
} "url": outbox_object_id(vote_id),
outbox_object = await save_outbox_object(db_session, vote_id, note) "inReplyTo": in_reply_to,
if not outbox_object.id: }
raise ValueError("Should never happen") 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) recipients = await _compute_recipients(db_session, note)
for rcp in recipients: for rcp in recipients:
await new_outgoing_activity(db_session, rcp, outbox_object.id) await new_outgoing_activity(db_session, rcp, outbox_object.id)
await db_session.commit() await db_session.commit()
return vote_id return vote_id

View File

@ -292,24 +292,36 @@
{% endif %} {% endif %}
{% if object.ap_type == "Question" %} {% if object.ap_type == "Question" %}
{% if object.is_from_inbox %} {% if is_admin and object.is_from_inbox and not object.is_poll_ended and not object.voted_for_answers %}
<form action="{{ request.url_for("admin_actions_vote") }}" method="POST"> <form action="{{ request.url_for("admin_actions_vote") }}" method="POST">
{{ embed_csrf_token() }} {{ embed_csrf_token() }}
{{ embed_redirect_url(object.permalink_id) }} {{ embed_redirect_url(object.permalink_id) }}
<input type="hidden" name="in_reply_to" value="{{ object.ap_id }}"> <input type="hidden" name="in_reply_to" value="{{ object.ap_id }}">
{% endif %} {% endif %}
{% if object.ap_object.oneOf %} {% if object.poll_items %}
<ul style="list-style-type: none;padding:0;"> <ul style="list-style-type: none;padding:0;">
{% set items = object.ap_object.oneOf or object.ap_object.anyOf %} {% for item in object.poll_items %}
{% for item in object.ap_object.oneOf %}
<li style="display:block;"> <li style="display:block;">
{% set pct = item | poll_item_pct(object.ap_object.votersCount) %} {% set pct = item | poll_item_pct(object.ap_object.votersCount) %}
{% set can_vote = is_admin and object.is_from_inbox and not object.is_poll_ended and not object.voted_for_answers %}
<p style="margin:20px 0 10px 0;"> <p style="margin:20px 0 10px 0;">
{% if object.is_from_inbox %} {% if can_vote %}
<input type="radio" name="name" value="{{ item.name }}"> <input type="{% if object.is_one_of_poll %}radio{% else %}checkbox{% endif %}" name="name" value="{{ item.name }}" id="{{object.permalink_id}}-{{item.name}}">
<label for="{{object.permalink_id}}-{{item.name}}">
{% endif %} {% endif %}
{{ item.name | clean_html(object) | safe }} <span style="float:right;">{{ pct }}% <span class="muted">({{ item.replies.totalItems }} votes)</span></span>
{{ item.name | clean_html(object) | safe }}
{% if object.voted_for_answers and item.name in object.voted_for_answers %}
<span class="muted" style="padding-left:20px;">you voted for this answer</span>
{% endif %}
{% if can_vote %}
</label>
{% endif %}
<span style="float:right;">{{ pct }}% <span class="muted">({{ item.replies.totalItems }} votes)</span></span>
</p> </p>
<svg class="poll-bar"> <svg class="poll-bar">
<line x1="0" y1="10px" x2="{{ pct or 1 }}%" y2="10px" style="stroke-width: 20px;"></line> <line x1="0" y1="10px" x2="{{ pct or 1 }}%" y2="10px" style="stroke-width: 20px;"></line>
@ -319,7 +331,7 @@
</ul> </ul>
{% endif %} {% endif %}
{% if object.is_from_inbox %} {% if can_vote %}
<p class="form"> <p class="form">
<input type="submit" value="vote"> <input type="submit" value="vote">
</p> </p>
@ -345,8 +357,12 @@
</li> </li>
{% 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>ends <time title="{{ endAt.replace(microsecond=0).isoformat() }}">{{ endAt | timeago }}</time></li> <li>
<li>{{ object.ap_object.votersCount }} voters</li> {% if object.is_poll_ended %}ended{% else %}ends{% endif %} <time title="{{ endAt.replace(microsecond=0).isoformat() }}">{{ endAt | timeago }}</time>
</li>
<li>
{{ object.ap_object.votersCount }} voters
</li>
{% endif %} {% endif %}
{% if is_admin %} {% if is_admin %}
<li> <li>