mirror of https://github.com/eyhc1/rendercv.git
Merge branch 'main' into pr/flowrolltide/80-2
This commit is contained in:
commit
e119f23acb
|
@ -88,4 +88,23 @@ In some of the tests:
|
||||||
|
|
||||||
When the `testdata` folder needs to be updated, it can be manually regenerated by setting `update_testdata` to `True` in `conftest.py` and running the tests.
|
When the `testdata` folder needs to be updated, it can be manually regenerated by setting `update_testdata` to `True` in `conftest.py` and running the tests.
|
||||||
|
|
||||||
Whenever the `testdata` folder is generated, the files should be reviewed manually to ensure everything works as expected.
|
Whenever the `testdata` folder is generated, the files should be reviewed manually to ensure everything works as expected.
|
||||||
|
|
||||||
|
|
||||||
|
## Frequently Asked Questions (FAQ)
|
||||||
|
|
||||||
|
### How can I add a new social network to RenderCV?
|
||||||
|
|
||||||
|
To add a new social network to RenderCV, go to the `rendercv/data_models.py` file and follow these steps:
|
||||||
|
|
||||||
|
1. Append the social network name (for example, "Facebook") to the `SocialNetworkName` type.
|
||||||
|
2. If necessary, implement its username validation in the `SocialNetwork.check_username` method.
|
||||||
|
3. Implement its URL generation using the `SocialNetwork.url` method. If the URL can be generated by appending the username to a hostname, only update `url_dictionary`.
|
||||||
|
4. Finally, include the $\LaTeX$ icon of the social network to the `icon_dictionary` in the `CurriculumVitae.connections` method. RenderCV uses the [`fontawesome5`](https://ctan.org/pkg/fontawesome5?lang=en) package. The available icons can be seen [here](https://fosszone.csd.auth.gr/CTAN/fonts/fontawesome5/doc/fontawesome5.pdf).
|
||||||
|
|
||||||
|
Then, the tests should be implemented for the new social network with the following steps:
|
||||||
|
|
||||||
|
1. Go to `tests/test_data_models.py` and update `test_social_network_url` accordingly.
|
||||||
|
2. Go to `tests/conftest.py` and add the new social network to `rendercv_filled_curriculum_vitae_data_model`.
|
||||||
|
3. Set `update_testdata` to `True` in `conftest.py` and run the tests to update the `testdata` folder.
|
||||||
|
4. Review the updated `testdata` folder manually to ensure everything works as expected. Then, set `update_testdata` to `False` and push the changes.
|
||||||
|
|
|
@ -17,9 +17,9 @@ locale_catalog:
|
||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
- The `cv` section is mandatory. It contains the **content of the CV**.
|
- The `cv` field is mandatory. It contains the **content of the CV**.
|
||||||
- The `design` section is optional. It contains the **design options of the CV**. If you don't provide a `design` section, RenderCV will use the default design options with the `classic` theme.
|
- The `design` field is optional. It contains the **design options of the CV**. If you don't provide a `design` field, RenderCV will use the default design options with the `classic` theme.
|
||||||
- The `locale_catalog` section is optional. You can provide translations for some of the strings used in the CV, for example, month abbreviations. RenderCV will use English strings if you don't provide a `locale_catalog` section.
|
- The `locale_catalog` field is optional. You can provide translations for some of the strings used in the CV, for example, month abbreviations. RenderCV will use English strings if you don't provide a `locale_catalog` field.
|
||||||
|
|
||||||
!!! tip
|
!!! tip
|
||||||
To maximize your productivity while editing the input YAML file, set up RenderCV's JSON Schema in your IDE. It will validate your inputs on the fly and give auto-complete suggestions.
|
To maximize your productivity while editing the input YAML file, set up RenderCV's JSON Schema in your IDE. It will validate your inputs on the fly and give auto-complete suggestions.
|
||||||
|
@ -40,9 +40,9 @@ locale_catalog:
|
||||||
```
|
```
|
||||||
3. Press `Ctrl + Space` to see the auto-complete suggestions.
|
3. Press `Ctrl + Space` to see the auto-complete suggestions.
|
||||||
|
|
||||||
## "`cv`" section of the YAML input
|
## "`cv`" field
|
||||||
|
|
||||||
The `cv` section of the YAML input starts with generic information, as shown below.
|
The `cv` field of the YAML input starts with generic information, as shown below.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
cv:
|
cv:
|
||||||
|
@ -60,7 +60,7 @@ cv:
|
||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
1. The available social networks are: {{available_social_networks}}. You can add more social networks by following the same pattern. The social network icons are automatically added to the header of the CV.
|
1. The available social networks are: {{available_social_networks}}.
|
||||||
|
|
||||||
None of the values above are required. You can omit any or all of them, and RenderCV will adapt to your input. These generic fields are used in the header of the CV.
|
None of the values above are required. You can omit any or all of them, and RenderCV will adapt to your input. These generic fields are used in the header of the CV.
|
||||||
|
|
||||||
|
@ -84,7 +84,9 @@ cv:
|
||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
The `sections` field is a dictionary where the keys are the section titles, and the values are lists. Each item of the list is an entry for that section.
|
### "`cv.sections`" field
|
||||||
|
|
||||||
|
The `cv.sections` field is a dictionary where the keys are the section titles, and the values are lists. Each item of the list is an entry for that section.
|
||||||
|
|
||||||
Here is an example:
|
Here is an example:
|
||||||
|
|
||||||
|
@ -121,7 +123,7 @@ The available entry types are: [`EducationEntry`](#education-entry), [`Experienc
|
||||||
Each entry type is a different object (a dictionary). Below, you can find all the entry types along with their optional/mandatory fields and how they appear in each built-in theme.
|
Each entry type is a different object (a dictionary). Below, you can find all the entry types along with their optional/mandatory fields and how they appear in each built-in theme.
|
||||||
|
|
||||||
{% for entry_name, entry in showcase_entries.items() %}
|
{% for entry_name, entry in showcase_entries.items() %}
|
||||||
### {{ entry_name }}
|
#### {{ entry_name }}
|
||||||
|
|
||||||
{% if entry_name == "Education Entry" %}
|
{% if entry_name == "Education Entry" %}
|
||||||
|
|
||||||
|
@ -166,8 +168,10 @@ Each entry type is a different object (a dictionary). Below, you can find all th
|
||||||
- `doi`: The DOI of the publication.
|
- `doi`: The DOI of the publication.
|
||||||
- `journal`: The journal of the publication.
|
- `journal`: The journal of the publication.
|
||||||
- `date`: The date as a custom string or in `YYYY-MM-DD`, `YYYY-MM`, or `YYYY` format.
|
- `date`: The date as a custom string or in `YYYY-MM-DD`, `YYYY-MM`, or `YYYY` format.
|
||||||
|
|
||||||
{% elif entry_name == "Normal Entry" %}
|
{% elif entry_name == "Normal Entry" %}
|
||||||
|
|
||||||
|
|
||||||
**Mandatory Fields:**
|
**Mandatory Fields:**
|
||||||
|
|
||||||
- `name`: The name of the entry.
|
- `name`: The name of the entry.
|
||||||
|
@ -179,6 +183,7 @@ Each entry type is a different object (a dictionary). Below, you can find all th
|
||||||
- `end_date`: The end date in `YYYY-MM-DD`, `YYYY-MM`, or `YYYY` format or "present".
|
- `end_date`: The end date in `YYYY-MM-DD`, `YYYY-MM`, or `YYYY` format or "present".
|
||||||
- `date`: The date as a custom string or in `YYYY-MM-DD`, `YYYY-MM`, or `YYYY` format. This will override `start_date` and `end_date`.
|
- `date`: The date as a custom string or in `YYYY-MM-DD`, `YYYY-MM`, or `YYYY` format. This will override `start_date` and `end_date`.
|
||||||
- `highlights`: A list of bullet points.
|
- `highlights`: A list of bullet points.
|
||||||
|
|
||||||
{% elif entry_name == "OneLineEntry" %}
|
{% elif entry_name == "OneLineEntry" %}
|
||||||
|
|
||||||
**Mandatory Fields:**
|
**Mandatory Fields:**
|
||||||
|
@ -204,14 +209,14 @@ Each entry type is a different object (a dictionary). Below, you can find all th
|
||||||
{{ entry["yaml"] }}
|
{{ entry["yaml"] }}
|
||||||
```
|
```
|
||||||
{% for figure in entry["figures"] %}
|
{% for figure in entry["figures"] %}
|
||||||
`{{ figure["theme"] }}` theme:
|
=== "`{{ figure["theme"] }}` theme"
|
||||||
![figure["alt_text"]]({{ figure["path"] }})
|
![figure["alt_text"]]({{ figure["path"] }})
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
## "`design`" section of the YAML input
|
## "`design`" field
|
||||||
|
|
||||||
The `cv` part of the input contains your content, and the `design` part contains your design options. The `design` part starts with a theme name. Currently, the available themes are: {{available_themes}}. However, custom themes can also be used (see [here](index.md#creating-custom-themes-with-the-create-theme-command).)
|
The `cv` field of the input contains your content, and the `design` field contains your design options. The `design` field starts with a theme name. Currently, the available themes are: {{available_themes}}. However, custom themes can also be used (see [here](index.md#creating-custom-themes-with-the-create-theme-command).)
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
design:
|
design:
|
||||||
|
@ -221,7 +226,7 @@ design:
|
||||||
|
|
||||||
Each theme may have different options for design. `classic`, `sb2nov`, and `engineeringresumes` almost use identical options, but `moderncv` is slightly different. Please use an IDE that supports JSON schema to avoid missing any available options for the theme (see [above](#structure-of-the-yaml-input-file)).
|
Each theme may have different options for design. `classic`, `sb2nov`, and `engineeringresumes` almost use identical options, but `moderncv` is slightly different. Please use an IDE that supports JSON schema to avoid missing any available options for the theme (see [above](#structure-of-the-yaml-input-file)).
|
||||||
|
|
||||||
An example `design` part for a `classic` theme is shown below:
|
An example `design` field for a `classic` theme is shown below:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
design:
|
design:
|
||||||
|
@ -263,9 +268,9 @@ design:
|
||||||
vertical_between_name_and_connections: 0.3 cm
|
vertical_between_name_and_connections: 0.3 cm
|
||||||
```
|
```
|
||||||
|
|
||||||
## "`locale_catalog`" section of the YAML input
|
## "`locale_catalog`" field
|
||||||
|
|
||||||
This section is what makes RenderCV a multilingual tool. RenderCV uses some English strings to render PDFs. For example, it takes the dates in ISO format (`2020-01-01`) and converts them into human-friendly strings (`"Jan. 2020"`). However, you can override these strings for your own language or needs with the `locale_catalog` section.
|
This field is what makes RenderCV a multilingual tool. RenderCV uses some English strings to render PDFs. For example, it takes the dates in ISO format (`2020-01-01`) and converts them into human-friendly strings (`"Jan. 2020"`). However, you can override these strings for your own language or needs with the `locale_catalog` field.
|
||||||
|
|
||||||
Here is an example:
|
Here is an example:
|
||||||
|
|
||||||
|
@ -290,4 +295,4 @@ locale_catalog:
|
||||||
years: years # translation of the word "years"
|
years: years # translation of the word "years"
|
||||||
present: present # translation of the word "present"
|
present: present # translation of the word "present"
|
||||||
to: to # translation of the word "to"
|
to: to # translation of the word "to"
|
||||||
```
|
```
|
||||||
|
|
101
rendercv/cli.py
101
rendercv/cli.py
|
@ -6,6 +6,7 @@ output.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import urllib.request
|
||||||
import pathlib
|
import pathlib
|
||||||
from typing import Annotated, Callable, Optional
|
from typing import Annotated, Callable, Optional
|
||||||
import re
|
import re
|
||||||
|
@ -37,11 +38,62 @@ app = typer.Typer(
|
||||||
rich_markup_mode="rich",
|
rich_markup_mode="rich",
|
||||||
add_completion=False,
|
add_completion=False,
|
||||||
invoke_without_command=True, # to make rendercv --version work
|
invoke_without_command=True, # to make rendercv --version work
|
||||||
|
no_args_is_help=True,
|
||||||
|
context_settings={"help_option_names": ["-h", "--help"]},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_latest_version_number_from_pypi() -> Optional[str]:
|
||||||
|
"""Get the latest version number of RenderCV from PyPI.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```python
|
||||||
|
get_latest_version_number_from_pypi()
|
||||||
|
```
|
||||||
|
will return:
|
||||||
|
`#!python "1.1"`
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Optional[str]: The latest version number of RenderCV from PyPI. Returns None if
|
||||||
|
the version number cannot be fetched.
|
||||||
|
"""
|
||||||
|
version = None
|
||||||
|
url = "https://pypi.org/pypi/rendercv/json"
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(url) as response:
|
||||||
|
data = response.read()
|
||||||
|
encoding = response.info().get_content_charset("utf-8")
|
||||||
|
json_data = json.loads(data.decode(encoding))
|
||||||
|
version = json_data["info"]["version"]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return version
|
||||||
|
|
||||||
|
|
||||||
|
def warn_if_new_version_is_available() -> bool:
|
||||||
|
"""Check if a new version of RenderCV is available and print a warning message if
|
||||||
|
there is a new version. Also, return True if there is a new version, and False
|
||||||
|
otherwise.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if there is a new version, and False otherwise.
|
||||||
|
"""
|
||||||
|
latest_version = get_latest_version_number_from_pypi()
|
||||||
|
if latest_version is not None and __version__ != latest_version:
|
||||||
|
warning(
|
||||||
|
f"A new version of RenderCV is available! You are using v{__version__},"
|
||||||
|
f" and the latest version is v{latest_version}."
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def welcome():
|
def welcome():
|
||||||
"""Print a welcome message to the terminal."""
|
"""Print a welcome message to the terminal."""
|
||||||
|
warn_if_new_version_is_available()
|
||||||
|
|
||||||
table = rich.table.Table(
|
table = rich.table.Table(
|
||||||
title=(
|
title=(
|
||||||
"\nWelcome to [bold]Render[dodger_blue3]CV[/dodger_blue3][/bold]! Some"
|
"\nWelcome to [bold]Render[dodger_blue3]CV[/dodger_blue3][/bold]! Some"
|
||||||
|
@ -106,7 +158,7 @@ def information(text: str):
|
||||||
Args:
|
Args:
|
||||||
text (str): The text of the information message.
|
text (str): The text of the information message.
|
||||||
"""
|
"""
|
||||||
print(f"[bold green]{text}")
|
print(f"[yellow]{text}")
|
||||||
|
|
||||||
|
|
||||||
def get_error_message_and_location_and_value_from_a_custom_error(
|
def get_error_message_and_location_and_value_from_a_custom_error(
|
||||||
|
@ -326,7 +378,11 @@ def handle_exceptions(function: Callable) -> Callable:
|
||||||
except pydantic.ValidationError as e:
|
except pydantic.ValidationError as e:
|
||||||
handle_validation_error(e)
|
handle_validation_error(e)
|
||||||
except ruamel.yaml.YAMLError as e:
|
except ruamel.yaml.YAMLError as e:
|
||||||
error("There is a YAML error in the input file!", e)
|
error(
|
||||||
|
"There is a YAML error in the input file!\n\nTry to use quotation marks"
|
||||||
|
" to make sure the YAML parser understands the field is a string.",
|
||||||
|
e,
|
||||||
|
)
|
||||||
except FileNotFoundError as e:
|
except FileNotFoundError as e:
|
||||||
error(e)
|
error(e)
|
||||||
except UnicodeDecodeError as e:
|
except UnicodeDecodeError as e:
|
||||||
|
@ -428,7 +484,7 @@ class LiveProgressReporter(rich.live.Live):
|
||||||
"""End the live progress reporting."""
|
"""End the live progress reporting."""
|
||||||
self.overall_progress.update(
|
self.overall_progress.update(
|
||||||
self.overall_task_id,
|
self.overall_task_id,
|
||||||
description=f"[bold green]{self.end_message}",
|
description=f"[yellow]{self.end_message}",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -521,8 +577,8 @@ def parse_data_model_override_arguments(
|
||||||
@app.command(
|
@app.command(
|
||||||
name="render",
|
name="render",
|
||||||
help=(
|
help=(
|
||||||
"Render a YAML input file. Example: [bold green]rendercv render"
|
"Render a YAML input file. Example: [yellow]rendercv render"
|
||||||
" John_Doe_CV.yaml[/bold green]"
|
" John_Doe_CV.yaml[/yellow]. Details: [cyan]rendercv render --help[/cyan]"
|
||||||
),
|
),
|
||||||
# allow extra arguments for updating the data model:
|
# allow extra arguments for updating the data model:
|
||||||
context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
|
context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
|
||||||
|
@ -530,12 +586,13 @@ def parse_data_model_override_arguments(
|
||||||
@handle_exceptions
|
@handle_exceptions
|
||||||
def cli_command_render(
|
def cli_command_render(
|
||||||
input_file_name: Annotated[
|
input_file_name: Annotated[
|
||||||
str,
|
str, typer.Argument(help="Name of the YAML input file.")
|
||||||
typer.Argument(help="Name of the YAML input file."),
|
|
||||||
],
|
],
|
||||||
use_local_latex_command: Annotated[
|
use_local_latex_command: Annotated[
|
||||||
Optional[str],
|
Optional[str],
|
||||||
typer.Option(
|
typer.Option(
|
||||||
|
"--use-local-latex-command",
|
||||||
|
"-use",
|
||||||
help=(
|
help=(
|
||||||
"Use the local LaTeX installation with the given command instead of the"
|
"Use the local LaTeX installation with the given command instead of the"
|
||||||
" RenderCV's TinyTeX."
|
" RenderCV's TinyTeX."
|
||||||
|
@ -545,36 +602,48 @@ def cli_command_render(
|
||||||
output_folder_name: Annotated[
|
output_folder_name: Annotated[
|
||||||
str,
|
str,
|
||||||
typer.Option(
|
typer.Option(
|
||||||
|
"--output-folder-name",
|
||||||
|
"-o",
|
||||||
help="Name of the output folder.",
|
help="Name of the output folder.",
|
||||||
),
|
),
|
||||||
] = "rendercv_output",
|
] = "rendercv_output",
|
||||||
latex_path: Annotated[
|
latex_path: Annotated[
|
||||||
Optional[str],
|
Optional[str],
|
||||||
typer.Option(
|
typer.Option(
|
||||||
|
"--latex-path",
|
||||||
|
"-latex",
|
||||||
help="Copy the LaTeX file to the given path.",
|
help="Copy the LaTeX file to the given path.",
|
||||||
),
|
),
|
||||||
] = None,
|
] = None,
|
||||||
pdf_path: Annotated[
|
pdf_path: Annotated[
|
||||||
Optional[str],
|
Optional[str],
|
||||||
typer.Option(
|
typer.Option(
|
||||||
|
"--pdf-path",
|
||||||
|
"-pdf",
|
||||||
help="Copy the PDF file to the given path.",
|
help="Copy the PDF file to the given path.",
|
||||||
),
|
),
|
||||||
] = None,
|
] = None,
|
||||||
markdown_path: Annotated[
|
markdown_path: Annotated[
|
||||||
Optional[str],
|
Optional[str],
|
||||||
typer.Option(
|
typer.Option(
|
||||||
|
"--markdown-path",
|
||||||
|
"-md",
|
||||||
help="Copy the Markdown file to the given path.",
|
help="Copy the Markdown file to the given path.",
|
||||||
),
|
),
|
||||||
] = None,
|
] = None,
|
||||||
html_path: Annotated[
|
html_path: Annotated[
|
||||||
Optional[str],
|
Optional[str],
|
||||||
typer.Option(
|
typer.Option(
|
||||||
|
"--html-path",
|
||||||
|
"-html",
|
||||||
help="Copy the HTML file to the given path.",
|
help="Copy the HTML file to the given path.",
|
||||||
),
|
),
|
||||||
] = None,
|
] = None,
|
||||||
png_path: Annotated[
|
png_path: Annotated[
|
||||||
Optional[str],
|
Optional[str],
|
||||||
typer.Option(
|
typer.Option(
|
||||||
|
"--png-path",
|
||||||
|
"-png",
|
||||||
help="Copy the PNG file to the given path.",
|
help="Copy the PNG file to the given path.",
|
||||||
),
|
),
|
||||||
] = None,
|
] = None,
|
||||||
|
@ -582,6 +651,7 @@ def cli_command_render(
|
||||||
bool,
|
bool,
|
||||||
typer.Option(
|
typer.Option(
|
||||||
"--dont-generate-markdown",
|
"--dont-generate-markdown",
|
||||||
|
"-nomd",
|
||||||
help="Don't generate the Markdown and HTML file.",
|
help="Don't generate the Markdown and HTML file.",
|
||||||
),
|
),
|
||||||
] = False,
|
] = False,
|
||||||
|
@ -589,6 +659,7 @@ def cli_command_render(
|
||||||
bool,
|
bool,
|
||||||
typer.Option(
|
typer.Option(
|
||||||
"--dont-generate-html",
|
"--dont-generate-html",
|
||||||
|
"-nohtml",
|
||||||
help="Don't generate the HTML file.",
|
help="Don't generate the HTML file.",
|
||||||
),
|
),
|
||||||
] = False,
|
] = False,
|
||||||
|
@ -596,6 +667,7 @@ def cli_command_render(
|
||||||
bool,
|
bool,
|
||||||
typer.Option(
|
typer.Option(
|
||||||
"--dont-generate-png",
|
"--dont-generate-png",
|
||||||
|
"-nopng",
|
||||||
help="Don't generate the PNG file.",
|
help="Don't generate the PNG file.",
|
||||||
),
|
),
|
||||||
] = False,
|
] = False,
|
||||||
|
@ -711,8 +783,8 @@ def cli_command_render(
|
||||||
@app.command(
|
@app.command(
|
||||||
name="new",
|
name="new",
|
||||||
help=(
|
help=(
|
||||||
"Generate a YAML input file to get started. Example: [bold green]rendercv new"
|
"Generate a YAML input file to get started. Example: [yellow]rendercv new"
|
||||||
' "John Doe"[/bold green]'
|
' "John Doe"[/yellow]. Details: [cyan]rendercv new --help[/cyan]'
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def cli_command_new(
|
def cli_command_new(
|
||||||
|
@ -783,8 +855,9 @@ def cli_command_new(
|
||||||
@app.command(
|
@app.command(
|
||||||
name="create-theme",
|
name="create-theme",
|
||||||
help=(
|
help=(
|
||||||
"Create a custom theme folder based on an existing theme. Example: [bold"
|
"Create a custom theme folder based on an existing theme. Example:"
|
||||||
" green]rendercv create-theme --based-on classic customtheme[/bold green]"
|
" [yellow]rendercv create-theme customtheme[/yellow]. Details: [cyan]rendercv"
|
||||||
|
" create-theme --help[/cyan]"
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def cli_command_create_theme(
|
def cli_command_create_theme(
|
||||||
|
@ -844,8 +917,10 @@ def cli_command_create_theme(
|
||||||
@app.callback()
|
@app.callback()
|
||||||
def main(
|
def main(
|
||||||
version_requested: Annotated[
|
version_requested: Annotated[
|
||||||
Optional[bool], typer.Option("--version", help="Show the version.")
|
Optional[bool], typer.Option("--version", "-v", help="Show the version.")
|
||||||
] = None,
|
] = None,
|
||||||
):
|
):
|
||||||
if version_requested:
|
if version_requested:
|
||||||
information(f"RenderCV v{__version__}")
|
there_is_a_new_version = warn_if_new_version_is_available()
|
||||||
|
if not there_is_a_new_version:
|
||||||
|
print(f"RenderCV v{__version__}")
|
||||||
|
|
|
@ -641,15 +641,15 @@ Entry = (
|
||||||
| BulletEntry
|
| BulletEntry
|
||||||
| str
|
| str
|
||||||
)
|
)
|
||||||
ListOfEntries = (
|
ListOfEntries = list[
|
||||||
list[OneLineEntry]
|
OneLineEntry
|
||||||
| list[NormalEntry]
|
| NormalEntry
|
||||||
| list[ExperienceEntry]
|
| ExperienceEntry
|
||||||
| list[EducationEntry]
|
| EducationEntry
|
||||||
| list[PublicationEntry]
|
| PublicationEntry
|
||||||
| list[BulletEntry]
|
| BulletEntry
|
||||||
| list[str]
|
| str
|
||||||
)
|
]
|
||||||
entry_types = Entry.__args__[:-1] # a tuple of all the entry types except str
|
entry_types = Entry.__args__[:-1] # a tuple of all the entry types except str
|
||||||
entry_type_names = [entry_type.__name__ for entry_type in entry_types] + ["TextEntry"]
|
entry_type_names = [entry_type.__name__ for entry_type in entry_types] + ["TextEntry"]
|
||||||
|
|
||||||
|
@ -838,6 +838,7 @@ SocialNetworkName = Literal[
|
||||||
"StackOverflow",
|
"StackOverflow",
|
||||||
"ResearchGate",
|
"ResearchGate",
|
||||||
"YouTube",
|
"YouTube",
|
||||||
|
"Google Scholar",
|
||||||
]
|
]
|
||||||
available_social_networks = get_args(SocialNetworkName)
|
available_social_networks = get_args(SocialNetworkName)
|
||||||
|
|
||||||
|
@ -909,6 +910,7 @@ class SocialNetwork(RenderCVBaseModel):
|
||||||
"ResearchGate": "https://researchgate.net/profile/",
|
"ResearchGate": "https://researchgate.net/profile/",
|
||||||
"YouTube": "https://youtube.com/",
|
"YouTube": "https://youtube.com/",
|
||||||
"Google Scholar": "https://scholar.google.com/citations?user=",
|
"Google Scholar": "https://scholar.google.com/citations?user=",
|
||||||
|
"Google Scholar": "https://scholar.google.com/citations?user=",
|
||||||
}
|
}
|
||||||
url = url_dictionary[self.network] + self.username
|
url = url_dictionary[self.network] + self.username
|
||||||
|
|
||||||
|
@ -1019,6 +1021,7 @@ class CurriculumVitae(RenderCVBaseModel):
|
||||||
"Twitter": "\\faTwitter",
|
"Twitter": "\\faTwitter",
|
||||||
"ResearchGate": "\\faResearchgate",
|
"ResearchGate": "\\faResearchgate",
|
||||||
"YouTube": "\\faYoutube",
|
"YouTube": "\\faYoutube",
|
||||||
|
"Google Scholar": "\\faGraduationCap",
|
||||||
}
|
}
|
||||||
for social_network in self.social_networks:
|
for social_network in self.social_networks:
|
||||||
clean_url = social_network.url.replace("https://", "").rstrip("/")
|
clean_url = social_network.url.replace("https://", "").rstrip("/")
|
||||||
|
@ -1032,6 +1035,9 @@ class CurriculumVitae(RenderCVBaseModel):
|
||||||
if social_network.network == "StackOverflow":
|
if social_network.network == "StackOverflow":
|
||||||
username = social_network.username.split("/")[1]
|
username = social_network.username.split("/")[1]
|
||||||
connection["placeholder"] = username
|
connection["placeholder"] = username
|
||||||
|
if social_network.network == "Google Scholar":
|
||||||
|
connection["placeholder"] = "Google Scholar"
|
||||||
|
|
||||||
connections.append(connection)
|
connections.append(connection)
|
||||||
|
|
||||||
return connections
|
return connections
|
||||||
|
@ -1761,27 +1767,16 @@ def generate_json_schema() -> dict[str, Any]:
|
||||||
# already have the required field. Moreover, we would like to warn
|
# already have the required field. Moreover, we would like to warn
|
||||||
# users if they provide null values. They can remove the fields if they
|
# users if they provide null values. They can remove the fields if they
|
||||||
# don't want to provide them.
|
# don't want to provide them.
|
||||||
null_type_dict = {}
|
null_type_dict = {
|
||||||
null_type_dict["type"] = "null"
|
"type": "null",
|
||||||
|
}
|
||||||
for field_name, field in value["properties"].items():
|
for field_name, field in value["properties"].items():
|
||||||
if "anyOf" in field:
|
if "anyOf" in field:
|
||||||
if (
|
if null_type_dict in field["anyOf"]:
|
||||||
len(field["anyOf"]) == 2
|
field["anyOf"].remove(null_type_dict)
|
||||||
and null_type_dict in field["anyOf"]
|
|
||||||
):
|
|
||||||
field["oneOf"] = [field["anyOf"][0]]
|
|
||||||
del field["anyOf"]
|
|
||||||
|
|
||||||
# For sections field of CurriculumVitae:
|
field["oneOf"] = field["anyOf"]
|
||||||
if "additionalProperties" in field["oneOf"][0]:
|
del field["anyOf"]
|
||||||
field["oneOf"][0]["additionalProperties"]["oneOf"] = (
|
|
||||||
field["oneOf"][0]["additionalProperties"]["anyOf"]
|
|
||||||
)
|
|
||||||
del field["oneOf"][0]["additionalProperties"]["anyOf"]
|
|
||||||
|
|
||||||
else:
|
|
||||||
field["oneOf"] = field["anyOf"]
|
|
||||||
del field["anyOf"]
|
|
||||||
|
|
||||||
return json_schema
|
return json_schema
|
||||||
|
|
||||||
|
|
1756
schema.json
1756
schema.json
File diff suppressed because it is too large
Load Diff
|
@ -283,6 +283,7 @@ def rendercv_filled_curriculum_vitae_data_model(
|
||||||
dm.SocialNetwork(network="GitHub", username="johndoe"),
|
dm.SocialNetwork(network="GitHub", username="johndoe"),
|
||||||
dm.SocialNetwork(network="Instagram", username="johndoe"),
|
dm.SocialNetwork(network="Instagram", username="johndoe"),
|
||||||
dm.SocialNetwork(network="Orcid", username="0000-0000-0000-0000"),
|
dm.SocialNetwork(network="Orcid", username="0000-0000-0000-0000"),
|
||||||
|
dm.SocialNetwork(network="Google Scholar", username="F8IyYrQAAAAJ"),
|
||||||
dm.SocialNetwork(network="Mastodon", username="@johndoe@example"),
|
dm.SocialNetwork(network="Mastodon", username="@johndoe@example"),
|
||||||
dm.SocialNetwork(network="Twitter", username="johndoe"),
|
dm.SocialNetwork(network="Twitter", username="johndoe"),
|
||||||
dm.SocialNetwork(network="StackOverflow", username="12323/johndoe"),
|
dm.SocialNetwork(network="StackOverflow", username="12323/johndoe"),
|
||||||
|
|
|
@ -11,6 +11,7 @@ import typer.testing
|
||||||
|
|
||||||
import rendercv.cli as cli
|
import rendercv.cli as cli
|
||||||
import rendercv.data_models as dm
|
import rendercv.data_models as dm
|
||||||
|
from rendercv import __version__
|
||||||
|
|
||||||
|
|
||||||
def run_render_command(input_file_path, working_path, extra_arguments=[]):
|
def run_render_command(input_file_path, working_path, extra_arguments=[]):
|
||||||
|
@ -509,3 +510,46 @@ def test_create_theme_command_theme_already_exists(tmp_path):
|
||||||
|
|
||||||
def test_main_file():
|
def test_main_file():
|
||||||
subprocess.run([sys.executable, "-m", "rendercv", "--help"], check=True)
|
subprocess.run([sys.executable, "-m", "rendercv", "--help"], check=True)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_latest_version_number_from_pypi():
|
||||||
|
version = cli.get_latest_version_number_from_pypi()
|
||||||
|
assert isinstance(version, str)
|
||||||
|
|
||||||
|
|
||||||
|
def test_if_welcome_prints_new_version_available(monkeypatch):
|
||||||
|
monkeypatch.setattr(cli, "get_latest_version_number_from_pypi", lambda: "99999")
|
||||||
|
import io
|
||||||
|
import contextlib
|
||||||
|
|
||||||
|
with contextlib.redirect_stdout(io.StringIO()) as f:
|
||||||
|
cli.welcome()
|
||||||
|
output = f.getvalue()
|
||||||
|
|
||||||
|
assert "A new version of RenderCV is available!" in output
|
||||||
|
|
||||||
|
|
||||||
|
def test_rendercv_version_when_there_is_a_new_version(monkeypatch):
|
||||||
|
monkeypatch.setattr(cli, "get_latest_version_number_from_pypi", lambda: "99999")
|
||||||
|
|
||||||
|
result = runner.invoke(cli.app, ["--version"])
|
||||||
|
|
||||||
|
assert "A new version of RenderCV is available!" in result.stdout
|
||||||
|
|
||||||
|
|
||||||
|
def test_rendercv_version_when_there_is_not_a_new_version(monkeypatch):
|
||||||
|
monkeypatch.setattr(cli, "get_latest_version_number_from_pypi", lambda: __version__)
|
||||||
|
|
||||||
|
result = runner.invoke(cli.app, ["--version"])
|
||||||
|
|
||||||
|
assert __version__ in result.stdout
|
||||||
|
|
||||||
|
|
||||||
|
def test_warn_if_new_version_is_available(monkeypatch):
|
||||||
|
monkeypatch.setattr(cli, "get_latest_version_number_from_pypi", lambda: __version__)
|
||||||
|
|
||||||
|
assert not cli.warn_if_new_version_is_available()
|
||||||
|
|
||||||
|
monkeypatch.setattr(cli, "get_latest_version_number_from_pypi", lambda: "999")
|
||||||
|
|
||||||
|
assert cli.warn_if_new_version_is_available()
|
||||||
|
|
|
@ -450,6 +450,11 @@ def test_invalid_social_networks(network, username):
|
||||||
"@myusername",
|
"@myusername",
|
||||||
"https://youtube.com/@myusername",
|
"https://youtube.com/@myusername",
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"Google Scholar",
|
||||||
|
"myusername",
|
||||||
|
"https://scholar.google.com/citations?user=myusername",
|
||||||
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_social_network_url(network, username, expected_url):
|
def test_social_network_url(network, username, expected_url):
|
||||||
|
|
|
@ -192,6 +192,8 @@
|
||||||
\kern 0.5 cm
|
\kern 0.5 cm
|
||||||
\mbox{\hrefWithoutArrow{https://orcid.org/0000-0000-0000-0000}{{\footnotesize\faOrcid}\hspace*{0.13cm}0000-0000-0000-0000}}
|
\mbox{\hrefWithoutArrow{https://orcid.org/0000-0000-0000-0000}{{\footnotesize\faOrcid}\hspace*{0.13cm}0000-0000-0000-0000}}
|
||||||
\kern 0.5 cm
|
\kern 0.5 cm
|
||||||
|
\mbox{\hrefWithoutArrow{https://scholar.google.com/citations?user=F8IyYrQAAAAJ}{{\footnotesize\faGraduationCap}\hspace*{0.13cm}Google Scholar}}
|
||||||
|
\kern 0.5 cm
|
||||||
\mbox{\hrefWithoutArrow{https://example/@johndoe}{{\footnotesize\faMastodon}\hspace*{0.13cm}@johndoe@example}}
|
\mbox{\hrefWithoutArrow{https://example/@johndoe}{{\footnotesize\faMastodon}\hspace*{0.13cm}@johndoe@example}}
|
||||||
\kern 0.5 cm
|
\kern 0.5 cm
|
||||||
\mbox{\hrefWithoutArrow{https://twitter.com/johndoe}{{\footnotesize\faTwitter}\hspace*{0.13cm}johndoe}}
|
\mbox{\hrefWithoutArrow{https://twitter.com/johndoe}{{\footnotesize\faTwitter}\hspace*{0.13cm}johndoe}}
|
||||||
|
|
|
@ -196,6 +196,10 @@
|
||||||
\kern 5.0 pt%
|
\kern 5.0 pt%
|
||||||
\AND%
|
\AND%
|
||||||
\kern 5.0 pt%
|
\kern 5.0 pt%
|
||||||
|
\mbox{\hrefWithoutArrow{https://scholar.google.com/citations?user=F8IyYrQAAAAJ}{scholar.google.com/citations?user=F8IyYrQAAAAJ}}%
|
||||||
|
\kern 5.0 pt%
|
||||||
|
\AND%
|
||||||
|
\kern 5.0 pt%
|
||||||
\mbox{\hrefWithoutArrow{https://example/@johndoe}{example/@johndoe}}%
|
\mbox{\hrefWithoutArrow{https://example/@johndoe}{example/@johndoe}}%
|
||||||
\kern 5.0 pt%
|
\kern 5.0 pt%
|
||||||
\AND%
|
\AND%
|
||||||
|
|
|
@ -51,6 +51,7 @@
|
||||||
\social[github]{johndoe}
|
\social[github]{johndoe}
|
||||||
\social[instagram]{johndoe}
|
\social[instagram]{johndoe}
|
||||||
\social[orcid]{0000-0000-0000-0000}
|
\social[orcid]{0000-0000-0000-0000}
|
||||||
|
\social[google scholar]{F8IyYrQAAAAJ}
|
||||||
\social[mastodon]{@johndoe@example}
|
\social[mastodon]{@johndoe@example}
|
||||||
\social[twitter]{johndoe}
|
\social[twitter]{johndoe}
|
||||||
\social[stackoverflow]{12323/johndoe}
|
\social[stackoverflow]{12323/johndoe}
|
||||||
|
|
|
@ -168,6 +168,8 @@
|
||||||
\kern 0.5 cm
|
\kern 0.5 cm
|
||||||
\mbox{\hrefWithoutArrow{https://orcid.org/0000-0000-0000-0000}{\color{black}{\footnotesize\faOrcid}\hspace*{0.13cm}0000-0000-0000-0000}}
|
\mbox{\hrefWithoutArrow{https://orcid.org/0000-0000-0000-0000}{\color{black}{\footnotesize\faOrcid}\hspace*{0.13cm}0000-0000-0000-0000}}
|
||||||
\kern 0.5 cm
|
\kern 0.5 cm
|
||||||
|
\mbox{\hrefWithoutArrow{https://scholar.google.com/citations?user=F8IyYrQAAAAJ}{\color{black}{\footnotesize\faGraduationCap}\hspace*{0.13cm}Google Scholar}}
|
||||||
|
\kern 0.5 cm
|
||||||
\mbox{\hrefWithoutArrow{https://example/@johndoe}{\color{black}{\footnotesize\faMastodon}\hspace*{0.13cm}@johndoe@example}}
|
\mbox{\hrefWithoutArrow{https://example/@johndoe}{\color{black}{\footnotesize\faMastodon}\hspace*{0.13cm}@johndoe@example}}
|
||||||
\kern 0.5 cm
|
\kern 0.5 cm
|
||||||
\mbox{\hrefWithoutArrow{https://twitter.com/johndoe}{\color{black}{\footnotesize\faTwitter}\hspace*{0.13cm}johndoe}}
|
\mbox{\hrefWithoutArrow{https://twitter.com/johndoe}{\color{black}{\footnotesize\faTwitter}\hspace*{0.13cm}johndoe}}
|
||||||
|
|
|
@ -192,6 +192,8 @@
|
||||||
\kern 0.5 cm
|
\kern 0.5 cm
|
||||||
\mbox{\hrefWithoutArrow{https://orcid.org/0000-0000-0000-0000}{{\footnotesize\faOrcid}\hspace*{0.13cm}0000-0000-0000-0000}}
|
\mbox{\hrefWithoutArrow{https://orcid.org/0000-0000-0000-0000}{{\footnotesize\faOrcid}\hspace*{0.13cm}0000-0000-0000-0000}}
|
||||||
\kern 0.5 cm
|
\kern 0.5 cm
|
||||||
|
\mbox{\hrefWithoutArrow{https://scholar.google.com/citations?user=F8IyYrQAAAAJ}{{\footnotesize\faGraduationCap}\hspace*{0.13cm}Google Scholar}}
|
||||||
|
\kern 0.5 cm
|
||||||
\mbox{\hrefWithoutArrow{https://example/@johndoe}{{\footnotesize\faMastodon}\hspace*{0.13cm}@johndoe@example}}
|
\mbox{\hrefWithoutArrow{https://example/@johndoe}{{\footnotesize\faMastodon}\hspace*{0.13cm}@johndoe@example}}
|
||||||
\kern 0.5 cm
|
\kern 0.5 cm
|
||||||
\mbox{\hrefWithoutArrow{https://twitter.com/johndoe}{{\footnotesize\faTwitter}\hspace*{0.13cm}johndoe}}
|
\mbox{\hrefWithoutArrow{https://twitter.com/johndoe}{{\footnotesize\faTwitter}\hspace*{0.13cm}johndoe}}
|
||||||
|
|
|
@ -196,6 +196,10 @@
|
||||||
\kern 5.0 pt%
|
\kern 5.0 pt%
|
||||||
\AND%
|
\AND%
|
||||||
\kern 5.0 pt%
|
\kern 5.0 pt%
|
||||||
|
\mbox{\hrefWithoutArrow{https://scholar.google.com/citations?user=F8IyYrQAAAAJ}{scholar.google.com/citations?user=F8IyYrQAAAAJ}}%
|
||||||
|
\kern 5.0 pt%
|
||||||
|
\AND%
|
||||||
|
\kern 5.0 pt%
|
||||||
\mbox{\hrefWithoutArrow{https://example/@johndoe}{example/@johndoe}}%
|
\mbox{\hrefWithoutArrow{https://example/@johndoe}{example/@johndoe}}%
|
||||||
\kern 5.0 pt%
|
\kern 5.0 pt%
|
||||||
\AND%
|
\AND%
|
||||||
|
|
|
@ -51,6 +51,7 @@
|
||||||
\social[github]{johndoe}
|
\social[github]{johndoe}
|
||||||
\social[instagram]{johndoe}
|
\social[instagram]{johndoe}
|
||||||
\social[orcid]{0000-0000-0000-0000}
|
\social[orcid]{0000-0000-0000-0000}
|
||||||
|
\social[google scholar]{F8IyYrQAAAAJ}
|
||||||
\social[mastodon]{@johndoe@example}
|
\social[mastodon]{@johndoe@example}
|
||||||
\social[twitter]{johndoe}
|
\social[twitter]{johndoe}
|
||||||
\social[stackoverflow]{12323/johndoe}
|
\social[stackoverflow]{12323/johndoe}
|
||||||
|
|
|
@ -168,6 +168,8 @@
|
||||||
\kern 0.5 cm
|
\kern 0.5 cm
|
||||||
\mbox{\hrefWithoutArrow{https://orcid.org/0000-0000-0000-0000}{\color{black}{\footnotesize\faOrcid}\hspace*{0.13cm}0000-0000-0000-0000}}
|
\mbox{\hrefWithoutArrow{https://orcid.org/0000-0000-0000-0000}{\color{black}{\footnotesize\faOrcid}\hspace*{0.13cm}0000-0000-0000-0000}}
|
||||||
\kern 0.5 cm
|
\kern 0.5 cm
|
||||||
|
\mbox{\hrefWithoutArrow{https://scholar.google.com/citations?user=F8IyYrQAAAAJ}{\color{black}{\footnotesize\faGraduationCap}\hspace*{0.13cm}Google Scholar}}
|
||||||
|
\kern 0.5 cm
|
||||||
\mbox{\hrefWithoutArrow{https://example/@johndoe}{\color{black}{\footnotesize\faMastodon}\hspace*{0.13cm}@johndoe@example}}
|
\mbox{\hrefWithoutArrow{https://example/@johndoe}{\color{black}{\footnotesize\faMastodon}\hspace*{0.13cm}@johndoe@example}}
|
||||||
\kern 0.5 cm
|
\kern 0.5 cm
|
||||||
\mbox{\hrefWithoutArrow{https://twitter.com/johndoe}{\color{black}{\footnotesize\faTwitter}\hspace*{0.13cm}johndoe}}
|
\mbox{\hrefWithoutArrow{https://twitter.com/johndoe}{\color{black}{\footnotesize\faTwitter}\hspace*{0.13cm}johndoe}}
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
- GitHub: [johndoe](https://github.com/johndoe)
|
- GitHub: [johndoe](https://github.com/johndoe)
|
||||||
- Instagram: [johndoe](https://instagram.com/johndoe)
|
- Instagram: [johndoe](https://instagram.com/johndoe)
|
||||||
- Orcid: [0000-0000-0000-0000](https://orcid.org/0000-0000-0000-0000)
|
- Orcid: [0000-0000-0000-0000](https://orcid.org/0000-0000-0000-0000)
|
||||||
|
- Google Scholar: [F8IyYrQAAAAJ](https://scholar.google.com/citations?user=F8IyYrQAAAAJ)
|
||||||
- Mastodon: [@johndoe@example](https://example/@johndoe)
|
- Mastodon: [@johndoe@example](https://example/@johndoe)
|
||||||
- Twitter: [johndoe](https://twitter.com/johndoe)
|
- Twitter: [johndoe](https://twitter.com/johndoe)
|
||||||
- StackOverflow: [12323/johndoe](https://stackoverflow.com/users/12323/johndoe)
|
- StackOverflow: [12323/johndoe](https://stackoverflow.com/users/12323/johndoe)
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
- GitHub: [johndoe](https://github.com/johndoe)
|
- GitHub: [johndoe](https://github.com/johndoe)
|
||||||
- Instagram: [johndoe](https://instagram.com/johndoe)
|
- Instagram: [johndoe](https://instagram.com/johndoe)
|
||||||
- Orcid: [0000-0000-0000-0000](https://orcid.org/0000-0000-0000-0000)
|
- Orcid: [0000-0000-0000-0000](https://orcid.org/0000-0000-0000-0000)
|
||||||
|
- Google Scholar: [F8IyYrQAAAAJ](https://scholar.google.com/citations?user=F8IyYrQAAAAJ)
|
||||||
- Mastodon: [@johndoe@example](https://example/@johndoe)
|
- Mastodon: [@johndoe@example](https://example/@johndoe)
|
||||||
- Twitter: [johndoe](https://twitter.com/johndoe)
|
- Twitter: [johndoe](https://twitter.com/johndoe)
|
||||||
- StackOverflow: [12323/johndoe](https://stackoverflow.com/users/12323/johndoe)
|
- StackOverflow: [12323/johndoe](https://stackoverflow.com/users/12323/johndoe)
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
- GitHub: [johndoe](https://github.com/johndoe)
|
- GitHub: [johndoe](https://github.com/johndoe)
|
||||||
- Instagram: [johndoe](https://instagram.com/johndoe)
|
- Instagram: [johndoe](https://instagram.com/johndoe)
|
||||||
- Orcid: [0000-0000-0000-0000](https://orcid.org/0000-0000-0000-0000)
|
- Orcid: [0000-0000-0000-0000](https://orcid.org/0000-0000-0000-0000)
|
||||||
|
- Google Scholar: [F8IyYrQAAAAJ](https://scholar.google.com/citations?user=F8IyYrQAAAAJ)
|
||||||
- Mastodon: [@johndoe@example](https://example/@johndoe)
|
- Mastodon: [@johndoe@example](https://example/@johndoe)
|
||||||
- Twitter: [johndoe](https://twitter.com/johndoe)
|
- Twitter: [johndoe](https://twitter.com/johndoe)
|
||||||
- StackOverflow: [12323/johndoe](https://stackoverflow.com/users/12323/johndoe)
|
- StackOverflow: [12323/johndoe](https://stackoverflow.com/users/12323/johndoe)
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
- GitHub: [johndoe](https://github.com/johndoe)
|
- GitHub: [johndoe](https://github.com/johndoe)
|
||||||
- Instagram: [johndoe](https://instagram.com/johndoe)
|
- Instagram: [johndoe](https://instagram.com/johndoe)
|
||||||
- Orcid: [0000-0000-0000-0000](https://orcid.org/0000-0000-0000-0000)
|
- Orcid: [0000-0000-0000-0000](https://orcid.org/0000-0000-0000-0000)
|
||||||
|
- Google Scholar: [F8IyYrQAAAAJ](https://scholar.google.com/citations?user=F8IyYrQAAAAJ)
|
||||||
- Mastodon: [@johndoe@example](https://example/@johndoe)
|
- Mastodon: [@johndoe@example](https://example/@johndoe)
|
||||||
- Twitter: [johndoe](https://twitter.com/johndoe)
|
- Twitter: [johndoe](https://twitter.com/johndoe)
|
||||||
- StackOverflow: [12323/johndoe](https://stackoverflow.com/users/12323/johndoe)
|
- StackOverflow: [12323/johndoe](https://stackoverflow.com/users/12323/johndoe)
|
||||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -8,6 +8,7 @@
|
||||||
<li>GitHub: <a href="https://github.com/johndoe">johndoe</a></li>
|
<li>GitHub: <a href="https://github.com/johndoe">johndoe</a></li>
|
||||||
<li>Instagram: <a href="https://instagram.com/johndoe">johndoe</a></li>
|
<li>Instagram: <a href="https://instagram.com/johndoe">johndoe</a></li>
|
||||||
<li>Orcid: <a href="https://orcid.org/0000-0000-0000-0000">0000-0000-0000-0000</a></li>
|
<li>Orcid: <a href="https://orcid.org/0000-0000-0000-0000">0000-0000-0000-0000</a></li>
|
||||||
|
<li>Google Scholar: <a href="https://scholar.google.com/citations?user=F8IyYrQAAAAJ">F8IyYrQAAAAJ</a></li>
|
||||||
<li>Mastodon: <a href="https://example/@johndoe">@johndoe@example</a></li>
|
<li>Mastodon: <a href="https://example/@johndoe">@johndoe@example</a></li>
|
||||||
<li>Twitter: <a href="https://twitter.com/johndoe">johndoe</a></li>
|
<li>Twitter: <a href="https://twitter.com/johndoe">johndoe</a></li>
|
||||||
<li>StackOverflow: <a href="https://stackoverflow.com/users/12323/johndoe">12323/johndoe</a></li>
|
<li>StackOverflow: <a href="https://stackoverflow.com/users/12323/johndoe">12323/johndoe</a></li>
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
<li>GitHub: <a href="https://github.com/johndoe">johndoe</a></li>
|
<li>GitHub: <a href="https://github.com/johndoe">johndoe</a></li>
|
||||||
<li>Instagram: <a href="https://instagram.com/johndoe">johndoe</a></li>
|
<li>Instagram: <a href="https://instagram.com/johndoe">johndoe</a></li>
|
||||||
<li>Orcid: <a href="https://orcid.org/0000-0000-0000-0000">0000-0000-0000-0000</a></li>
|
<li>Orcid: <a href="https://orcid.org/0000-0000-0000-0000">0000-0000-0000-0000</a></li>
|
||||||
|
<li>Google Scholar: <a href="https://scholar.google.com/citations?user=F8IyYrQAAAAJ">F8IyYrQAAAAJ</a></li>
|
||||||
<li>Mastodon: <a href="https://example/@johndoe">@johndoe@example</a></li>
|
<li>Mastodon: <a href="https://example/@johndoe">@johndoe@example</a></li>
|
||||||
<li>Twitter: <a href="https://twitter.com/johndoe">johndoe</a></li>
|
<li>Twitter: <a href="https://twitter.com/johndoe">johndoe</a></li>
|
||||||
<li>StackOverflow: <a href="https://stackoverflow.com/users/12323/johndoe">12323/johndoe</a></li>
|
<li>StackOverflow: <a href="https://stackoverflow.com/users/12323/johndoe">12323/johndoe</a></li>
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
<li>GitHub: <a href="https://github.com/johndoe">johndoe</a></li>
|
<li>GitHub: <a href="https://github.com/johndoe">johndoe</a></li>
|
||||||
<li>Instagram: <a href="https://instagram.com/johndoe">johndoe</a></li>
|
<li>Instagram: <a href="https://instagram.com/johndoe">johndoe</a></li>
|
||||||
<li>Orcid: <a href="https://orcid.org/0000-0000-0000-0000">0000-0000-0000-0000</a></li>
|
<li>Orcid: <a href="https://orcid.org/0000-0000-0000-0000">0000-0000-0000-0000</a></li>
|
||||||
|
<li>Google Scholar: <a href="https://scholar.google.com/citations?user=F8IyYrQAAAAJ">F8IyYrQAAAAJ</a></li>
|
||||||
<li>Mastodon: <a href="https://example/@johndoe">@johndoe@example</a></li>
|
<li>Mastodon: <a href="https://example/@johndoe">@johndoe@example</a></li>
|
||||||
<li>Twitter: <a href="https://twitter.com/johndoe">johndoe</a></li>
|
<li>Twitter: <a href="https://twitter.com/johndoe">johndoe</a></li>
|
||||||
<li>StackOverflow: <a href="https://stackoverflow.com/users/12323/johndoe">12323/johndoe</a></li>
|
<li>StackOverflow: <a href="https://stackoverflow.com/users/12323/johndoe">12323/johndoe</a></li>
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
<li>GitHub: <a href="https://github.com/johndoe">johndoe</a></li>
|
<li>GitHub: <a href="https://github.com/johndoe">johndoe</a></li>
|
||||||
<li>Instagram: <a href="https://instagram.com/johndoe">johndoe</a></li>
|
<li>Instagram: <a href="https://instagram.com/johndoe">johndoe</a></li>
|
||||||
<li>Orcid: <a href="https://orcid.org/0000-0000-0000-0000">0000-0000-0000-0000</a></li>
|
<li>Orcid: <a href="https://orcid.org/0000-0000-0000-0000">0000-0000-0000-0000</a></li>
|
||||||
|
<li>Google Scholar: <a href="https://scholar.google.com/citations?user=F8IyYrQAAAAJ">F8IyYrQAAAAJ</a></li>
|
||||||
<li>Mastodon: <a href="https://example/@johndoe">@johndoe@example</a></li>
|
<li>Mastodon: <a href="https://example/@johndoe">@johndoe@example</a></li>
|
||||||
<li>Twitter: <a href="https://twitter.com/johndoe">johndoe</a></li>
|
<li>Twitter: <a href="https://twitter.com/johndoe">johndoe</a></li>
|
||||||
<li>StackOverflow: <a href="https://stackoverflow.com/users/12323/johndoe">12323/johndoe</a></li>
|
<li>StackOverflow: <a href="https://stackoverflow.com/users/12323/johndoe">12323/johndoe</a></li>
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 455 KiB After Width: | Height: | Size: 460 KiB |
Loading…
Reference in New Issue