Finish support for multiple answers polls

This commit is contained in:
Thomas Sileo 2019-08-15 14:47:41 +02:00
parent c125891681
commit 49ffe3ab75
7 changed files with 67 additions and 38 deletions

View file

@ -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)),

View file

@ -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):

View file

@ -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
# Check for duplicate votes # Hash the choice/answer (so we can use it as a key)
if DB.activities.find_one( answer_key = _answer_key(choice)
{
is_single_choice = bool(question._data.get("oneOf", []))
dup_query = {
"activity.object.actor": create.get_actor().id, "activity.object.actor": create.get_actor().id,
"meta.answer_to": question.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
if DB.activities.find_one(dup_query):
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

View file

@ -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:

View file

@ -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>

View file

@ -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 %}

View file

@ -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)