-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #9 from syeopite/update-npf
Update npf-renderer to 0.12.0
- Loading branch information
Showing
20 changed files
with
500 additions
and
31 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,6 +18,9 @@ | |
} | ||
|
||
#banner { | ||
width: 100%; | ||
height: 100%; | ||
object-fit: cover; | ||
border-radius: 10px 10px 0px 0px; | ||
} | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
"use strict"; | ||
|
||
function requestPollResults(poll_element, pollId) { | ||
return new Promise(function (resolve, reject) { | ||
let post = poll_element.closest(".post"); | ||
const blogName = post.getElementsByClassName("blog-name")[0].innerHTML; | ||
const postId = post.dataset.postId; | ||
|
||
const pollResultsFetch = fetch(`/api/v1/poll/${blogName}/${postId}/${pollId}/results`); | ||
|
||
pollResultsFetch.then((results) => { | ||
return results.json(); | ||
}).then((parsed_results) => { | ||
return resolve(parsed_results); | ||
}); | ||
}) | ||
} | ||
|
||
function fill_poll_results(poll_element, results) { | ||
const sorted_poll_results = Object.entries(results.response.results).sort((a,b) => (a[1]-b[1])).reverse(); | ||
|
||
// First we must find the total number of votes and the winner(s) of the poll | ||
let total_votes = 0; | ||
|
||
// Answer ID to winner status and amount of votes | ||
const processed_poll_results = {}; | ||
const winner_vote_count = sorted_poll_results[0][1]; | ||
|
||
for (let [answer_id, votes] of sorted_poll_results) { | ||
processed_poll_results[answer_id] = {"is_winner": (winner_vote_count == votes), "votes": votes}; | ||
total_votes += votes | ||
} | ||
|
||
const answerIdChoiceElementArray = []; | ||
const pollBody = poll_element.getElementsByClassName("poll-body")[0]; | ||
|
||
for (let choiceElement of pollBody.children) { | ||
answerIdChoiceElementArray.push([choiceElement.dataset.answerId, choiceElement]); | ||
} | ||
|
||
for (let [answer_id, choiceElement] of answerIdChoiceElementArray) { | ||
const answer_results = processed_poll_results[answer_id] || {"is_winner": false, "votes": 0}; | ||
const is_winner = answer_results.is_winner; | ||
const answer_votes = answer_results.votes; | ||
|
||
let numericalVoteProportion | ||
|
||
if (answer_votes == 0 || total_votes == 0) { | ||
numericalVoteProportion = 0; | ||
} else { | ||
numericalVoteProportion = answer_votes/total_votes; | ||
} | ||
|
||
const voteProportionElement = document.createElement("span"); | ||
voteProportionElement.classList.add("vote-proportion"); | ||
voteProportionElement["style"] = `width: ${((numericalVoteProportion) * 100).toFixed(3)}%;`; | ||
|
||
const voteCountElement = document.createElement("span"); | ||
voteCountElement.classList.add("vote-count"); | ||
|
||
// A greater rounding precision is needed here | ||
const comparison = Math.round((numericalVoteProportion) * 10000)/10000 | ||
if ((comparison > 0.001) || comparison == 0) { | ||
voteCountElement.innerHTML = new Intl.NumberFormat("en-US", {style: "percent", maximumSignificantDigits: 3}).format(Math.round((numericalVoteProportion) * 1000)/1000); | ||
} else { | ||
voteCountElement.innerHTML = "< 0.1%"; | ||
} | ||
|
||
if (is_winner) { | ||
choiceElement.classList.add("poll-winner"); | ||
} | ||
|
||
choiceElement.appendChild(voteProportionElement); | ||
choiceElement.appendChild(voteCountElement); | ||
} | ||
|
||
const pollMetadataElement = poll_element.getElementsByClassName("poll-metadata")[0] | ||
|
||
const totalVotesElement = document.createElement("span") | ||
totalVotesElement.innerHTML = `${total_votes} votes` | ||
|
||
const separatorElement = document.createElement("span") | ||
separatorElement.innerHTML = '•' | ||
separatorElement.classList.add("separator") | ||
|
||
const pollTimestamp = pollMetadataElement.children[0] | ||
|
||
const newMetadataContents = document.createDocumentFragment() | ||
newMetadataContents.append(totalVotesElement, separatorElement, pollTimestamp.cloneNode(true)) | ||
|
||
pollTimestamp.replaceWith(newMetadataContents) | ||
|
||
poll_element.classList.add("populated") | ||
} | ||
|
||
const pollBlocks = document.getElementsByClassName("poll-block"); | ||
|
||
|
||
function populate_polls() { | ||
for (let poll of pollBlocks) { | ||
if (poll.classList.contains("populated")) { | ||
continue; | ||
} | ||
|
||
requestPollResults(poll, poll.dataset.pollId).then((answers) => {fill_poll_results(poll, answers)}) | ||
} | ||
}; | ||
|
||
// TODO lazy load polls | ||
populate_polls(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,172 @@ | ||
"""Extensions to npf-renderer to allow asynchronous code and some other custom styling""" | ||
|
||
import dominate | ||
import npf_renderer | ||
|
||
from .helpers import url_handler | ||
|
||
|
||
class NPFParser(npf_renderer.parse.Parser): | ||
def __init__(self, content, poll_callback=None): | ||
super().__init__(content, poll_callback) | ||
|
||
async def _parse_poll_block(self): | ||
poll_id = self.current.get("clientId") or self.current.get("client_id") | ||
if poll_id is None: | ||
raise ValueError("Invalid poll ID") | ||
|
||
question = self.current["question"] | ||
|
||
answers = {} | ||
for raw_ans in self.current["answers"]: | ||
answer_id = raw_ans.get("clientId") or raw_ans.get("client_id") | ||
answer_text = raw_ans.get("answerText") or raw_ans.get("answer_text") | ||
|
||
if answer_id is None or answer_text is None: | ||
raise ValueError("Invalid poll answer") | ||
|
||
answers[answer_id] = answer_text | ||
|
||
votes = None | ||
total_votes = None | ||
|
||
if self.poll_result_callback: | ||
callback_response = await self.poll_result_callback(poll_id) | ||
|
||
#{answer_id: vote_count} | ||
raw_results = callback_response["results"].items() | ||
processed_results = sorted(raw_results, key=lambda item: -item[1]) | ||
|
||
votes_dict = {} | ||
total_votes = 0 | ||
|
||
for index, results in enumerate(processed_results): | ||
vote_count = results[1] | ||
total_votes += vote_count | ||
|
||
if index == 0: | ||
votes_dict[results[0]] = npf_renderer.objects.poll_block.PollResult(is_winner=True, vote_count=vote_count) | ||
else: | ||
votes_dict[results[0]] = npf_renderer.objects.poll_block.PollResult(is_winner=False, vote_count=vote_count) | ||
|
||
votes = npf_renderer.objects.poll_block.PollResults( | ||
timestamp=callback_response["timestamp"], | ||
results=votes_dict | ||
) | ||
|
||
creation_timestamp = self.current["timestamp"] | ||
expires_after = self.current["settings"]["expireAfter"] | ||
|
||
return npf_renderer.objects.poll_block.PollBlock( | ||
poll_id=poll_id, | ||
question=question, | ||
answers=answers, | ||
|
||
creation_timestamp=int(creation_timestamp), | ||
expires_after=int(expires_after), | ||
|
||
votes=votes, | ||
total_votes=total_votes, | ||
) | ||
|
||
async def __parse_block(self): | ||
"""Parses a content block and appends the result to self.parsed_result | ||
Works by routing specific content types to corresponding parse methods | ||
""" | ||
|
||
match self.current["type"]: | ||
case "text": | ||
block = self._parse_text() | ||
case "image": | ||
block = self._parse_image_block() | ||
case "link": | ||
block = self._parse_link_block() | ||
case "audio": | ||
block = self._parse_audio_block() | ||
case "video": | ||
block = self._parse_video_block() | ||
case "poll": | ||
block = await self._parse_poll_block() | ||
case _: | ||
block = unsupported.Unsupported(self.current["type"]) | ||
|
||
self.parsed_result.append(block) | ||
|
||
async def parse(self): | ||
"""Begins the parsing chain and returns the final list of parsed objects""" | ||
while self.next(): | ||
await self.__parse_block() | ||
|
||
return self.parsed_result | ||
|
||
|
||
class NPFFormatter(npf_renderer.format.Formatter): | ||
def __init__(self, content, layout=None, blog_name=None, post_id=None, *, url_handler=None, forbid_external_iframes=False): | ||
super().__init__(content, layout, url_handler=url_handler, forbid_external_iframes=forbid_external_iframes) | ||
|
||
# We store the blog and post ID as to be able to render a link to | ||
# fetch poll results for JS disabled users | ||
self.blog_name = blog_name | ||
self.post_id = post_id | ||
|
||
def _format_poll(self, block): | ||
poll_html = super()._format_poll(block) | ||
poll_html["data-poll-id"] = block.poll_id | ||
|
||
poll_body = poll_html[1] | ||
for index, answer_id in enumerate(block.answers.keys()): | ||
poll_body[index]["data-answer-id"] = answer_id | ||
|
||
if (self.blog_name and self.post_id) and not block.votes: | ||
poll_footer = poll_html[2] | ||
no_script_fallback = dominate.tags.noscript( | ||
dominate.tags.a( | ||
"See Results", | ||
href=f"/{self.blog_name}/{self.post_id}?fetch_polls=true", | ||
cls="toggle-poll-results" | ||
) | ||
) | ||
|
||
poll_footer.children.insert(0, no_script_fallback) | ||
|
||
return poll_html | ||
|
||
|
||
async def format_npf(contents, layouts=None, blog_name=None, post_id=None,*, poll_callback=None): | ||
"""Wrapper around npf_renderer.format_npf for extra functionalities | ||
- Replaces internal Parser and Formatter with the modified variants above | ||
- Accepts extra arguments to add additional details to formatted results | ||
- Automatically sets Priviblur-specific rendering arguments | ||
Arguments (new): | ||
blog_name: | ||
Name of the blog the post comes from. This is used to render links to the parent post | ||
post_id: | ||
Unique ID of the post. This is used to render links to the parent post | ||
""" | ||
try: | ||
contents = await NPFParser(contents, poll_callback=poll_callback).parse() | ||
if layouts: | ||
layouts = npf_renderer.parse.LayoutParser(layouts).parse() | ||
|
||
contains_render_errors = False | ||
|
||
formatted = NPFFormatter( | ||
contents, layouts, | ||
blog_name=blog_name, | ||
post_id=post_id, | ||
url_handler=url_handler, | ||
forbid_external_iframes=True, | ||
).format() | ||
|
||
except npf_renderer.exceptions.RenderErrorDisclaimerError as e: | ||
contains_render_errors = True | ||
formatted = e.rendered_result | ||
assert formatted is not None | ||
except Exception as e: | ||
formatted = dominate.tags.div(cls="post-body has-error") | ||
contains_render_errors = True | ||
|
||
return contains_render_errors, formatted.render(pretty=False) |
Oops, something went wrong.