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")
print(of)
if of == "anyOf":
choices["anyOf"] = answers
else:
choices["oneOf"] = answers
print(choices)
raw_question = dict(
attributedTo=MY_PERSON.id,
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 _actor_hash
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 update_cached_actor
from core.db import update_one_activity
@ -142,8 +141,7 @@ def task_cache_object() -> _Response:
obj = activity.get_object()
# Refetch the object actor (without cache)
with no_cache():
obj_actor = ap.fetch_remote_activity(obj.get_actor().id)
obj_actor = ap.fetch_remote_activity(obj.get_actor().id, no_cache=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}")
# Reload the actor without caching (in case it got upated)
with no_cache():
actor = ap.fetch_remote_activity(activity.get_actor().id)
actor = ap.fetch_remote_activity(activity.get_actor().id, no_cache=True)
# Fetch the Open Grah metadata if it's a `Create`
if activity.has_type(ap.ActivityType.CREATE):

View file

@ -2,12 +2,11 @@ import binascii
import hashlib
import logging
import os
from contextlib import contextmanager
import time
from datetime import datetime
from datetime import timezone
from typing import Any
from typing import Dict
from typing import Iterator
from typing import List
from typing import Optional
from urllib.parse import urljoin
@ -45,22 +44,10 @@ _NewMeta = Dict[str, Any]
SIG_AUTH = HTTPSigAuth(KEY)
_ACTIVITY_CACHE_ENABLED = True
ACTORS_CACHE = LRUCache(maxsize=256)
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:
"""Helper for removing MongoDB's `_id` field."""
doc = doc.copy()
@ -177,6 +164,7 @@ def post_to_inbox(activity: ap.BaseActivity) -> None:
return
save(Box.INBOX, activity)
time.sleep(1)
logger.info(f"spawning tasks for {activity!r}")
if not activity.has_type([ap.ActivityType.DELETE, ap.ActivityType.UPDATE]):
Tasks.cache_actor(activity.id)
@ -202,6 +190,7 @@ def post_to_outbox(activity: ap.BaseActivity) -> str:
activity.reset_object_cache()
save(Box.OUTBOX, activity)
time.sleep(5)
Tasks.cache_actor(activity.id)
Tasks.finish_post_to_outbox(activity.id)
return activity.id
@ -361,8 +350,8 @@ class MicroblogPubBackend(Backend):
logger.info(f"dereference {iri} via HTTP")
return super().fetch_iri(iri)
def fetch_iri(self, iri: str, no_cache=False) -> ap.ObjectType:
if not no_cache and _ACTIVITY_CACHE_ENABLED:
def fetch_iri(self, iri: str, **kwargs: Any) -> ap.ObjectType:
if not kwargs.pop("no_cache", False):
# Fetch the activity by checking the local DB first
data = self._fetch_iri(iri)
logger.debug(f"_fetch_iri({iri!r}) == {data!r}")
@ -397,21 +386,26 @@ class MicroblogPubBackend(Backend):
logger.info("invalid choice")
return
# Check for duplicate votes
if DB.activities.find_one(
{
# 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
if DB.activities.find_one(dup_query):
logger.info("duplicate response")
return
# Update the DB
answer_key = _answer_key(choice)
DB.activities.update_one(
{"activity.object.id": question.id},
{"meta.object_id": question.id},
{
"$inc": {
"meta.question_replies": 1,
@ -425,6 +419,7 @@ class MicroblogPubBackend(Backend):
{
"$set": {
"meta.answer_to": question.id,
"meta.poll_answer_choice": choice,
"meta.stream": False,
"meta.poll_answer": True,
}
@ -462,7 +457,11 @@ class MicroblogPubBackend(Backend):
# Keep track of our own votes
DB.activities.update_one(
{"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

View file

@ -8,7 +8,6 @@ from little_boxes.errors import NotAnActivityError
import config
from core.activitypub import _answer_key
from core.activitypub import no_cache
from core.activitypub import post_to_outbox
from core.activitypub import update_cached_actor
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):
with no_cache():
actor = ap.fetch_remote_activity(obj.get_actor().id)
actor = ap.fetch_remote_activity(obj.get_actor().id, no_cache=True)
update_cached_actor(actor)
else:

View file

@ -49,12 +49,10 @@
<option value="10080">7 days</option>
</select></p>
<input type="hidden" name="of" value="oneOf" />
<!--
<p><select name="of">
<option value="oneOf">Single choice</option>
<option value="anyOf">Multiple choices</option>
</select></p>-->
</select></p>
{% for i in range(4) %}
<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 %}
{% endif %}
<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">
<input type="hidden" name="redirect" value="{{ redir }}">
<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-text">
<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>
</li>
{% 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>
<p><small>
{% if real_end_time | gtnow %}

View file

@ -206,6 +206,11 @@ def get_actor(url):
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()
def get_answer_count(choice, obj, meta):
count_from_meta = meta.get("question_answers", {}).get(_answer_key(choice), 0)