mirror of
https://git.sr.ht/~tsileo/microblog.pub
synced 2024-11-15 03:04:28 +00:00
Finish support for multiple answers polls
This commit is contained in:
parent
c125891681
commit
49ffe3ab75
7 changed files with 67 additions and 38 deletions
|
@ -493,11 +493,13 @@ def api_new_question() -> _Response:
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
of = _user_api_arg("of")
|
of = _user_api_arg("of")
|
||||||
|
print(of)
|
||||||
if of == "anyOf":
|
if of == "anyOf":
|
||||||
choices["anyOf"] = answers
|
choices["anyOf"] = answers
|
||||||
else:
|
else:
|
||||||
choices["oneOf"] = answers
|
choices["oneOf"] = answers
|
||||||
|
|
||||||
|
print(choices)
|
||||||
raw_question = dict(
|
raw_question = dict(
|
||||||
attributedTo=MY_PERSON.id,
|
attributedTo=MY_PERSON.id,
|
||||||
cc=list(set(cc)),
|
cc=list(set(cc)),
|
||||||
|
|
|
@ -20,7 +20,6 @@ from core.activitypub import SIG_AUTH
|
||||||
from core.activitypub import Box
|
from core.activitypub import Box
|
||||||
from core.activitypub import _actor_hash
|
from core.activitypub import _actor_hash
|
||||||
from core.activitypub import _add_answers_to_question
|
from core.activitypub import _add_answers_to_question
|
||||||
from core.activitypub import no_cache
|
|
||||||
from core.activitypub import post_to_outbox
|
from core.activitypub import post_to_outbox
|
||||||
from core.activitypub import update_cached_actor
|
from core.activitypub import update_cached_actor
|
||||||
from core.db import update_one_activity
|
from core.db import update_one_activity
|
||||||
|
@ -142,8 +141,7 @@ def task_cache_object() -> _Response:
|
||||||
obj = activity.get_object()
|
obj = activity.get_object()
|
||||||
|
|
||||||
# Refetch the object actor (without cache)
|
# Refetch the object actor (without cache)
|
||||||
with no_cache():
|
obj_actor = ap.fetch_remote_activity(obj.get_actor().id, no_cache=True)
|
||||||
obj_actor = ap.fetch_remote_activity(obj.get_actor().id)
|
|
||||||
|
|
||||||
cache = {MetaKey.OBJECT: obj.to_dict(embed=True)}
|
cache = {MetaKey.OBJECT: obj.to_dict(embed=True)}
|
||||||
|
|
||||||
|
@ -269,8 +267,7 @@ def task_cache_actor() -> _Response:
|
||||||
app.logger.info(f"activity={activity!r}")
|
app.logger.info(f"activity={activity!r}")
|
||||||
|
|
||||||
# Reload the actor without caching (in case it got upated)
|
# Reload the actor without caching (in case it got upated)
|
||||||
with no_cache():
|
actor = ap.fetch_remote_activity(activity.get_actor().id, no_cache=True)
|
||||||
actor = ap.fetch_remote_activity(activity.get_actor().id)
|
|
||||||
|
|
||||||
# Fetch the Open Grah metadata if it's a `Create`
|
# Fetch the Open Grah metadata if it's a `Create`
|
||||||
if activity.has_type(ap.ActivityType.CREATE):
|
if activity.has_type(ap.ActivityType.CREATE):
|
||||||
|
|
|
@ -2,12 +2,11 @@ import binascii
|
||||||
import hashlib
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from contextlib import contextmanager
|
import time
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from datetime import timezone
|
from datetime import timezone
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
from typing import Iterator
|
|
||||||
from typing import List
|
from typing import List
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from urllib.parse import urljoin
|
from urllib.parse import urljoin
|
||||||
|
@ -45,22 +44,10 @@ _NewMeta = Dict[str, Any]
|
||||||
|
|
||||||
SIG_AUTH = HTTPSigAuth(KEY)
|
SIG_AUTH = HTTPSigAuth(KEY)
|
||||||
|
|
||||||
_ACTIVITY_CACHE_ENABLED = True
|
|
||||||
ACTORS_CACHE = LRUCache(maxsize=256)
|
ACTORS_CACHE = LRUCache(maxsize=256)
|
||||||
MY_PERSON = ap.Person(**ME)
|
MY_PERSON = ap.Person(**ME)
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
|
||||||
def no_cache() -> Iterator[None]:
|
|
||||||
"""Context manager for disabling the "DB cache" when fetching AP activities."""
|
|
||||||
global _ACTIVITY_CACHE_ENABLED
|
|
||||||
_ACTIVITY_CACHE_ENABLED = False
|
|
||||||
try:
|
|
||||||
yield
|
|
||||||
finally:
|
|
||||||
_ACTIVITY_CACHE_ENABLED = True
|
|
||||||
|
|
||||||
|
|
||||||
def _remove_id(doc: ap.ObjectType) -> ap.ObjectType:
|
def _remove_id(doc: ap.ObjectType) -> ap.ObjectType:
|
||||||
"""Helper for removing MongoDB's `_id` field."""
|
"""Helper for removing MongoDB's `_id` field."""
|
||||||
doc = doc.copy()
|
doc = doc.copy()
|
||||||
|
@ -177,6 +164,7 @@ def post_to_inbox(activity: ap.BaseActivity) -> None:
|
||||||
return
|
return
|
||||||
|
|
||||||
save(Box.INBOX, activity)
|
save(Box.INBOX, activity)
|
||||||
|
time.sleep(1)
|
||||||
logger.info(f"spawning tasks for {activity!r}")
|
logger.info(f"spawning tasks for {activity!r}")
|
||||||
if not activity.has_type([ap.ActivityType.DELETE, ap.ActivityType.UPDATE]):
|
if not activity.has_type([ap.ActivityType.DELETE, ap.ActivityType.UPDATE]):
|
||||||
Tasks.cache_actor(activity.id)
|
Tasks.cache_actor(activity.id)
|
||||||
|
@ -202,6 +190,7 @@ def post_to_outbox(activity: ap.BaseActivity) -> str:
|
||||||
activity.reset_object_cache()
|
activity.reset_object_cache()
|
||||||
|
|
||||||
save(Box.OUTBOX, activity)
|
save(Box.OUTBOX, activity)
|
||||||
|
time.sleep(5)
|
||||||
Tasks.cache_actor(activity.id)
|
Tasks.cache_actor(activity.id)
|
||||||
Tasks.finish_post_to_outbox(activity.id)
|
Tasks.finish_post_to_outbox(activity.id)
|
||||||
return activity.id
|
return activity.id
|
||||||
|
@ -361,8 +350,8 @@ class MicroblogPubBackend(Backend):
|
||||||
logger.info(f"dereference {iri} via HTTP")
|
logger.info(f"dereference {iri} via HTTP")
|
||||||
return super().fetch_iri(iri)
|
return super().fetch_iri(iri)
|
||||||
|
|
||||||
def fetch_iri(self, iri: str, no_cache=False) -> ap.ObjectType:
|
def fetch_iri(self, iri: str, **kwargs: Any) -> ap.ObjectType:
|
||||||
if not no_cache and _ACTIVITY_CACHE_ENABLED:
|
if not kwargs.pop("no_cache", False):
|
||||||
# Fetch the activity by checking the local DB first
|
# Fetch the activity by checking the local DB first
|
||||||
data = self._fetch_iri(iri)
|
data = self._fetch_iri(iri)
|
||||||
logger.debug(f"_fetch_iri({iri!r}) == {data!r}")
|
logger.debug(f"_fetch_iri({iri!r}) == {data!r}")
|
||||||
|
@ -397,21 +386,26 @@ class MicroblogPubBackend(Backend):
|
||||||
logger.info("invalid choice")
|
logger.info("invalid choice")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Hash the choice/answer (so we can use it as a key)
|
||||||
|
answer_key = _answer_key(choice)
|
||||||
|
|
||||||
|
is_single_choice = bool(question._data.get("oneOf", []))
|
||||||
|
dup_query = {
|
||||||
|
"activity.object.actor": create.get_actor().id,
|
||||||
|
"meta.answer_to": question.id,
|
||||||
|
**({} if is_single_choice else {"meta.poll_answer_choice": choice}),
|
||||||
|
}
|
||||||
|
|
||||||
|
print(f"dup_q={dup_query}")
|
||||||
# Check for duplicate votes
|
# Check for duplicate votes
|
||||||
if DB.activities.find_one(
|
if DB.activities.find_one(dup_query):
|
||||||
{
|
|
||||||
"activity.object.actor": create.get_actor().id,
|
|
||||||
"meta.answer_to": question.id,
|
|
||||||
}
|
|
||||||
):
|
|
||||||
logger.info("duplicate response")
|
logger.info("duplicate response")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Update the DB
|
# Update the DB
|
||||||
answer_key = _answer_key(choice)
|
|
||||||
|
|
||||||
DB.activities.update_one(
|
DB.activities.update_one(
|
||||||
{"activity.object.id": question.id},
|
{"meta.object_id": question.id},
|
||||||
{
|
{
|
||||||
"$inc": {
|
"$inc": {
|
||||||
"meta.question_replies": 1,
|
"meta.question_replies": 1,
|
||||||
|
@ -425,6 +419,7 @@ class MicroblogPubBackend(Backend):
|
||||||
{
|
{
|
||||||
"$set": {
|
"$set": {
|
||||||
"meta.answer_to": question.id,
|
"meta.answer_to": question.id,
|
||||||
|
"meta.poll_answer_choice": choice,
|
||||||
"meta.stream": False,
|
"meta.stream": False,
|
||||||
"meta.poll_answer": True,
|
"meta.poll_answer": True,
|
||||||
}
|
}
|
||||||
|
@ -462,7 +457,11 @@ class MicroblogPubBackend(Backend):
|
||||||
# Keep track of our own votes
|
# Keep track of our own votes
|
||||||
DB.activities.update_one(
|
DB.activities.update_one(
|
||||||
{"activity.object.id": reply.id, "box": "inbox"},
|
{"activity.object.id": reply.id, "box": "inbox"},
|
||||||
{"$set": {"meta.voted_for": create.get_object().name}},
|
{
|
||||||
|
"$set": {
|
||||||
|
f"meta.poll_answers_sent.{_answer_key(create.get_object().name)}": True
|
||||||
|
}
|
||||||
|
},
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,6 @@ from little_boxes.errors import NotAnActivityError
|
||||||
|
|
||||||
import config
|
import config
|
||||||
from core.activitypub import _answer_key
|
from core.activitypub import _answer_key
|
||||||
from core.activitypub import no_cache
|
|
||||||
from core.activitypub import post_to_outbox
|
from core.activitypub import post_to_outbox
|
||||||
from core.activitypub import update_cached_actor
|
from core.activitypub import update_cached_actor
|
||||||
from core.db import DB
|
from core.db import DB
|
||||||
|
@ -93,8 +92,7 @@ def _update_process_inbox(update: ap.Update, new_meta: _NewMeta) -> None:
|
||||||
)
|
)
|
||||||
|
|
||||||
elif obj.has_type(ap.ACTOR_TYPES):
|
elif obj.has_type(ap.ACTOR_TYPES):
|
||||||
with no_cache():
|
actor = ap.fetch_remote_activity(obj.get_actor().id, no_cache=True)
|
||||||
actor = ap.fetch_remote_activity(obj.get_actor().id)
|
|
||||||
update_cached_actor(actor)
|
update_cached_actor(actor)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -49,12 +49,10 @@
|
||||||
<option value="10080">7 days</option>
|
<option value="10080">7 days</option>
|
||||||
</select></p>
|
</select></p>
|
||||||
|
|
||||||
<input type="hidden" name="of" value="oneOf" />
|
|
||||||
<!--
|
|
||||||
<p><select name="of">
|
<p><select name="of">
|
||||||
<option value="oneOf">Single choice</option>
|
<option value="oneOf">Single choice</option>
|
||||||
<option value="anyOf">Multiple choices</option>
|
<option value="anyOf">Multiple choices</option>
|
||||||
</select></p>-->
|
</select></p>
|
||||||
|
|
||||||
{% for i in range(4) %}
|
{% for i in range(4) %}
|
||||||
<p><input type="text" name="answer{{i}}" placeholder="Answer #{{i+1}}"></p>
|
<p><input type="text" name="answer{{i}}" placeholder="Answer #{{i+1}}"></p>
|
||||||
|
|
|
@ -88,7 +88,7 @@
|
||||||
{% set pct = cnt * 100.0 / total_votes %}
|
{% set pct = cnt * 100.0 / total_votes %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li class="answer">
|
<li class="answer">
|
||||||
{% if session.logged_in and not meta.voted_for and not (real_end_time | gtnow) and not (obj.id | is_from_outbox) %}
|
{% if session.logged_in and not meta.poll_answers_sent and not (real_end_time | gtnow) and not (obj.id | is_from_outbox) %}
|
||||||
<span><form action="/api/vote" class="action-form" method="POST">
|
<span><form action="/api/vote" class="action-form" method="POST">
|
||||||
<input type="hidden" name="redirect" value="{{ redir }}">
|
<input type="hidden" name="redirect" value="{{ redir }}">
|
||||||
<input type="hidden" name="id" value="{{ obj.id }}">
|
<input type="hidden" name="id" value="{{ obj.id }}">
|
||||||
|
@ -100,10 +100,40 @@
|
||||||
<span class="answer-bar color-menu-background" style="width:{{pct}}%;"></span>
|
<span class="answer-bar color-menu-background" style="width:{{pct}}%;"></span>
|
||||||
<span class="answer-text">
|
<span class="answer-text">
|
||||||
<span>{{ '%0.0f'| format(pct) }}%</span>
|
<span>{{ '%0.0f'| format(pct) }}%</span>
|
||||||
{{ oneOf.name }} {% if oneOf.name == meta.voted_for %}(your vote){% endif %}
|
{{ oneOf.name }} {% if oneOf.name | poll_answer_key in meta.poll_answers_sent %}(your vote){% endif %}
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{% if obj.anyOf %}
|
||||||
|
|
||||||
|
{% for anyOf in obj.anyOf %}
|
||||||
|
{% set pct = 0 %}
|
||||||
|
{% if total_votes > 0 %}
|
||||||
|
{% set cnt = anyOf.name | get_answer_count(obj, meta) %}
|
||||||
|
{% set pct = cnt * 100.0 / total_votes %}
|
||||||
|
{% endif %}
|
||||||
|
<li class="answer">
|
||||||
|
{% set already_voted = anyOf.name | poll_answer_key in meta.poll_answers_sent %}
|
||||||
|
{% if session.logged_in and not already_voted and not (real_end_time | gtnow) and not (obj.id | is_from_outbox) %}
|
||||||
|
<span><form action="/api/vote" class="action-form" method="POST">
|
||||||
|
<input type="hidden" name="redirect" value="{{ redir }}">
|
||||||
|
<input type="hidden" name="id" value="{{ obj.id }}">
|
||||||
|
<input type="hidden" name="choice" value="{{ anyOf.name }}">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
|
<button type="submit" class="bar-item">vote</button>
|
||||||
|
</form></span>
|
||||||
|
{% elif session.logged_in and already_voted %}
|
||||||
|
<span style="position:relative;top:5px;height:10px;width:50px;display:inline-block;"></span>
|
||||||
|
{% endif %}
|
||||||
|
<span class="answer-bar color-menu-background" style="width:{{pct}}%;"></span>
|
||||||
|
<span class="answer-text">
|
||||||
|
<span>{{ '%0.0f'| format(pct) }}%</span>
|
||||||
|
{{ anyOf.name }} {% if anyOf.name | poll_answer_key in meta.poll_answers_sent %}(your vote){% endif %}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
<p><small>
|
<p><small>
|
||||||
{% if real_end_time | gtnow %}
|
{% if real_end_time | gtnow %}
|
||||||
|
|
|
@ -206,6 +206,11 @@ def get_actor(url):
|
||||||
return f"Error<{url}/{exc!r}>"
|
return f"Error<{url}/{exc!r}>"
|
||||||
|
|
||||||
|
|
||||||
|
@filters.app_template_filter()
|
||||||
|
def poll_answer_key(choice: str) -> str:
|
||||||
|
return _answer_key(choice)
|
||||||
|
|
||||||
|
|
||||||
@filters.app_template_filter()
|
@filters.app_template_filter()
|
||||||
def get_answer_count(choice, obj, meta):
|
def get_answer_count(choice, obj, meta):
|
||||||
count_from_meta = meta.get("question_answers", {}).get(_answer_key(choice), 0)
|
count_from_meta = meta.get("question_answers", {}).get(_answer_key(choice), 0)
|
||||||
|
|
Loading…
Reference in a new issue