diff options
-rw-r--r-- | resumejson_converter/example/resume.json | 95 | ||||
-rw-r--r-- | resumejson_converter/example/template.html | 266 | ||||
-rw-r--r-- | resumejson_converter/generators/html.py | 28 | ||||
-rw-r--r-- | resumejson_converter/generators/pdf.py | 10 | ||||
-rw-r--r-- | resumejson_converter/utils/json.py | 25 | ||||
-rw-r--r-- | tests/filters/test_birthday.py | 17 | ||||
-rw-r--r-- | tests/filters/test_clean.py | 12 | ||||
-rw-r--r-- | tests/filters/test_datediff.py | 84 | ||||
-rw-r--r-- | tests/filters/test_dateedit.py | 30 | ||||
-rw-r--r-- | tests/generators/html/test_generate_html.py | 72 | ||||
-rw-r--r-- | tests/generators/pdf/test_generate_pdf.py | 55 | ||||
-rw-r--r-- | tests/utils/json/test_load.py | 52 | ||||
-rw-r--r-- | tests/utils/templates/test_templates.py | 33 |
13 files changed, 753 insertions, 26 deletions
diff --git a/resumejson_converter/example/resume.json b/resumejson_converter/example/resume.json new file mode 100644 index 0000000..91d713e --- /dev/null +++ b/resumejson_converter/example/resume.json @@ -0,0 +1,95 @@ +{ + + "basics": { + "name": "John Doe", + "label": "Programmer", + "picture": "", + "email": "john@gmail.com", + "phone": "(912) 555-4321", + "website": "http://johndoe.com", + "summary": "A summary of John Doe...", + "location": { + "address": "2712 Broadway St", + "postalCode": "CA 94115", + "city": "San Francisco", + "countryCode": "US", + "region": "California" + }, + "profiles": [{ + "network": "Twitter", + "username": "john", + "url": "http://twitter.com/john" + }] + }, + "work": [{ + "company": "Company", + "position": "President", + "website": "http://company.com", + "startDate": "2013-01-01", + "endDate": "2014-01-01", + "summary": "Description...", + "highlights": [ + "Started the company" + ] + }], + "volunteer": [{ + "organization": "Organization", + "position": "Volunteer", + "website": "http://organization.com/", + "startDate": "2012-01-01", + "endDate": "2013-01-01", + "summary": "Description...", + "highlights": [ + "Awarded 'Volunteer of the Month'" + ] + }], + "education": [{ + "institution": "University", + "area": "Software Development", + "studyType": "Bachelor", + "startDate": "2011-01-01", + "endDate": "2013-01-01", + "gpa": "4.0", + "courses": [ + "DB1101 - Basic SQL" + ] + }], + "awards": [{ + "title": "Award", + "date": "2014-11-01", + "awarder": "Company", + "summary": "There is no spoon." + }], + "publications": [{ + "name": "Publication", + "publisher": "Company", + "releaseDate": "2014-10-01", + "website": "http://publication.com", + "summary": "Description..." + }], + "skills": [{ + "name": "Web Development", + "level": "Master", + "keywords": [ + "HTML", + "CSS", + "Javascript" + ] + }], + "languages": [{ + "language": "English", + "fluency": "Native speaker" + }], + "interests": [{ + "name": "Wildlife", + "keywords": [ + "Ferrets", + "Unicorns" + ] + }], + "references": [{ + "name": "Jane Doe", + "reference": "Reference..." + }] + +} diff --git a/resumejson_converter/example/template.html b/resumejson_converter/example/template.html new file mode 100644 index 0000000..d57be01 --- /dev/null +++ b/resumejson_converter/example/template.html @@ -0,0 +1,266 @@ +<!DOCTYPE html> +<html> + <head> + <title>Neodarz's CV</title> + <meta charset="utf-8"> + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/fork-awesome@1.1.7/css/fork-awesome.min.css" integrity="sha256-gsmEoJAws/Kd3CjuOQzLie5Q3yshhvmo7YNtBG7aaEY=" crossorigin="anonymous"> + <style> + html, body { + font-size: 13px; + font-family: "Latin Modern Roman"; + padding: 0; + margin: 0; + height: 100%; + } + + h1 { + margin-top: 7px; + } + + a { + color: #0E5484; + text-decoration: none; + } + + .container { + display: flex; + height: 100%; + } + + .left { + background: #E7E7E7; + flex: 1; + padding: 0 1rem; + } + + .right { + flex: 2; + padding: 0 1rem; + } + + /* Left page */ + .picture { + width: 225px; + height: 225px; + overflow: hidden; + border-radius: 50%; + margin: auto; + margin-top: 20px; + } + + .picture > img { + width: 100%; + margin-top: -5%; + } + + .fa { + background: #0E5484; + width: 23px; + text-align: center; + padding: 4px 0; + border-radius: 6px; + color: #fff; + } + + .infos > div { + margin: 10px 0; + } + + .infos > div > a, .infos > div > span { + margin-left: 10px; + } + + .postecode { + margin-left: 33px !important; + } + + .about { + width: 100%; + height: 25px; + border-bottom: 1px solid black; + margin-top: 50px; + } + + .about > span { + font-size: 25px; + background-color: #E7E7E7; + padding-right: 10px; + } + + .about_subtitle { + margin-left: 10px; + } + + .subtitle { + position: relative; + margin-bottom: 70px; + } + + .name { + color: #0E5484; + font-size: 30px; + } + + .label { + position: absolute; + right: 0; + font-size: 20px; + } + + /* Right page */ + .block { + display: inline-block; + position: relative; + width: 100%; + } + + .date { + display: inline-block; + position: absolute; + left: 0; + } + + .title { + display: inline-block; + position: absolute; + left: 100px; + color: #4D4D4D; + } + + .location { + display: inline-block; + float: right; + } + .content { + margin-left: 100px; + margin-bottom: 13px; + line-height: 15px; + margin-top: 17px; + margin-right: 50px; + } + + .content > p, .content > ul { + margin: 0; + } + + table { + } + + td:nth-child(1) { + vertical-align: top; + } + td:nth-child(2) { + padding-top: 15px; + vertical-align: bottom; + padding-left: 25px; + } + </style> + </head> + <body> + <div class="container"> + <div class="left"> + <div class="picture"><img src="{{ resume.basics.picture }}"></div> + <div class="subtitle"> + <div class="name">{{ resume.basics.name }}</div> + <div class="label">{{ resume.basics.label }}</div> + </div> + <div class="infos"> + <div><i class="fa fa-info" aria-hidden="true"></i><span>{{ resume.basics.birthdate }}</span></div> + <div> + <i class="fa fa-envelope" aria-hidden="true"></i><span>{{ resume.basics.location.address }}</span><br> + <span class="postecode">{{ resume.basics.location.postalCode }} {{ resume.basics.location.city }}</span> + </div> + <div><i class="fa fa-phone" aria-hidden="true"></i><span>{{ resume.basics.phone|clean }}</span></div> + <div><i class="fa fa-globe" aria-hidden="true"></i><a href="{{ resume.basics.website }}">{{ resume.basics.website }}</a></div> + <div><i class="fa fa-git" aria-hidden="true"></i><a href="{{ resume.basics.git }}">{{ resume.basics.git }}</a></div> + <div><i class="fa fa-at" aria-hidden="true"></i><a href="mailto:{{ resume.basics.email }}">{{ resume.basics.email }}</a></div> + </div> + <div class="about"> + <span>A propos</span> + </div> + <p class="about_subtitle">{{ resume.basics.summary }}</p> + </div> + <div class="right"> + <h1>{{ resume.basics.title }}</h1> + <h2>Expériences</h2> + {% for work in resume.work %} + <div class="block"> + <div class="header"> + <div class="date">{{ work.startDate }}</div> + <div class="title">{{ work.company|datediff(work.startDate, work.endDate) }}</div> + <div class="location">{{ work.location }}</div> + </div> + <div class="content"> + <p>{{ work.summary }}</p> + <ul> + {% for highlight in work.highlights %} + <li>{{ highlight }}</li> + {% endfor %} + </ul> + </div> + </div> + {% endfor %} + <h2>Formations</h2> + {% for education in resume.education %} + <div class="block"> + <div class="header"> + <div class="date">{{ education.startDate }}</div> + <div class="title">{{ education.studyType}} - {{ education.area }} - {{ education.institution }}</div> + <div class="location">{{ education.location }}</div> + </div> + <div class="content"> + <ul> + {% for course in education.courses %} + <li>{{ course }}</li> + {% endfor %} + </ul> + </div> + </div> + {% endfor %} + <h2>Compétences</h2> + <table> + {% for skill in resume.skills %} + <tr> + <td>{{ skill.name }}</td> + <td> + {% for keyword in skill.keywords %} + {{ keyword }} + {% if not loop.last %}/{% endif %} + {% endfor %} + </td> + </tr> + {% endfor %} + </table> + <h2>Volontériat</h2> + {% for volunteer in resume.volunteer %} + <div class="block"> + <div class="header"> + <div class="date">{{ volunteer.startDate }}</div> + <div class="title">{{ volunteer.organization }}</div> + </div> + <div class="content"> + <p>{{ volunteer.summary }}</p> + <ul> + {% for highlight in volunteer.highlights %} + <li>{{ highlight }}</li> + {% endfor %} + </ul> + </div> + </div> + {% endfor %} + <h2>Intérets</h2> + <table> + {% for interest in resume.interests %} + <tr> + <td>{{ interest.name }}</td> + {% for keyword in interest.keywords %} + <td>{{ keyword }}</td> + {% endfor %} + </tr> + {% endfor %} + </table> + </div> + </div> + </body> +</html> + diff --git a/resumejson_converter/generators/html.py b/resumejson_converter/generators/html.py index 1b34e91..b1d2802 100644 --- a/resumejson_converter/generators/html.py +++ b/resumejson_converter/generators/html.py @@ -1,14 +1,14 @@ import os.path as path -import sys import codecs import logging import jinja2 +from jinja2.exceptions import TemplateSyntaxError import resumejson_converter.filters as filters -def generate(resume, template, out_path): +def generate(resume, template, out_path=None): """ Return html version of a JSON resume based on a template. @@ -32,18 +32,22 @@ def generate(resume, template, out_path): try: template = env.get_template(template_name) except jinja2.exceptions.TemplateNotFound: - logging.exception("File not found: '{}'!" - .format(template)) - sys.exit(1) + logging.error("File not found: '{}'!" + .format(template)) + raise html = template.render(dict(resume=resume)) - except jinja2.exceptions.TemplateSyntaxError as e: - logging.exception("Syntax error in template file '{}' line {}: {}" - .format(e.name, e.lineno, e.message)) - sys.exit(1) + except TemplateSyntaxError as e: + logging.error("Syntax error in template file '{}' line {}: {}" + .format(e.name, e.lineno, e.message)) + raise else: logging.info("HTML generation done.") if out_path: - file = codecs.open(out_path, "w", "utf-8-sig") - file.write(html) - file.close + try: + file = codecs.open(out_path, "w", "utf-8-sig") + file.write(html) + file.close + except IOError: + logging.error("Permission error when writing html file!") + raise return html diff --git a/resumejson_converter/generators/pdf.py b/resumejson_converter/generators/pdf.py index b8c2905..ed772a8 100644 --- a/resumejson_converter/generators/pdf.py +++ b/resumejson_converter/generators/pdf.py @@ -1,18 +1,17 @@ -import sys import logging import pdfkit -def generate(html): +def generate(html, pdf_output_path="out/out.pdf"): """ Generate a pdf file in out/out.pdf in current folder where main script is executed. + + Use pdf_out_path to select final pdf generated destination. """ logging.info("PDF generation...") - pdf_output_path = "out/out.pdf" - import os if not os.path.exists("out"): os.makedirs("out", exist_ok=True) @@ -34,6 +33,7 @@ def generate(html): configuration=config, options=options) except (IOError, OSError) as e: - logging.exception("Something append: {}".format(e)) + logging.error("Something append: {}".format(e)) + raise else: logging.info("PDF generated at {}".format(pdf_output_path)) diff --git a/resumejson_converter/utils/json.py b/resumejson_converter/utils/json.py index 77e1312..fb44899 100644 --- a/resumejson_converter/utils/json.py +++ b/resumejson_converter/utils/json.py @@ -1,4 +1,3 @@ -import sys import logging import json @@ -34,17 +33,25 @@ def load(json_filepath): """ try: logging.info("JSON parsing...") - with open(json_filepath, 'r') as f: - resume_json = json.load(f) + if type(json_filepath) is str: + with open(json_filepath, 'r') as f: + resume_json = json.load(f) + elif type(json_filepath) is dict: + resume_json = json_filepath + else: + msg = "{} is not a valid type, type accepted are <class 'str'>\n\ + or <class 'dict'>.".format(type(json_filepath)) + logging.error(msg) + raise TypeError except IOError: msg = "Json file could not be loaded. Perhaps file path: \n\ [{}] is incorrect".format(json_filepath) - logging.exception(msg) - sys.exit(1) + logging.error(msg) + raise except ValueError: - logging.exception( + logging.error( "Json file could not be loaded. The syntax is not valid.") - sys.exit(1) + raise else: resume = Resume(resume_json) try: @@ -52,8 +59,8 @@ def load(json_filepath): except InvalidResumeError: msg = "The json resume don't respect standard. Check: \n\ https://jsonresume.org/schema/." - logging.exception(msg) - sys.exit(1) + logging.error(msg) + raise else: logging.info("JSON parsing done.") return JsonObject(resume_json) diff --git a/tests/filters/test_birthday.py b/tests/filters/test_birthday.py new file mode 100644 index 0000000..257f8c4 --- /dev/null +++ b/tests/filters/test_birthday.py @@ -0,0 +1,17 @@ +import pytest + +import resumejson_converter.filters as filters + + +def test_birthday_no_parameter(): + with pytest.raises(TypeError): + filters.birthday() + + +def test_birthday_false_date(): + with pytest.raises(ValueError): + filters.birthday("1990-10-25a") + + +def test_birthday(): + assert filters.birthday("1990-10-25") == "25 octobre 1990" diff --git a/tests/filters/test_clean.py b/tests/filters/test_clean.py new file mode 100644 index 0000000..32553b4 --- /dev/null +++ b/tests/filters/test_clean.py @@ -0,0 +1,12 @@ +import pytest + +import resumejson_converter.filters as filters + + +def test_clean_no_parameter(): + with pytest.raises(TypeError): + filters.clean() + + +def test_clean(): + assert filters.clean("0011223344") == "00 11 22 33 44" diff --git a/tests/filters/test_datediff.py b/tests/filters/test_datediff.py new file mode 100644 index 0000000..845330b --- /dev/null +++ b/tests/filters/test_datediff.py @@ -0,0 +1,84 @@ +import pytest + +import resumejson_converter.filters as filters + + +def test_datediff_only_one_parameter(): + with pytest.raises(TypeError): + filters.datediff() + + +def test_datediff_only_one_parameter(): + with pytest.raises(TypeError): + filters.datediff("Hello World") + + +def test_datediff_only_two_parameter(): + with pytest.raises(TypeError): + filters.datediff("Hello World", "2019-01-02") + + +def test_datediff_false_end_date(): + with pytest.raises(ValueError): + filters.datediff("Hello", "2019-01-02", "2019-01-02d") + + +def test_datediff_false_start_date(): + with pytest.raises(ValueError): + filters.datediff("Hello", "2019-01-02a", "2019-01-02") + + +def test_datediff_one_year(): + assert filters.datediff( + "Hello World!", + "2018-01-02", + "2019-01-03" + ) == "Hello World! - 1 an" + + +def test_datediff_two_year(): + assert filters.datediff( + "Hello World!", + "2017-01-02", + "2019-01-03" + ) == "Hello World! - 2 ans" + + +def test_datediff_two_years(): + assert filters.datediff( + "Hello World!", + "2017-01-02", + "2019-01-03" + ) == "Hello World! - 2 ans" + + +def test_datediff_one_month(): + assert filters.datediff( + "Hello World!", + "2019-01-02", + "2019-02-03" + ) == "Hello World! - 1 mois" + + +def test_datediff_one_week(): + assert filters.datediff( + "Hello World!", + "2019-01-01", + "2019-01-09" + ) == "Hello World! - 1 semaine" + + +def test_datediff_two_weeks(): + assert filters.datediff( + "Hello World!", + "2019-01-01", + "2019-01-16" + ) == "Hello World! - 2 semaines" + + +def test_datediff_two_days(): + assert filters.datediff( + "Hello World!", + "2019-01-02", + "2019-01-04" + ) == "Hello World! - 2 jours" diff --git a/tests/filters/test_dateedit.py b/tests/filters/test_dateedit.py new file mode 100644 index 0000000..5191b26 --- /dev/null +++ b/tests/filters/test_dateedit.py @@ -0,0 +1,30 @@ +import pytest + +import resumejson_converter.filters as filters + + +def test_dateedit_only_one_parameter(): + with pytest.raises(TypeError): + filters.dateedit() + + +def test_dateedit_only_one_parameter(): + with pytest.raises(TypeError): + filters.dateedit("2019-01-01") + + +def test_dateedit_false_date(): + with pytest.raises(ValueError): + filters.dateedit("2019-01-01i", "") + + +def test_dateedit_only_start_date(): + assert filters.dateedit("2019-01-01", "") == "2019 - Auj." + + +def test_dateedite_same_year(): + assert filters.dateedit("2019-01-01", "2019-02-02") == "2019" + + +def test_dateedit_start_date_and_end_date_different(): + assert filters.dateedit("2019-01-01", "2017-05-01") == "2019 - 2017" diff --git a/tests/generators/html/test_generate_html.py b/tests/generators/html/test_generate_html.py new file mode 100644 index 0000000..f1ce676 --- /dev/null +++ b/tests/generators/html/test_generate_html.py @@ -0,0 +1,72 @@ +import pytest +import os + +import resumejson_converter + +from jinja2.exceptions import TemplateSyntaxError + +from resumejson_converter.generators.html import generate as generate_html +from resumejson_converter.utils import json as ujson + + +@pytest.fixture(scope="session") +def gen_template_html(tmpdir_factory): + tmp_file = tmpdir_factory.mktemp( + "resumejson_converter_test" + ).join("template.html") + + with open(tmp_file, "w") as f: + f.write("<p>{{ resume.basics.name }}</p>") + return tmp_file + + +@pytest.fixture(scope="session") +def gen_incorrect_template_html(tmpdir_factory): + tmp_file = tmpdir_factory.mktemp( + "resumejson_converter_test" + ).join("template.html") + + with open(tmp_file, "w") as f: + f.write("<p>{{ resume.basics.name }</p>") + return tmp_file + + +def test_generate_html(): + with pytest.raises(TypeError): + generate_html() + + +def test_generate_html_one_parameters(): + with pytest.raises(TypeError): + generate_html("resume") + + +def test_generate_html(gen_template_html): + resume_path = "{}/example/resume.json".format( + os.path.dirname(resumejson_converter.__file__)) + template_path = gen_template_html + resume = ujson.load(resume_path) + assert generate_html(resume, template_path) == "<p>John Doe</p>" + + +def test_generate_html_incorect_path(): + with pytest.raises(IOError): + generate_html("resume", "/tmp/template") + + +def test_generate_html_incorrect_template(gen_incorrect_template_html): + resume_path = "{}/example/resume.json".format( + os.path.dirname(resumejson_converter.__file__)) + template_path = gen_incorrect_template_html + resume = ujson.load(resume_path) + with pytest.raises(TemplateSyntaxError): + generate_html(resume, template_path) + + +def test_generate_html_out_path_without_right(gen_template_html): + resume_path = "{}/example/resume.json".format( + os.path.dirname(resumejson_converter.__file__)) + template_path = gen_template_html + resume = ujson.load(resume_path) + with pytest.raises(IOError): + generate_html(resume, template_path, "/my_cv.html") diff --git a/tests/generators/pdf/test_generate_pdf.py b/tests/generators/pdf/test_generate_pdf.py new file mode 100644 index 0000000..738508c --- /dev/null +++ b/tests/generators/pdf/test_generate_pdf.py @@ -0,0 +1,55 @@ +import pytest +import os +from pathlib import Path + +import resumejson_converter + +from jinja2.exceptions import TemplateSyntaxError + +from resumejson_converter.generators.html import generate as generate_html +from resumejson_converter.generators.pdf import generate as generate_pdf +from resumejson_converter.utils import json as ujson + + +@pytest.fixture(scope="session") +def gen_template_html(tmpdir_factory): + tmp_file = tmpdir_factory.mktemp( + "resumejson_converter_test" + ).join("template.html") + + with open(tmp_file, "w") as f: + f.write("<p>{{ resume.basics.name }}</p>") + return tmp_file + + +@pytest.fixture(scope="session") +def gen_incorrect_template_html(tmpdir_factory): + tmp_file = tmpdir_factory.mktemp( + "resumejson_converter_test" + ).join("template.html") + + with open(tmp_file, "w") as f: + f.write("<p>{{ resume.basics.name }</p>") + return tmp_file + + +@pytest.fixture(scope="session") +def gen_dest_path(tmpdir_factory): + return tmpdir_factory.mktemp("out").join("out.pdf") + + +def test_generate_pdf(): + with pytest.raises(TypeError): + generate_html() + + +def test_generate_pdf_generated( + gen_template_html, + gen_incorrect_template_html): + resume_path = "{}/example/resume.json".format( + os.path.dirname(resumejson_converter.__file__)) + template_path = gen_template_html + resume = ujson.load(resume_path) + html = generate_html(resume, template_path) + generate_pdf(html, gen_incorrect_template_html) + assert Path(gen_incorrect_template_html).is_file diff --git a/tests/utils/json/test_load.py b/tests/utils/json/test_load.py new file mode 100644 index 0000000..d809069 --- /dev/null +++ b/tests/utils/json/test_load.py @@ -0,0 +1,52 @@ +import pytest +import os + +from jsonresume.exceptions import InvalidResumeError + +import resumejson_converter + +from resumejson_converter.utils import json as ujson + + +@pytest.fixture(scope="session") +def prepare_tmp_json(tmpdir_factory): + tmp_file = tmpdir_factory.mktemp( + "resumejson_converter_test" + ).join("resume_not_standard.json") + + with open(tmp_file, "w") as f: + f.write("{'Hello': 'World!'}") + return tmp_file + + +def test_json_load_no_parameter(): + with pytest.raises(TypeError): + ujson.load() + + +def test_json_load(): + resume_path = "{}/example/resume.json".format( + os.path.dirname(resumejson_converter.__file__)) + assert type(ujson.load(resume_path)) is ujson.JsonObject + + +def test_json_load_incorrect_path(): + with pytest.raises(IOError): + resume_json = "/tmp/sd54f5f48ds4f98ds7g48d468s4f68ds4f.json" + ujson.load(resume_json) + + +def test_json_load_resume_dont_respect_standard(): + with pytest.raises(InvalidResumeError): + resume_path = {"Hello": 'World!'} + ujson.load(resume_path) + + +def test_json_load_array(): + with pytest.raises(TypeError): + ujson.load(["o", "f"]) + + +def test_json_load_resume_dont_respect_standard_from_file(prepare_tmp_json): + with pytest.raises(ValueError): + ujson.load(str(prepare_tmp_json)) diff --git a/tests/utils/templates/test_templates.py b/tests/utils/templates/test_templates.py new file mode 100644 index 0000000..66c77f6 --- /dev/null +++ b/tests/utils/templates/test_templates.py @@ -0,0 +1,33 @@ +import pytest +import os + +from datetime import datetime + +import resumejson_converter + +from resumejson_converter.utils import templates + + +def test_td_format_no_argument(): + with pytest.raises(TypeError): + templates.td_format() + + +def test_td_format_classic(): + startDateTime = datetime.strptime( + "2015-02-20 10:15:20", + '%Y-%m-%d %H:%M:%S') + endDateTime = datetime.strptime( + "2019-03-25 11:16:25", + '%Y-%m-%d %H:%M:%S') + diffDate = endDateTime - startDateTime + + assert templates.td_format(diffDate) == "4 ans, 1 mois, 4 jours, 1 heure, 1 minute, 5 secondes" + + +def test_td_format_week(): + startDateTime = datetime.strptime("2019-02-01", '%Y-%m-%d') + endDateTime = datetime.strptime("2019-02-16", '%Y-%m-%d') + diffDate = endDateTime - startDateTime + + assert templates.td_format(diffDate) == "2 semaines, 24 heures" |