├── MANIFEST.in ├── .gitignore ├── tests ├── not_enough_columns.csv ├── example.csv ├── too_many_columns.csv ├── test_feature.py ├── example_with_utf8.csv ├── test_jinja_helpers.py ├── test_parser.py └── test_dreamwidth.py ├── .coveragerc ├── tox.ini ├── .travis.yml ├── src └── itpe │ ├── jinja_helpers.py │ ├── templates │ └── podfic_template.html │ ├── __init__.py │ └── dreamwidth.py ├── LICENSE ├── README.rst ├── setup.py └── HISTORY.rst /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include src/itpe/templates * 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build artefacts 2 | *.egg-info 3 | build 4 | dist 5 | *.pyc 6 | 7 | # Test artefacts 8 | .tox 9 | .coverage 10 | .hypothesis 11 | .pytest_cache 12 | -------------------------------------------------------------------------------- /tests/not_enough_columns.csv: -------------------------------------------------------------------------------- 1 | from_user,for_user,cover_art,cover_artist,editor,title,title_link,authors,fandom,pairing,warnings,length,mp3_link,podbook_link,podbook_compiler 2 | tw/from_user,dw/for_user 3 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | include = 4 | **/.tox/*/lib/*/site-packages/itpe/*.py 5 | 6 | [report] 7 | fail_under = 100 8 | show_missing = True 9 | exclude_lines = 10 | pragma: no cover 11 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27, py34, py35, py36, lint 3 | 4 | [testenv] 5 | deps = 6 | coverage 7 | hypothesis 8 | pytest 9 | pytest-cov 10 | commands = 11 | coverage run -m py.test tests 12 | coverage report 13 | 14 | [testenv:lint] 15 | basepython = python3.6 16 | deps = flake8 17 | commands = flake8 --max-complexity 10 src 18 | -------------------------------------------------------------------------------- /tests/example.csv: -------------------------------------------------------------------------------- 1 | from_user,for_user,cover_art,cover_artist,editor,title,title_link,authors,fandom,pairing,warnings,length,mp3_link,podbook_link,podbook_compiler 2 | tw/from_user,dw/for_user,https://example.org/cover_art.jpg,dw/cover_artist,dw/editor,My Great Title,https://ao3.org/my_fic,tw/author,Harry Potter,ron/hermione,no warnings,1:30,https://example.org/file.mp3,https://example.org/file.m4b,dw/podbook_compiler 3 | -------------------------------------------------------------------------------- /tests/too_many_columns.csv: -------------------------------------------------------------------------------- 1 | from_user,for_user,cover_art,cover_artist,editor,title,title_link,authors,fandom,pairing,warnings,length,mp3_link,podbook_link,podbook_compiler 2 | tw/from_user,dw/for_user,https://example.org/cover_art.jpg,dw/cover_artist,dw/editor,My Great Title,https://ao3.org/my_fic,tw/author,Harry Potter,ron/hermione,no warnings,1:30,https://example.org/file.mp3,https://example.org/file.m4b,dw/podbook_compiler,an_extra_column,another_extra_column 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | sudo: false 3 | 4 | branches: 5 | only: 6 | - master 7 | 8 | cache: 9 | directories: 10 | - $HOME/.cache/pip 11 | 12 | matrix: 13 | include: 14 | - python: 3.6 15 | env: TOXENV=lint 16 | - python: 2.7 17 | env: TOXENV=py27 18 | - python: 3.4 19 | env: TOXENV=py34 20 | - python: 3.5 21 | env: TOXENV=py35 22 | - python: 3.6 23 | env: TOXENV=py36 24 | 25 | script: 26 | - tox 27 | 28 | install: 29 | - "pip install -U pip setuptools" 30 | - "pip install -U tox" 31 | -------------------------------------------------------------------------------- /tests/test_feature.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 3 | 4 | import os 5 | import sys 6 | import tempfile 7 | 8 | from itpe import main 9 | 10 | 11 | def test_command_line(): 12 | sys.argv = ["itpe", "--input", "tests/example.csv"] 13 | main() 14 | assert os.path.exists("tests/example.html") 15 | os.unlink("tests/example.html") 16 | 17 | 18 | def test_command_line_with_explicit_output(): 19 | out_path = os.path.join(tempfile.mkdtemp(), "example.html") 20 | sys.argv = ["itpe", "--input", "tests/example.csv", "--output", out_path] 21 | main() 22 | assert os.path.exists(out_path) 23 | os.unlink(out_path) 24 | -------------------------------------------------------------------------------- /tests/example_with_utf8.csv: -------------------------------------------------------------------------------- 1 | from_user,for_user,cover_art,cover_artist,editor,title,title_link,authors,fandom,pairing,warnings,length,mp3_link,podbook_link,podbook_compiler 2 | tw/from_user,dw/for_user,https://example.org/cover_art.jpg,dw/cover_artist,dw/editor,My Great Title,https://ao3.org/my_fic,tw/author,Harry Potter,ron/hermione,no warnings,1:30,https://example.org/file.mp3,https://example.org/file.m4b,dw/podbook_compiler 3 | tw/fröm_user,dw/for_usér,https://example.org/cover_art.jpg,dw/cover_artist,dw/editor,My Great Title,https://ao3.org/my_fic,tw/author,Harry Potter,ron/hermione,no warnings,1:30,https://example.org/file.mp3,https://example.org/file.m4b,dw/podbook_compiler 4 | -------------------------------------------------------------------------------- /src/itpe/jinja_helpers.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 2 | 3 | from jinja2 import Environment, PackageLoader 4 | 5 | from .dreamwidth import render_user_links 6 | 7 | 8 | def condense_into_single_line(text): 9 | """ 10 | Remove all the newlines from a block of text, compressing multiple 11 | lines of HTML onto a single line. Used as a Jinja2 filter. 12 | """ 13 | lines = [line.lstrip() for line in text.split('\n')] 14 | return ''.join(lines) 15 | 16 | 17 | def get_jinja2_template(): 18 | """Set up the Jinja2 environment.""" 19 | env = Environment( 20 | loader=PackageLoader('itpe', 'templates'), 21 | trim_blocks=True 22 | ) 23 | 24 | env.filters['condense'] = condense_into_single_line 25 | env.filters['userlinks'] = render_user_links 26 | 27 | template = env.get_template('podfic_template.html') 28 | 29 | return template 30 | -------------------------------------------------------------------------------- /tests/test_jinja_helpers.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 3 | 4 | from hypothesis import given 5 | from hypothesis.strategies import text 6 | import pytest 7 | 8 | from itpe.jinja_helpers import condense_into_single_line, get_jinja2_template 9 | 10 | @given(text()) 11 | def test_condense_into_single_line_removes_newlines(xs): 12 | assert "\n" not in condense_into_single_line(xs) 13 | 14 | 15 | @given(text()) 16 | def test_condense_into_single_line_is_stable(xs): 17 | condensed_xs = condense_into_single_line(xs) 18 | assert condense_into_single_line(condensed_xs) == condensed_xs 19 | 20 | 21 | @pytest.mark.parametrize("text, expected_output", [ 22 | ("foo bar", "foo bar"), 23 | ("foo \nbar", "foo bar"), 24 | ("foo \nbar\n", "foo bar"), 25 | (" foo \nbar\n", "foo bar"), 26 | ]) 27 | def test_condense_into_single_line_strips_from_left(text, expected_output): 28 | assert condense_into_single_line(text) == expected_output 29 | 30 | 31 | def test_can_get_template(): 32 | get_jinja2_template() 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Alex Chan 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ITPE 2 | ==== 3 | 4 | Scripts for generating the masterpost HTML for ITPE (the Informal Twitter 5 | Podfic Exchange). 6 | 7 | Installation 8 | ------------ 9 | 10 | You need to install Python first. You can download Python: 11 | 12 | * from the `official Python website `_ 13 | * using `conda `_ 14 | 15 | There are other ways to install Python, but these are probably the easiest 16 | if you don't already have a working Python installation. 17 | 18 | You may be asked "Python 2.7 or Python 3"? Either should be fine -- these 19 | scripts should work with any version of Python that you can install in 2018. 20 | 21 | Once you have Python, install the scripts by running: 22 | 23 | .. code-block:: console 24 | 25 | pip install itpe 26 | 27 | Usage 28 | ----- 29 | 30 | Once the scripts are installed, run ``itpe --help`` to get a usage message. 31 | 32 | Support 33 | ------- 34 | 35 | If you are having issues, contact Alex Chan 36 | (`alex@alexwlchan.net `_). 37 | 38 | License 39 | ------- 40 | 41 | These scripts are licensed under the MIT license. 42 | -------------------------------------------------------------------------------- /tests/test_parser.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 2 | 3 | import os 4 | 5 | import pytest 6 | 7 | from itpe import get_podfics, Podfic 8 | 9 | 10 | def test_can_get_podfics(): 11 | r = get_podfics("tests/example.csv") 12 | assert len(r) == 1 13 | assert r[0] == Podfic( 14 | "tw/from_user", 15 | "dw/for_user", 16 | "https://example.org/cover_art.jpg", 17 | "dw/cover_artist", 18 | "dw/editor", 19 | "My Great Title", 20 | "https://ao3.org/my_fic", 21 | "tw/author", 22 | "Harry Potter", 23 | "ron/hermione", 24 | "no warnings", 25 | "1:30", 26 | "https://example.org/file.mp3", 27 | "https://example.org/file.m4b", 28 | "dw/podbook_compiler", 29 | ) 30 | 31 | 32 | @pytest.mark.parametrize('path', ["too_many_columns.csv", "not_enough_columns.csv"]) 33 | def test_wrong_number_of_columns_is_valueerror(path): 34 | with pytest.raises(ValueError): 35 | get_podfics(os.path.join("tests", path)) 36 | 37 | 38 | def test_can_read_csv_with_utf8(): 39 | r = get_podfics("tests/example_with_utf8.csv") 40 | assert len(r) == 2 41 | assert r[1].from_user == u"tw/fröm_user" 42 | assert r[1].for_user == u"dw/for_usér" 43 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | 4 | import codecs 5 | import os 6 | 7 | from setuptools import find_packages, setup 8 | 9 | 10 | def local_file(name): 11 | return os.path.relpath(os.path.join(os.path.dirname(__file__), name)) 12 | 13 | 14 | SOURCE = local_file('src') 15 | README = local_file('README.rst') 16 | long_description = codecs.open(README, encoding='utf-8').read() 17 | 18 | 19 | setup( 20 | name='itpe', 21 | version='2019.1', 22 | description='Scripts for generating the master post for ITPE (the Informal Twitter Podfic Exchange)', 23 | long_description=long_description, 24 | url='https://github.com/alexwlchan/itpe', 25 | author='Alex Chan', 26 | author_email='alex@alexwlchan.net', 27 | license='MIT', 28 | 29 | # See https://pypi.python.org/pypi?%3Aaction=list_classifiers 30 | classifiers=[ 31 | 'Development Status :: 5 - Production/Stable', 32 | 'Intended Audience :: Other Audience', 33 | 'License :: OSI Approved :: MIT License', 34 | 'Programming Language :: Python :: 2', 35 | 'Programming Language :: Python :: 2.7', 36 | 'Programming Language :: Python :: 3', 37 | 'Programming Language :: Python :: 3.4', 38 | 'Programming Language :: Python :: 3.5', 39 | 'Programming Language :: Python :: 3.6', 40 | ], 41 | packages=find_packages(SOURCE), 42 | package_dir={'': SOURCE}, 43 | include_package_data=True, 44 | install_requires=[ 45 | "csv23<0.2", 46 | 'docopt', 47 | 'jinja2', 48 | 'termcolor', 49 | ], 50 | 51 | # To provide executable scripts, use entry points in preference to the 52 | # "scripts" keyword. Entry points provide cross-platform support and allow 53 | # pip to create the appropriate form of executable for the target platform. 54 | entry_points={ 55 | 'console_scripts': [ 56 | 'itpe=itpe:main', 57 | ], 58 | }, 59 | ) 60 | -------------------------------------------------------------------------------- /tests/test_dreamwidth.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 2 | 3 | import pytest 4 | 5 | from itpe import dreamwidth as dreamwidth_usernames 6 | 7 | 8 | @pytest.mark.parametrize('input_str, expected', [ 9 | # Strings with spaces or special phrases are skipped 10 | (' ', ''), 11 | ('fish cakes', 'fish cakes'), 12 | ('anonymous', 'anonymous'), 13 | ('anonymous cat', 'anonymous cat'), 14 | 15 | # Strings without a site prefix default to Dreamwidth 16 | ('dog', ''), 17 | ('fish', ''), 18 | ('horse', ''), 19 | ('gerbil', ''), 20 | 21 | # Strings with the dw/ prefix don't include a site= attribute 22 | ('dw/ferret', ''), 23 | ('dw/rabbit', ''), 24 | ('dw/bunny', ''), 25 | 26 | # Strings with non-dw/ prefixes include a site= attribute 27 | ('tw/snake', ''), 28 | ('ao3/newt', ''), 29 | ('tum/iguana', ''), 30 | ("tm/iguana", ""), 31 | 32 | # Comma-separated strings render correctly 33 | ('lion, ff/tiger', ', '), 34 | ('panther, cheetah, puma', ', , '), 35 | ('lj/lynx,', ''), 36 | 37 | # Ampersand-separated strings render correctly 38 | ('rhino &', ''), 39 | ('pin/hippo & elephant', ' & '), 40 | 41 | # Strings with commas and ampersands render correctly 42 | ('fish, squid & clam', ', & '), 43 | ]) 44 | def test_render_user_links(input_str, expected): 45 | assert dreamwidth_usernames.render_user_links(input_str) == expected 46 | 47 | 48 | @pytest.mark.parametrize('bad_input_str', [ 49 | # Strings with >1 slash 50 | 'parrot/budgie/parakeet', 51 | 'cat///mouse', 52 | 53 | # Strings with an unknown site prefix 54 | 'nope/turtle', 55 | 'bad/tortoise', 56 | '/reptile', 57 | ]) 58 | def test_bad_strings_are_valueerror(bad_input_str): 59 | with pytest.raises(ValueError): 60 | dreamwidth_usernames.render_user_links(bad_input_str) 61 | -------------------------------------------------------------------------------- /HISTORY.rst: -------------------------------------------------------------------------------- 1 | Release History 2 | =============== 3 | 4 | 2019.1 (release date: 2018-12-31) 5 | --------------------------------- 6 | 7 | Now the `2018 masterlist `_ 8 | is posted, this release adds two bugfixes in anticipation of 2019, as requested 9 | by the lovely mods: 10 | 11 | - You can now specify a Tumblr handle as ``tm/username``, in addition to the 12 | existing format. 13 | - Cover art won't overflow off the screen on narrow screens (i.e. phones). 14 | 15 | 2018.4 (release date: 2018-12-27) 16 | --------------------------------- 17 | 18 | Another hotfix for Python 2 support. 19 | 20 | 2018.3 (release date: 2018-12-27) 21 | --------------------------------- 22 | 23 | Another attempt to handle non-ASCII characters in Python 2. 24 | 25 | 2018.2 (release date: 2018-12-27) 26 | --------------------------------- 27 | 28 | A failed release to fix a Python 2 Unicode bug. 29 | 30 | 2018.1 (release date: 2018-12-27) 31 | --------------------------------- 32 | 33 | This is an attempt to release the code that *should* have been present 34 | in the 2017.2 release. 35 | 36 | 2017.2 (release date: 2018-01-03) 37 | --------------------------------- 38 | 39 | This releases fixes a bug that was found while building the 2017 masterpost: 40 | 41 | - Don't throw a UnicodeDecodeError if there are unusual characters in the 42 | input CSV. 43 | 44 | 45 | 2017.1 (release date: 2018-01-02) 46 | --------------------------------- 47 | 48 | This releases fixes a bug that was found while building the 2017 masterpost: 49 | 50 | - Actually include the template ``podfic-template.html``. This was present 51 | if you downloaded the repo and worked from that, but not if you installed 52 | through pip. 53 | 54 | 2016.0 (release date: 2017-01-01) 55 | --------------------------------- 56 | 57 | - The first new release. This contains a single change from 2015: the 58 | spreadsheets no longer include an AO3 link for the original fic, so we don't 59 | look for this when we generate the templates. 60 | 61 | 2015.0 62 | ------ 63 | 64 | - The last historical release, for ITPE 2015. Smaller changes compared to 2014. 65 | 66 | 2014.0 67 | ------ 68 | 69 | - Another historical release, recording the state of the scripts used for 70 | ITPE 2014. I don't remember much about these except that I rewrote the 71 | entire script in disgust at what I'd written for 2013. 72 | 73 | 2013.0 74 | ------ 75 | 76 | - First production release! This is a historical release, recording the state 77 | of the scripts used for ITPE 2013. 78 | -------------------------------------------------------------------------------- /src/itpe/templates/podfic_template.html: -------------------------------------------------------------------------------- 1 | {% macro optional_metadata(label, metadata_value, display_text=None) %} 2 | {%- if metadata_value %} 3 | {%- if display_text == None %}{% set display_text = metadata_value %}{% endif %} 4 | 5 | {{ label }}: 6 | {{ display_text }} 7 | {% endif %} 8 | {% endmacro %} 9 | 10 | 11 | 12 | {% filter condense %} 13 | {# title line #} 14 | 15 | {% if podfic.from_user or podfic.for_user %} 16 |

17 | 18 | {% if podfic.from_user %} 19 | From {{ podfic.from_user|userlinks }} 20 | {% if podfic.for_user %} 21 | , for {{ podfic.for_user|userlinks }} 22 | {% endif %} 23 | {% else %} 24 | For {{ podfic.for_user|userlinks }} 25 | {% endif %} 26 | 27 |

28 | {% endif %} 29 | 30 | {% endfilter %} 31 | 32 | 33 | {% filter condense %} 34 | {# cover art #} 35 | 36 | {% filter condense %} 37 | {% if podfic.cover_art %} 38 |
39 | 40 |
41 | {% endif %} 42 | {% endfilter %} 43 | {% if podfic.cover_art and podfic.editor %}
{% endif %} 44 | {% filter condense %} 45 | {% if podfic.cover_artist %} 46 | 47 | Cover artwork by {{ podfic.cover_artist|userlinks }} 48 | 49 | {% endif %} 50 | {% endfilter %} 51 | {% filter condense %} 52 | {% if podfic.editor %} 53 | 54 | Edited by {{ podfic.editor|userlinks }} 55 | 56 | {% endif %} 57 | {% endfilter %} 58 | {% if (podfic.editor and podfic.podbook_compiler) or (podfic.podbook_compiler and podfic.cover_art) %}
{% endif %} 59 | {% filter condense %} 60 | {% if podfic.podbook_compiler %} 61 | 62 | Podbook compiled by {{ podfic.podbook_compiler|userlinks }} 63 | 64 | {% endif %} 65 | {% endfilter %} 66 | 67 | {% endfilter %} 68 | 69 | 70 | 71 | {%- if podfic.title %} 72 | 73 | 74 | 75 | {% endif %} 76 | {% set metadata_value = podfic.authors|userlinks %} 77 | {{ optional_metadata("Authors", podfic.authors|userlinks) }} 78 | {{ optional_metadata("Fandom", podfic.fandom) }} 79 | {{ optional_metadata("Pairing", podfic.pairing) }} 80 | {{ optional_metadata("Warnings", podfic.warnings) }} 81 | {{ optional_metadata("Length", podfic.length)}} 82 | 83 | 84 | 89 | 90 |
Title:{{ podfic.title }}
Download{% if podfic.mp3_link and podfic.podbook_link %}s{% endif %}: 85 | {% if podfic.mp3_link %}MP3{% endif %}{% if podfic.mp3_link and podfic.podbook_link %} | {{'\n'}}{% endif %} 86 | {% if podfic.podbook_link %}Podbook{% endif %}{% if podfic.ao3_link and podfic.podbook_link %} | {{'\n'}}{% endif %} 87 | {% if podfic.ao3_link %}at AO3{{'\n'}}{% endif -%} 88 | {{'\n'}}
91 | -------------------------------------------------------------------------------- /src/itpe/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # encoding = utf-8 3 | """ 4 | Generate the HTML for the #ITPE master post. 5 | 6 | Usage: 7 | itpe_generator.py --input [--output ] [--width=] 8 | itpe_generator.py (-h | --help) 9 | itpe_generator.py --version 10 | 11 | Options: 12 | -h --help Show this screen. 13 | --input CSV file containing the ITPE data. 14 | --output HTML file for writing the generated HTML. 15 | --width= Width of the cover art in px [default: 500px]. 16 | 17 | """ 18 | 19 | import collections 20 | import os 21 | import re 22 | 23 | import csv23 24 | 25 | from .jinja_helpers import get_jinja2_template 26 | 27 | 28 | # Headings for the CSV fields. These don't have to exactly match the spelling/ 29 | # spacing of the CSV, but the order should be the same. We define a set of 30 | # heading names here so that we have consistent names to use in the script 31 | HEADINGS = [ 32 | 'from_user', 33 | 'for_user', 34 | 'cover_art', 35 | 'cover_artist', 36 | 'editor', 37 | 'title', 38 | 'title_link', 39 | 'authors', 40 | 'fandom', 41 | 'pairing', 42 | 'warnings', 43 | 'length', 44 | 'mp3_link', 45 | 'podbook_link', 46 | 'podbook_compiler', 47 | ] 48 | 49 | Podfic = collections.namedtuple('Podfic', HEADINGS) 50 | 51 | 52 | def csv_reader(path): 53 | with csv23.open_reader(path, encoding="utf-8") as reader: 54 | # Skip the first row, which only contains headings. 55 | next(reader) 56 | 57 | for row in reader: 58 | yield row 59 | 60 | 61 | def get_podfics_from_rows(rows): 62 | for idx, r in enumerate(rows): 63 | try: 64 | yield Podfic(*r) 65 | except TypeError: 66 | raise ValueError("Row %d has the wrong number of entries" % idx) 67 | 68 | 69 | def get_podfics(input_file): 70 | """Read a CSV file and return a list of Podfic instances.""" 71 | podfics = [] 72 | 73 | rows = csv_reader(input_file) 74 | 75 | for idx, podfic in enumerate(get_podfics_from_rows(rows)): 76 | print("Reading row %d..." % idx) 77 | podfics.append(podfic) 78 | 79 | return podfics 80 | 81 | 82 | def generate_html(all_podfics, width): 83 | template = get_jinja2_template() 84 | for podfic in all_podfics: 85 | yield template.render(podfic=podfic, width=width) 86 | 87 | 88 | def main(): 89 | 90 | from docopt import docopt 91 | arguments = docopt(__doc__, version="ITPE 2019.1") 92 | 93 | # Strip everything except the digits from the width option, then append 94 | # 'px' for the CSS attribute 95 | arguments['--width'] = re.sub(r'[^0-9]', '', arguments['--width']) + "px" 96 | 97 | # If the caller doesn't give us an output path, guess one based on the 98 | # input file 99 | if arguments[''] is None: 100 | basepath, _ = os.path.splitext(arguments['']) 101 | arguments[''] = basepath + '.html' 102 | 103 | # Get a list of podfics from the input CSV file 104 | all_podfics = get_podfics(arguments['']) 105 | 106 | # Turn those podfics into HTML 107 | podfic_html = generate_html( 108 | all_podfics=all_podfics, 109 | width=arguments['--width'] 110 | ) 111 | 112 | # Write the output HTML, with a
between items to add space 113 | # in the rendered page. 114 | html_to_write = u'\n
\n'.join(podfic_html) 115 | html_bytes = html_to_write.encode("utf8") 116 | with open(arguments[''], "wb") as outfile: 117 | outfile.write(html_bytes) 118 | 119 | print("HTML has been written to %s." % arguments['']) 120 | 121 | 122 | if __name__ == "__main__": # pragma: no cover 123 | main() 124 | -------------------------------------------------------------------------------- /src/itpe/dreamwidth.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | If you want to link to another user on Dreamwidth, you can use a 4 | tag, which gets annotated with a small icon to show that 5 | it's another Dreamwidth account: 6 | 7 | 8 | 9 | You can also specify a site attribute with a variety of services, which 10 | add service-appropriate icons: 11 | 12 | 13 | 14 | See the Dreamwidth FAQs: http://www.dreamwidth.org/support/faqbrowse?faqid=87 15 | 16 | The ITPE template uses a compact syntax, prefixing a username with an 17 | abbreviation and a slash, e.g. "tum/staff" or "tw/support". 18 | 19 | This file contains a function for turning these templae strings into 20 | the full tags. 21 | """ 22 | 23 | from jinja2 import Template 24 | import termcolor 25 | 26 | 27 | SITE_PREFIXES = { 28 | 'ao3': 'archiveofourown.org', 29 | 'blog': 'blogspot.com', 30 | 'dj': 'deadjournal.com', 31 | 'del': 'delicious.com', 32 | 'dev': 'deviantart.com', 33 | 'dw': 'dreamwidth.org', 34 | 'da': 'da', 35 | 'etsy': 'etsy.com', 36 | 'ff': 'fanfiction.net', 37 | 'ink': 'inksome.com', 38 | 'ij': 'insanejournal.com', 39 | 'jf': 'journalfen.com', 40 | 'last': 'last.fm', 41 | 'lj': 'livejournal.com', 42 | 'pin': 'pinboard.in', 43 | 'pk': 'plurk.com', 44 | 'rvl': 'ravelry.com', 45 | 'tw': 'twitter.com', 46 | 'tum': 'tumblr.com', 47 | 'wp': 'wordpress.com', 48 | 49 | # Added as a request for the mods as an alt; they often forget the 50 | # abbreviation is "tum". 51 | "tm": "tumblr.com", 52 | } 53 | 54 | USERLINK = Template("") 56 | 57 | SPECIAL_CASE_NAMES = [ 58 | '(various)', 59 | 'anonymous' 60 | ] 61 | 62 | 63 | def warn(message): 64 | termcolor.cprint(message, color='yellow') 65 | 66 | 67 | def _single_user_link(user_str): 68 | """Renders a short username string into an HTML link.""" 69 | 70 | if not user_str.strip(): 71 | warn("Skipping empty user string.") 72 | return '' 73 | 74 | # If there's a space in the user string, then we drop through a raw 75 | # string (e.g. "the podfic community") 76 | if (' ' in user_str) or (user_str.lower() in SPECIAL_CASE_NAMES): 77 | warn("Skipping user string '%s'" % user_str) 78 | return user_str 79 | 80 | # If there aren't any slashes, then treat it as a Dreamwidth user 81 | if '/' not in user_str: 82 | warn("No site specified for '%s'; assuming Dreamwidth" % user_str) 83 | return USERLINK.render(name=user_str) 84 | 85 | # If there's one slash, split the string and work out the site name. 86 | # Throws a ValueError if there's more than one slash. 87 | try: 88 | short_site, name = user_str.split('/') 89 | except ValueError: 90 | raise ValueError("Invalid user string '%s'" % user_str) 91 | 92 | try: 93 | site = SITE_PREFIXES[short_site] 94 | except KeyError: 95 | raise ValueError("Invalid site prefix in string '%s'" % user_str) 96 | 97 | # If it's a Dreamwidth user, we don't need to specify the site attribute 98 | return USERLINK.render(name=name, site=site) 99 | 100 | 101 | def render_user_links(name_str): 102 | """ 103 | Takes a string of names, possibly separated with commas or ampersands, 104 | and returns an appropriate string of tags. 105 | """ 106 | if '&' in name_str: 107 | components = (render_user_links(part.strip()) 108 | for part in name_str.split('&')) 109 | return ' & '.join(c for c in components if c) 110 | else: 111 | components = (_single_user_link(name.strip()) 112 | for name in name_str.split(',')) 113 | return ', '.join(c for c in components if c) 114 | --------------------------------------------------------------------------------