From 5b7dbc440c343b19816751b68e147249b567bf04 Mon Sep 17 00:00:00 2001 From: Leonardo Giordani Date: Fri, 12 Mar 2021 16:34:46 +0000 Subject: [PATCH] Modernize code and add a test --- .editorconfig | 15 ++ .github/workflows/main.yml | 104 ++++++++++++ .gitignore | 1 + .pre-commit-config.yaml | 31 ++++ CONTRIBUTING.md | 9 ++ README.md | 105 ++++++------ RELEASE.md | 3 + pelican/plugins/share_post/conftest.py | 9 ++ pelican/plugins/share_post/share_post.py | 149 ++++++++++-------- .../plugins/share_post/test_data/article.md | 8 + pelican/plugins/share_post/test_share_post.py | 65 ++++++++ pyproject.toml | 73 +++++++++ tasks.py | 79 ++++++++++ tox.ini | 3 + 14 files changed, 545 insertions(+), 109 deletions(-) create mode 100644 .editorconfig create mode 100644 .github/workflows/main.yml create mode 100644 .gitignore create mode 100644 .pre-commit-config.yaml create mode 100644 CONTRIBUTING.md create mode 100644 RELEASE.md create mode 100644 pelican/plugins/share_post/conftest.py create mode 100644 pelican/plugins/share_post/test_data/article.md create mode 100644 pelican/plugins/share_post/test_share_post.py create mode 100644 pyproject.toml create mode 100644 tasks.py create mode 100644 tox.ini diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..862c1e1 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.py] +max_line_length = 88 + +[*.yml] +indent_size = 2 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..c5b40b3 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,104 @@ +name: build + +on: [push, pull_request] + +env: + PYTEST_ADDOPTS: "--color=yes" + +jobs: + test: + name: Test - ${{ matrix.python-version }} + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.6, 3.7, 3.8, 3.9] + + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Set up Pip cache + uses: actions/cache@v2 + id: pip-cache + with: + path: ~/.cache/pip + key: pip-${{ hashFiles('**/pyproject.toml') }} + - name: Upgrade Pip + run: python -m pip install --upgrade pip + - name: Install Poetry + run: python -m pip install poetry + - name: Set up Poetry cache + uses: actions/cache@v2 + id: poetry-cache + with: + path: ~/.cache/pypoetry/virtualenvs + key: poetry-${{ hashFiles('**/poetry.lock') }} + - name: Install dependencies + run: | + poetry run pip install --upgrade pip + poetry install + - name: Run tests + run: poetry run invoke tests + + + lint: + name: Lint + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: "3.x" + - name: Set Poetry cache + uses: actions/cache@v2 + id: poetry-cache + with: + path: ~/.cache/pypoetry/virtualenvs + key: poetry-${{ hashFiles('**/poetry.lock') }} + - name: Upgrade Pip + run: python -m pip install --upgrade pip + - name: Install Poetry + run: python -m pip install poetry + - name: Install dependencies + run: | + poetry run pip install --upgrade pip + poetry install + - name: Run linters + run: poetry run invoke lint + + + deploy: + name: Deploy + environment: Deployment + needs: [test, lint] + runs-on: ubuntu-latest + if: ${{ github.ref=='refs/heads/main' && github.event_name!='pull_request' }} + + steps: + - uses: actions/checkout@v2 + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: "3.x" + - name: Check release + id: check_release + run: | + python -m pip install --upgrade pip + python -m pip install poetry githubrelease httpx==0.16.1 autopub + echo "##[set-output name=release;]$(autopub check)" + - name: Publish + if: ${{ steps.check_release.outputs.release=='' }} + env: + GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} + PYPI_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + run: | + git remote set-url origin https://$GITHUB_TOKEN@github.com/${{ github.repository }} + autopub prepare + poetry build + autopub commit + autopub githubrelease + poetry publish -u __token__ -p $PYPI_PASSWORD diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c04bc49 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +poetry.lock diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..3338bab --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,31 @@ +# See https://pre-commit.com/hooks.html for info on hooks +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v3.4.0 + hooks: + - id: check-added-large-files + - id: check-ast + - id: check-case-conflict + - id: check-toml + - id: check-yaml + - id: debug-statements + - id: detect-private-key + - id: end-of-file-fixer + - id: trailing-whitespace + + - repo: https://github.com/psf/black + rev: 19.10b0 + hooks: + - id: black + + - repo: https://gitlab.com/pycqa/flake8 + rev: 3.9.0 + hooks: + - id: flake8 + args: [--max-line-length=88] + language_version: python3.7 + + - repo: https://github.com/PyCQA/isort + rev: 5.7.0 + hooks: + - id: isort diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..f96daf4 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,9 @@ +Contributing +============ + +Contributions are welcome and much appreciated. Every little bit helps. You can contribute by improving the documentation, adding missing features, and fixing bugs. You can also help out by reviewing and commenting on [existing issues][]. + +To start contributing to this plugin, review the [Contributing to Pelican][] documentation, beginning with the **Contributing Code** section. + +[existing issues]: https://github.com/pelican-plugins/share-post/issues +[Contributing to Pelican]: https://docs.getpelican.com/en/latest/contribute.html diff --git a/README.md b/README.md index c1651b3..926e737 100644 --- a/README.md +++ b/README.md @@ -1,75 +1,86 @@ -# Share Post +# Share Post: A Plugin for Pelican -A Pelican plugin to create share URLs of article +[![Build Status](https://img.shields.io/github/workflow/status/pelican-plugins/share-post/build)](https://github.com/pelican-plugins/share-post/actions) +[![PyPI Version](https://img.shields.io/pypi/v/pelican-share-post)](https://pypi.org/project/pelican-share-post/) +![License](https://img.shields.io/pypi/l/pelican-share-post?color=blue) -# Author +Share Post is a Pelican plugin that creates share links in articles that allow site visitors to share the current article with others in a privacy-friendly manner. -Copyright (c) Talha Mansoor +Many web sites have share widgets to let readers share posts on social networks. Most of these widgets are used by vendors for online tracking. These widgets can also be visually-distracting and negatively affect readers’ attention. -Author | Talha Mansoor -----------------|----- -Author Email | talha131@gmail.com -Author Homepage | http://onCrashReboot.com -Github Account | https://github.com/talha131 +Share Post creates old-school URLs for some popular sites which your theme can use. These links do not have the ability to track site visitors. They can also be unobtrusive depending on how Pelican theme uses them. -### Contributors -* [Jonathan DEKHTIAR](https://github.com/DEKHTIARJonathan) - contact@jonathandekhtiar.eu -* [Paolo Melchiorre](https://github.com/pauloxnet) - [www.paulox.net](https://www.paulox.net/) +Installation +------------ -## Why do you need it? +This plugin can be installed via: -Almost all website have share widgets to let readers share posts on social -networks. Most of these widgets are used by vendors for online tracking. These -widgets are also visual which quite often become a distraction and negatively -affect readers attention. + python -m pip install pelican-share-post -`share_post` creates old school URLs for some popular sites which your theme -can use. These links do not have the ability to track the users. They can also -be unobtrusive depending on how Pelican theme uses them. +Usage +----- -## Requirements +This plugin adds to each Pelican article a dictionary of URLs that, when followed, allows the reader to easily share the article via specific channels. When activated, the plugin adds the attribute `share_post` to each article with the following format: -`share_post` requires BeautifulSoup - -```bash -pip install beautifulsoup4 +``` python +article.share_post = { + "facebook": "", + "email": "", + "twitter": "", + "diaspora": "", + "linkedin": "", + "hacker-news": "", + "reddit": "", +} ``` -## How to Use +You can then access those variables in your template. For example: -`share_post` adds a dictionary attribute to `article` which can be accessed via -`article.share_post`. Keys of the dictionary are as follows, - -1. `facebook` -1. `email` -1. `twitter` -1. `diaspora` -1. `linkedin` -1. `hacker-news` -1. `reddit` - -## Template Example - -```html +``` html+jinja {% if article.share_post and article.status != 'draft' %}

Share on: - Diaspora* + Diaspora* ❄ - Twitter + Twitter ❄ - Facebook + Facebook ❄ - LinkedIn + LinkedIn ❄ - HackerNews + HackerNews ❄ - Email + Email ❄ - Reddit + Reddit

{% endif %} ``` + +Contributing +------------ + +Contributions are welcome and much appreciated. Every little bit helps. You can contribute by improving the documentation, adding missing features, and fixing bugs. You can also help out by reviewing and commenting on [existing issues][]. + +To start contributing to this plugin, review the [Contributing to Pelican][] documentation, beginning with the **Contributing Code** section. + +[existing issues]: https://github.com/pelican-plugins/share-post/issues +[Contributing to Pelican]: https://docs.getpelican.com/en/latest/contribute.html + + +Contributors +------------ + +* [Talha Mansoor](https://www.oncrashreboot.com) - talha131@gmail.com +* [Jonathan DEKHTIAR](https://github.com/DEKHTIARJonathan) - contact@jonathandekhtiar.eu +* [Justin Mayer](https://justinmayer.com) +* [Leonardo Giordani](https://www.thedigitalcatonline.com) + + +License +------- + +This project is licensed under the MIT license. diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 0000000..9b81d15 --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,3 @@ +Release type: major + +Initial release as namespace plugin diff --git a/pelican/plugins/share_post/conftest.py b/pelican/plugins/share_post/conftest.py new file mode 100644 index 0000000..57affce --- /dev/null +++ b/pelican/plugins/share_post/conftest.py @@ -0,0 +1,9 @@ +import pytest + +from pelican.tests.support import temporary_folder + + +@pytest.fixture +def tmp_folder(): + with temporary_folder() as tf: + yield tf diff --git a/pelican/plugins/share_post/share_post.py b/pelican/plugins/share_post/share_post.py index d2a3c64..f30b088 100644 --- a/pelican/plugins/share_post/share_post.py +++ b/pelican/plugins/share_post/share_post.py @@ -1,100 +1,125 @@ """ -Share Post plugin. +Share Post +========== -This plugin adds share URL to article. These links are textual which means no -online tracking of your readers. +This plugin was originally created by +Talha Mansoor + +This plugin adds social share URLs to each article. """ +# If you want to add a new link_processor please +# have a look at the create_link decorator and +# follow the example of the other functions + +from urllib.parse import quote + from bs4 import BeautifulSoup from pelican import contents, signals from pelican.generators import ArticlesGenerator, PagesGenerator -try: - from urllib.parse import quote -except ImportError: - from urllib import quote +_create_link_functions = [] -def article_title(content): - main_title = BeautifulSoup(content.title, 'html.parser').get_text().strip() - sub_title = '' - if hasattr(content, 'subtitle'): - sub_title = ' ' + BeautifulSoup(content.subtitle, 'html.parser').get_text().strip() # noqa - return quote(('%s%s' % (main_title, sub_title)).encode('utf-8')) +# Use this decorator to mark a function as +# a link creator. The function's prototype shall be +# create_link_NAME(title, url, content) +# where +# NAME is the name of the target, e.g. "dispora" or "facebook" +# title is the HTML-safe title of the content +# url is the content URL +# content is the full object, should you need to extract more data. +def create_link(f): + _create_link_functions.append(f) + return f -def article_url(content): - site_url = content.settings['SITEURL'] - return quote(('%s/%s' % (site_url, content.url)).encode('utf-8')) +@create_link +def create_link_email(title, url, content): + return f"mailto:?subject={title}&body={url}" -def article_summary(content): - return quote(BeautifulSoup(content.summary, 'html.parser').get_text().strip().encode('utf-8')) # noqa +@create_link +def create_link_hacker_news(title, url, content): + return f"https://news.ycombinator.com/submitlink?t={title}&u={url}" -def twitter_hastags(content): - tags = getattr(content, 'tags', []) - hashtags = ','.join((tag.slug for tag in tags)) - return '' if not hashtags else '&hashtags=%s' % hashtags +@create_link +def create_link_diaspora(title, url, content): + return f"https://sharetodiaspora.github.io/?title={title}&url={url}" -def twitter_via(content): - twitter_username = content.settings.get('TWITTER_USERNAME', '') - return '' if not twitter_username else '&via=%s' % twitter_username +@create_link +def create_link_facebook(title, url, content): + return f"https://www.facebook.com/sharer/sharer.php?u={url}" -def share_post(content): +@create_link +def create_link_twitter(title, url, content): + twitter_username = content.settings.get("TWITTER_USERNAME", "") + via = f"&via={twitter_username}" if twitter_username else "" + + tags = getattr(content, "tags", []) + tags = ",".join([tag.slug for tag in tags]) + hashtags = f"&hashtags={tags}" if tags else "" + + return f"https://twitter.com/intent/tweet?text={title}&url={url}{via}{hashtags}" + + +@create_link +def create_link_reddit(title, url, content): + return f"https://www.reddit.com/submit?url={url}&title={title}" + + +@create_link +def create_link_linkedin(title, url, content): + summary = quote( + BeautifulSoup(content.summary, "html.parser").get_text().strip().encode("utf-8") + ) + + return ( + f"https://www.linkedin.com/shareArticle?" + f"mini=true&url={url}&title={title}&" + f"summary={summary}&source={url}" + ) + + +def create_share_links(content): if isinstance(content, contents.Static): return - title = article_title(content) - url = article_url(content) - summary = article_summary(content) - hastags = twitter_hastags(content) - via = twitter_via(content) + main_title = BeautifulSoup(content.title, "html.parser").get_text().strip() - mail_link = 'mailto:?subject=%s&body=%s' % (title, url) - diaspora_link = 'https://sharetodiaspora.github.io/?title=%s&url=%s' % ( - title, url) - facebook_link = 'https://www.facebook.com/sharer/sharer.php?u=%s' % url - twitter_link = 'https://twitter.com/intent/tweet?text=%s&url=%s%s%s' % ( - title, url, via, hastags) - hackernews_link = 'https://news.ycombinator.com/submitlink?t=%s&u=%s' % ( - title, url) - linkedin_link = 'https://www.linkedin.com/shareArticle?mini=true&url=%s&title=%s&summary=%s&source=%s' % ( # noqa - url, title, summary, url - ) - reddit_link = 'https://www.reddit.com/submit?url=%s&title=%s' % ( - url, title) + try: + sub_title = ( + " " + BeautifulSoup(content.subtitle, "html.parser").get_text().strip() + ) + except AttributeError: + sub_title = "" - content.share_post = { - 'diaspora': diaspora_link, - 'twitter': twitter_link, - 'facebook': facebook_link, - 'linkedin': linkedin_link, - 'hacker-news': hackernews_link, - 'email': mail_link, - 'reddit': reddit_link, - } + title = quote(f"{main_title}{sub_title}".encode("utf-8")) + + site_url = content.settings["SITEURL"] + url = quote(f"{site_url}/{content.url}".encode("utf-8")) + + content.share_post = {} + for func in _create_link_functions: + key = func.__name__.replace("create_link_", "").replace("_", "-") + content.share_post[key] = func(title, url, content) def run_plugin(generators): for generator in generators: if isinstance(generator, ArticlesGenerator): for article in generator.articles: - share_post(article) + create_share_links(article) for translation in article.translations: - share_post(translation) + create_share_links(translation) elif isinstance(generator, PagesGenerator): for page in generator.pages: - share_post(page) + create_share_links(page) def register(): - try: - signals.all_generators_finalized.connect(run_plugin) - except AttributeError: - # NOTE: This results in #314 so shouldn't really be relied on - # https://github.com/getpelican/pelican-plugins/issues/314 - signals.content_object_init.connect(share_post) + signals.all_generators_finalized.connect(run_plugin) diff --git a/pelican/plugins/share_post/test_data/article.md b/pelican/plugins/share_post/test_data/article.md new file mode 100644 index 0000000..8c302b2 --- /dev/null +++ b/pelican/plugins/share_post/test_data/article.md @@ -0,0 +1,8 @@ +Title: Test post +Date: 2021-02-01 13:00:00 +Category: test +Tags: foo, bar, foobar +Summary: I have a lot to test +Series: test_series + +Content diff --git a/pelican/plugins/share_post/test_share_post.py b/pelican/plugins/share_post/test_share_post.py new file mode 100644 index 0000000..a35fc59 --- /dev/null +++ b/pelican/plugins/share_post/test_share_post.py @@ -0,0 +1,65 @@ +import os + +from share_post import run_plugin + +from pelican.generators import ArticlesGenerator +from pelican.tests.support import get_context, get_settings + +from . import share_post + + +def test_share_post(tmp_folder): + base_path = os.path.dirname(os.path.abspath(__file__)) + test_data_path = os.path.join(base_path, "test_data") + + share_post.register() + + settings = get_settings() + context = get_context(settings) + + generator = ArticlesGenerator( + context=context, + settings=settings, + path=test_data_path, + theme=settings["THEME"], + output_path=tmp_folder, + ) + generator.generate_context() + + run_plugin([generator]) + + share_links = generator.articles[0].share_post + + assert ( + share_links["diaspora"] + == "https://sharetodiaspora.github.io/?title=Test%20post&url=/test-post.html" + ) + + assert share_links["twitter"] == ( + "https://twitter.com/intent/tweet?text=Test%20post" + "&url=/test-post.html&hashtags=foo,bar,foobar" + ) + + assert ( + share_links["facebook"] + == "https://www.facebook.com/sharer/sharer.php?u=/test-post.html" + ) + assert share_links["linkedin"] == ( + "https://www.linkedin.com/shareArticle?" + "mini=true&url=/test-post.html&title=Test%20post&" + "summary=I%20have%20a%20lot%20to%20test&source=/test-post.html" + ) + + assert ( + share_links["hacker-news"] + == "https://news.ycombinator.com/submitlink?t=Test%20post&u=/test-post.html" + ) + + assert ( + share_links["email"] == "mailto:?subject=Test%20post&body=/test-post.html" + ) + + assert ( + share_links["reddit"] + == "https://www.reddit.com/submit?url=/test-post.html&title=Test%20post" + ) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..4a11803 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,73 @@ +[tool.poetry] +name = "pelican-share-post" +version = "0.0.0" +description = "A Pelican plugin to create share URLs of article" +authors = ["Talha Mansoor "] +license = "MIT" +readme = "README.md" +keywords = ["pelican", "plugin", "social"] +repository = "https://github.com/pelican-plugins/share-post" +documentation = "https://docs.getpelican.com" +packages = [ + { include = "pelican" }, +] + +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Console", + "Framework :: Pelican", + "Framework :: Pelican :: Plugins", + "Intended Audience :: End Users/Desktop", + "Operating System :: OS Independent", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Software Development :: Libraries :: Python Modules", +] + +[tool.poetry.urls] +"Funding" = "https://donate.getpelican.com/" +"Issue Tracker" = "https://github.com/pelican-plugins/share-post/issues" + +[tool.poetry.dependencies] +python = "^3.6" +pelican = "^4.5" +markdown = {version = "^3.2.2", optional = true} +beautifulsoup4 = "^4.9.3" + +[tool.poetry.dev-dependencies] +black = {version = "^19.10b0", allow-prereleases = true} +flake8 = "^3.9" +flake8-black = "^0.2.0" +invoke = "^1.3" +isort = "^5.4" +livereload = "^2.6" +markdown = "^3.2.2" +pytest = "^6.0" +pytest-cov = "^2.8" +pytest-pythonpath = "^0.7.3" +pytest-sugar = "^0.9.4" +Werkzeug = "^1.0" + +[tool.poetry.extras] +markdown = ["markdown"] + +[tool.autopub] +project-name = "Share Post" +git-username = "botpub" +git-email = "botpub@autopub.rocks" +append-github-contributor = true + +[tool.isort] +# Maintain compatibility with Black +profile = "black" +multi_line_output = 3 + +# Sort imports within their section independent of the import type +force_sort_within_sections = true + +# Designate "pelican" as separate import section +known_pelican = "pelican" +sections = "FUTURE,STDLIB,THIRDPARTY,PELICAN,FIRSTPARTY,LOCALFOLDER" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/tasks.py b/tasks.py new file mode 100644 index 0000000..0377b73 --- /dev/null +++ b/tasks.py @@ -0,0 +1,79 @@ +import os +from pathlib import Path +from shutil import which + +from invoke import task + +PKG_NAME = "share_post" +PKG_PATH = Path(f"pelican/plugins/{PKG_NAME}") +ACTIVE_VENV = os.environ.get("VIRTUAL_ENV", None) +VENV_HOME = Path(os.environ.get("WORKON_HOME", "~/.local/share/virtualenvs")) +VENV_PATH = Path(ACTIVE_VENV) if ACTIVE_VENV else (VENV_HOME / PKG_NAME) +VENV = str(VENV_PATH.expanduser()) + +TOOLS = ["poetry", "pre-commit"] +POETRY = which("poetry") if which("poetry") else (VENV / Path("bin") / "poetry") +PRECOMMIT = ( + which("pre-commit") if which("pre-commit") else (VENV / Path("bin") / "pre-commit") +) + + +@task +def tests(c): + """Run the test suite""" + c.run(f"{VENV}/bin/pytest", pty=True) + + +@task +def black(c, check=False, diff=False): + """Run Black auto-formatter, optionally with --check or --diff""" + check_flag, diff_flag = "", "" + if check: + check_flag = "--check" + if diff: + diff_flag = "--diff" + c.run(f"{VENV}/bin/black {check_flag} {diff_flag} {PKG_PATH} tasks.py") + + +@task +def isort(c, check=False, diff=False): + check_flag, diff_flag = "", "" + if check: + check_flag = "-c" + if diff: + diff_flag = "--diff" + c.run(f"{VENV}/bin/isort {check_flag} {diff_flag} .") + + +@task +def flake8(c): + c.run(f"{VENV}/bin/flake8 {PKG_PATH} tasks.py") + + +@task +def lint(c): + isort(c, check=True) + black(c, check=True) + flake8(c) + + +@task +def tools(c): + """Install tools in the virtual environment if not already on PATH""" + for tool in TOOLS: + if not which(tool): + c.run(f"{VENV}/bin/pip install {tool}") + + +@task +def precommit(c): + """Install pre-commit hooks to .git/hooks/pre-commit""" + c.run(f"{PRECOMMIT} install") + + +@task +def setup(c): + c.run(f"{VENV}/bin/pip install -U pip") + tools(c) + c.run(f"{POETRY} install") + precommit(c) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..abbd0dc --- /dev/null +++ b/tox.ini @@ -0,0 +1,3 @@ +[flake8] +max-line-length = 88 +ignore = E203, W503