mirror of https://github.com/eyhc1/rendercv.git
enhance data_models.py
This commit is contained in:
parent
e83107fa6e
commit
c33875240c
|
@ -12,7 +12,7 @@ has provided a valid RenderCV input. This is achieved through the use of
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from datetime import date as Date
|
from datetime import date as Date
|
||||||
from typing import Literal, Any
|
from typing import Literal, Any, Type
|
||||||
from typing_extensions import Annotated, Optional
|
from typing_extensions import Annotated, Optional
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
from urllib.request import urlopen, HTTPError
|
from urllib.request import urlopen, HTTPError
|
||||||
|
@ -50,20 +50,21 @@ def get_date_object(date: str | int) -> Date:
|
||||||
"""
|
"""
|
||||||
if isinstance(date, int):
|
if isinstance(date, int):
|
||||||
date_object = Date.fromisoformat(f"{date}-01-01")
|
date_object = Date.fromisoformat(f"{date}-01-01")
|
||||||
elif re.match(r"\d{4}-\d{2}-\d{2}", date):
|
elif re.fullmatch(r"\d{4}-\d{2}-\d{2}", date):
|
||||||
# Then it is in YYYY-MM-DD format
|
# Then it is in YYYY-MM-DD format
|
||||||
date_object = Date.fromisoformat(date)
|
date_object = Date.fromisoformat(date)
|
||||||
elif re.match(r"\d{4}-\d{2}", date):
|
elif re.fullmatch(r"\d{4}-\d{2}", date):
|
||||||
# Then it is in YYYY-MM format
|
# Then it is in YYYY-MM format
|
||||||
date_object = Date.fromisoformat(f"{date}-01")
|
date_object = Date.fromisoformat(f"{date}-01")
|
||||||
elif re.match(r"\d{4}", date):
|
elif re.fullmatch(r"\d{4}", date):
|
||||||
# Then it is in YYYY format
|
# Then it is in YYYY format
|
||||||
date_object = Date.fromisoformat(f"{date}-01-01")
|
date_object = Date.fromisoformat(f"{date}-01-01")
|
||||||
elif date == "present":
|
elif date == "present":
|
||||||
date_object = Date.today()
|
date_object = Date.today()
|
||||||
else:
|
else:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f'The date string "{date}" is not in YYYY-MM-DD, YYYY-MM, or YYYY format.'
|
"This is not a valid date! Please use either YYYY-MM-DD, YYYY-MM, or"
|
||||||
|
" YYYY format."
|
||||||
)
|
)
|
||||||
|
|
||||||
return date_object
|
return date_object
|
||||||
|
@ -178,7 +179,9 @@ class EntryBase(RenderCVBaseModel):
|
||||||
url: Optional[pydantic.HttpUrl] = None
|
url: Optional[pydantic.HttpUrl] = None
|
||||||
url_text_input: Optional[str] = pydantic.Field(default=None, alias="url_text")
|
url_text_input: Optional[str] = pydantic.Field(default=None, alias="url_text")
|
||||||
|
|
||||||
@pydantic.model_validator(mode="after") # type: ignore
|
@pydantic.model_validator(
|
||||||
|
mode="after",
|
||||||
|
) # type: ignore
|
||||||
@classmethod
|
@classmethod
|
||||||
def check_dates(cls, model: "EntryBase") -> "EntryBase":
|
def check_dates(cls, model: "EntryBase") -> "EntryBase":
|
||||||
"""
|
"""
|
||||||
|
@ -212,31 +215,43 @@ class EntryBase(RenderCVBaseModel):
|
||||||
elif not start_date_is_provided and end_date_is_provided:
|
elif not start_date_is_provided and end_date_is_provided:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
'"end_date" is provided in of the entries, but "start_date" is not.'
|
'"end_date" is provided in of the entries, but "start_date" is not.'
|
||||||
' "start_date" is required.'
|
' Either provide both "start_date" and "end_date" or provide "date".',
|
||||||
|
"start_date", # this is the location of the error
|
||||||
|
"", # this supposed to be the value of the error
|
||||||
)
|
)
|
||||||
|
|
||||||
if model.start_date is not None and model.end_date is not None:
|
if model.start_date is not None and model.end_date is not None:
|
||||||
end_date = get_date_object(model.end_date)
|
try:
|
||||||
start_date = get_date_object(model.start_date)
|
end_date = get_date_object(model.end_date)
|
||||||
|
except ValueError as e:
|
||||||
|
raise ValueError(str(e), "end_date", model.end_date)
|
||||||
|
|
||||||
|
try:
|
||||||
|
start_date = get_date_object(model.start_date)
|
||||||
|
except ValueError as e:
|
||||||
|
raise ValueError(str(e), "start_date", model.start_date)
|
||||||
|
|
||||||
if start_date > end_date:
|
if start_date > end_date:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
'"start_date" can not be after "end_date". The start date is'
|
'"start_date" can not be after "end_date"',
|
||||||
f" {start_date} and the end date is {end_date}."
|
"start_date/end_date", # this is the location of the error
|
||||||
|
"", # this supposed to be the value of the error
|
||||||
)
|
)
|
||||||
elif end_date > Date.today():
|
elif end_date > Date.today():
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f'"end_date" cannot be in the future. The end date is {end_date}.'
|
'"end_date" cannot be in the future.',
|
||||||
|
"end_date", # this is the location of the error
|
||||||
|
model.end_date, # this is value of the error
|
||||||
)
|
)
|
||||||
elif start_date > Date.today():
|
elif start_date > Date.today():
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
'"start_date" cannot be in the future. The start date is'
|
'"start_date" cannot be in the future.',
|
||||||
f" {start_date}."
|
"start_date", # this is the location of the error
|
||||||
|
model.start_date, # this is value of the error
|
||||||
)
|
)
|
||||||
|
|
||||||
return model
|
return model
|
||||||
|
|
||||||
# @pydantic.computed_field
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def date_string(self) -> str:
|
def date_string(self) -> str:
|
||||||
"""
|
"""
|
||||||
|
@ -285,7 +300,6 @@ class EntryBase(RenderCVBaseModel):
|
||||||
|
|
||||||
return date_string
|
return date_string
|
||||||
|
|
||||||
# @pydantic.computed_field
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def time_span_string(self) -> str:
|
def time_span_string(self) -> str:
|
||||||
"""
|
"""
|
||||||
|
@ -356,7 +370,6 @@ class EntryBase(RenderCVBaseModel):
|
||||||
|
|
||||||
return time_span_string
|
return time_span_string
|
||||||
|
|
||||||
# @pydantic.computed_field
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def url_text(self) -> Optional[str]:
|
def url_text(self) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
|
@ -472,10 +485,7 @@ class PublicationEntry(RenderCVBaseModel):
|
||||||
"""Check if the date is in the past."""
|
"""Check if the date is in the past."""
|
||||||
date_object = get_date_object(date)
|
date_object = get_date_object(date)
|
||||||
if date_object > Date.today():
|
if date_object > Date.today():
|
||||||
raise ValueError(
|
raise ValueError("The publication date cannot be in the future!")
|
||||||
f"The publication date {date} cannot be in the future. The publication"
|
|
||||||
" date should be in the past."
|
|
||||||
)
|
|
||||||
|
|
||||||
return date
|
return date
|
||||||
|
|
||||||
|
@ -493,17 +503,15 @@ class PublicationEntry(RenderCVBaseModel):
|
||||||
urlopen(doi_url)
|
urlopen(doi_url)
|
||||||
except HTTPError as err:
|
except HTTPError as err:
|
||||||
if err.code == 404:
|
if err.code == 404:
|
||||||
raise ValueError(f"{doi} cannot be found in the DOI System.")
|
raise ValueError("DOI cannot be found in the DOI System!")
|
||||||
|
|
||||||
return doi
|
return doi
|
||||||
|
|
||||||
# @pydantic.computed_field
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def doi_url(self) -> str:
|
def doi_url(self) -> str:
|
||||||
"""Return the URL of the DOI."""
|
"""Return the URL of the DOI."""
|
||||||
return f"https://doi.org/{self.doi}"
|
return f"https://doi.org/{self.doi}"
|
||||||
|
|
||||||
# @pydantic.computed_field
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def date_string(self) -> str:
|
def date_string(self) -> str:
|
||||||
"""Return the date string of the publication."""
|
"""Return the date string of the publication."""
|
||||||
|
@ -603,49 +611,142 @@ Section = Annotated[
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def get_entry_and_section_type(
|
||||||
|
entry: (
|
||||||
|
dict[str, Any]
|
||||||
|
| EducationEntry
|
||||||
|
| ExperienceEntry
|
||||||
|
| PublicationEntry
|
||||||
|
| NormalEntry
|
||||||
|
| OneLineEntry
|
||||||
|
| str
|
||||||
|
),
|
||||||
|
) -> tuple[
|
||||||
|
str,
|
||||||
|
Type[
|
||||||
|
SectionWithTextEntries
|
||||||
|
| SectionWithOneLineEntries
|
||||||
|
| SectionWithExperienceEntries
|
||||||
|
| SectionWithEducationEntries
|
||||||
|
| SectionWithPublicationEntries
|
||||||
|
| SectionWithNormalEntries
|
||||||
|
],
|
||||||
|
]:
|
||||||
|
"""Determine the entry and section type based on the entry.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
entry (dict[str, Any] | EducationEntry | ExperienceEntry | PublicationEntry |
|
||||||
|
NormalEntry | OneLineEntry | str): The entry to determine the type.
|
||||||
|
Returns:
|
||||||
|
tuple[str, Type[SectionWithTextEntries | SectionWithOneLineEntries |
|
||||||
|
SectionWithExperienceEntries | SectionWithEducationEntries |
|
||||||
|
SectionWithPublicationEntries | SectionWithNormalEntries]]: The entry type and the
|
||||||
|
section type.
|
||||||
|
"""
|
||||||
|
if isinstance(entry, dict):
|
||||||
|
if isinstance(entry, str):
|
||||||
|
entry_type = "TextEntry"
|
||||||
|
section_type = SectionWithTextEntries
|
||||||
|
elif "details" in entry:
|
||||||
|
entry_type = "OneLineEntry"
|
||||||
|
section_type = SectionWithOneLineEntries
|
||||||
|
elif "company" in entry:
|
||||||
|
entry_type = "ExperienceEntry"
|
||||||
|
section_type = SectionWithExperienceEntries
|
||||||
|
elif "institution" in entry:
|
||||||
|
entry_type = "EducationEntry"
|
||||||
|
section_type = SectionWithEducationEntries
|
||||||
|
elif "title" in entry:
|
||||||
|
entry_type = "PublicationEntry"
|
||||||
|
section_type = SectionWithPublicationEntries
|
||||||
|
elif "name" in entry:
|
||||||
|
entry_type = "NormalEntry"
|
||||||
|
section_type = SectionWithNormalEntries
|
||||||
|
else:
|
||||||
|
raise ValueError("The entry is not provided correctly.")
|
||||||
|
else:
|
||||||
|
if isinstance(entry, str):
|
||||||
|
entry_type = "TextEntry"
|
||||||
|
section_type = SectionWithTextEntries
|
||||||
|
elif isinstance(entry, OneLineEntry):
|
||||||
|
entry_type = "OneLineEntry"
|
||||||
|
section_type = SectionWithOneLineEntries
|
||||||
|
elif isinstance(entry, ExperienceEntry):
|
||||||
|
entry_type = "ExperienceEntry"
|
||||||
|
section_type = SectionWithExperienceEntries
|
||||||
|
elif isinstance(entry, EducationEntry):
|
||||||
|
entry_type = "EducationEntry"
|
||||||
|
section_type = SectionWithEducationEntries
|
||||||
|
elif isinstance(entry, PublicationEntry):
|
||||||
|
entry_type = "PublicationEntry"
|
||||||
|
section_type = SectionWithPublicationEntries
|
||||||
|
elif isinstance(entry, NormalEntry):
|
||||||
|
entry_type = "NormalEntry"
|
||||||
|
section_type = SectionWithNormalEntries
|
||||||
|
else:
|
||||||
|
raise RuntimeError(
|
||||||
|
"This error shouldn't have been raised. Please open an issue on GitHub."
|
||||||
|
)
|
||||||
|
|
||||||
|
return entry_type, section_type
|
||||||
|
|
||||||
|
|
||||||
|
def validate_section_input(
|
||||||
|
sections_input: Section | list[Any],
|
||||||
|
) -> Section | list[Any]:
|
||||||
|
"""Validate a SectionInput object and raise an error if it is not valid.
|
||||||
|
|
||||||
|
Sections input is very complex. It is either a `Section` object or a list of
|
||||||
|
entries. Since there are multiple entry types, it is not possible to validate it
|
||||||
|
directly. This function looks at the entry list's first element and determines the
|
||||||
|
section's entry type based on the first element. Then, it validates the rest of the
|
||||||
|
list based on the determined entry type. If it is a `Section` object, then it
|
||||||
|
validates it directly.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sections_input (Section | list[Any]): The sections input to validate.
|
||||||
|
Returns:
|
||||||
|
Section | list[Any]: The validated sections input.
|
||||||
|
"""
|
||||||
|
if isinstance(sections_input, list):
|
||||||
|
# find the entry type based on the first element of the list:
|
||||||
|
try:
|
||||||
|
entry_type, section_type = get_entry_and_section_type(sections_input[0])
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError("The entries are not provided correctly.")
|
||||||
|
|
||||||
|
test_section = {
|
||||||
|
"title": "Test Section",
|
||||||
|
"entry_type": entry_type,
|
||||||
|
"entries": sections_input,
|
||||||
|
}
|
||||||
|
|
||||||
|
section_type.model_validate(test_section)
|
||||||
|
|
||||||
|
return sections_input
|
||||||
|
|
||||||
|
|
||||||
|
# Create a custom type called SectionInput so that it can be validated with
|
||||||
|
# `validate_section_input` function.
|
||||||
|
SectionInput = Annotated[
|
||||||
|
Section
|
||||||
|
| list[
|
||||||
|
EducationEntry
|
||||||
|
| ExperienceEntry
|
||||||
|
| PublicationEntry
|
||||||
|
| NormalEntry
|
||||||
|
| OneLineEntry
|
||||||
|
| str
|
||||||
|
],
|
||||||
|
pydantic.BeforeValidator(validate_section_input),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
# ======================================================================================
|
# ======================================================================================
|
||||||
# Full RenderCV data models: ===========================================================
|
# Full RenderCV data models: ===========================================================
|
||||||
# ======================================================================================
|
# ======================================================================================
|
||||||
|
|
||||||
# RenderCV requires users to specify the entry type for each section in their CV in
|
|
||||||
# order to render the correct thing in the CV. However, for certain sections, specifying
|
|
||||||
# the entry type can be redundant (for example, for the "Education" section, the entry
|
|
||||||
# type is probably "EducationEntry"). To simplify this process for users, default entry
|
|
||||||
# types are stored in a dictionary for certain section titles so that users do not have
|
|
||||||
# to specify them.
|
|
||||||
|
|
||||||
# If you have new section titles that you would like to add to this dictionary, please
|
|
||||||
# open an issue or pull request on GitHub.
|
|
||||||
default_entry_types_for_a_given_title: dict[
|
|
||||||
str,
|
|
||||||
tuple[type[EducationEntry], type[SectionWithEducationEntries]]
|
|
||||||
| tuple[type[ExperienceEntry], type[SectionWithExperienceEntries]]
|
|
||||||
| tuple[type[PublicationEntry], type[SectionWithPublicationEntries]]
|
|
||||||
| tuple[type[NormalEntry], type[SectionWithNormalEntries]]
|
|
||||||
| tuple[type[OneLineEntry], type[SectionWithOneLineEntries]]
|
|
||||||
| tuple[type[str], type[SectionWithTextEntries]],
|
|
||||||
] = {
|
|
||||||
"Education": (EducationEntry, SectionWithEducationEntries),
|
|
||||||
"Experience": (ExperienceEntry, SectionWithExperienceEntries),
|
|
||||||
"Work Experience": (ExperienceEntry, SectionWithExperienceEntries),
|
|
||||||
"Research Experience": (ExperienceEntry, SectionWithExperienceEntries),
|
|
||||||
"Publications": (PublicationEntry, SectionWithPublicationEntries),
|
|
||||||
"Papers": (PublicationEntry, SectionWithPublicationEntries),
|
|
||||||
"Projects": (NormalEntry, SectionWithNormalEntries),
|
|
||||||
"Academic Projects": (NormalEntry, SectionWithNormalEntries),
|
|
||||||
"University Projects": (NormalEntry, SectionWithNormalEntries),
|
|
||||||
"Personal Projects": (NormalEntry, SectionWithNormalEntries),
|
|
||||||
"Certificates": (NormalEntry, SectionWithNormalEntries),
|
|
||||||
"Extracurricular Activities": (ExperienceEntry, SectionWithExperienceEntries),
|
|
||||||
"Test Scores": (OneLineEntry, SectionWithOneLineEntries),
|
|
||||||
"Skills": (OneLineEntry, SectionWithOneLineEntries),
|
|
||||||
"Programming Skills": (NormalEntry, SectionWithNormalEntries),
|
|
||||||
"Other Skills": (OneLineEntry, SectionWithOneLineEntries),
|
|
||||||
"Awards": (OneLineEntry, SectionWithOneLineEntries),
|
|
||||||
"Interests": (OneLineEntry, SectionWithOneLineEntries),
|
|
||||||
"Summary": (str, SectionWithTextEntries),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class SocialNetwork(RenderCVBaseModel):
|
class SocialNetwork(RenderCVBaseModel):
|
||||||
"""This class is the data model of a social network."""
|
"""This class is the data model of a social network."""
|
||||||
|
@ -667,19 +768,14 @@ class SocialNetwork(RenderCVBaseModel):
|
||||||
"""Check if the `SocialNetwork` is provided correctly."""
|
"""Check if the `SocialNetwork` is provided correctly."""
|
||||||
if model.network == "Mastodon":
|
if model.network == "Mastodon":
|
||||||
if not model.username.startswith("@"):
|
if not model.username.startswith("@"):
|
||||||
raise ValueError(
|
raise ValueError("Mastodon username should start with '@'!", "username")
|
||||||
"Mastodon username should start with '@'. The username is"
|
|
||||||
f" {model.username}."
|
|
||||||
)
|
|
||||||
if model.username.count("@") > 2:
|
if model.username.count("@") > 2:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"Mastodon username should contain only two '@'. The username is"
|
"Mastodon username should contain only two '@'!", "username"
|
||||||
f" {model.username}."
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return model
|
return model
|
||||||
|
|
||||||
# @pydantic.computed_field
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def url(self) -> pydantic.HttpUrl:
|
def url(self) -> pydantic.HttpUrl:
|
||||||
"""Return the URL of the social network."""
|
"""Return the URL of the social network."""
|
||||||
|
@ -730,65 +826,13 @@ class CurriculumVitae(RenderCVBaseModel):
|
||||||
"The social networks of the person. They will be rendered in the heading."
|
"The social networks of the person. They will be rendered in the heading."
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
sections_input: dict[
|
sections_input: dict[str, SectionInput] = pydantic.Field(
|
||||||
str,
|
|
||||||
Section
|
|
||||||
| list[
|
|
||||||
EducationEntry
|
|
||||||
| ExperienceEntry
|
|
||||||
| PublicationEntry
|
|
||||||
| NormalEntry
|
|
||||||
| OneLineEntry
|
|
||||||
| str
|
|
||||||
],
|
|
||||||
] = pydantic.Field(
|
|
||||||
default=None,
|
default=None,
|
||||||
title="Sections",
|
title="Sections",
|
||||||
description="The sections of the CV.",
|
description="The sections of the CV.",
|
||||||
alias="sections",
|
alias="sections",
|
||||||
)
|
)
|
||||||
|
|
||||||
@pydantic.field_validator("sections_input", mode="before")
|
|
||||||
@classmethod
|
|
||||||
def validate_sections(
|
|
||||||
cls,
|
|
||||||
sections_input: dict[str, Section | list[Any]],
|
|
||||||
) -> dict[str, Section | list[Any]]:
|
|
||||||
"""
|
|
||||||
Parse and validate the sections of the CV.
|
|
||||||
"""
|
|
||||||
if sections_input is not None:
|
|
||||||
for title, section_or_entries in sections_input.items():
|
|
||||||
if isinstance(section_or_entries, list):
|
|
||||||
title = title.replace("_", " ").title()
|
|
||||||
if title in default_entry_types_for_a_given_title:
|
|
||||||
(
|
|
||||||
entry_type,
|
|
||||||
section_type,
|
|
||||||
) = default_entry_types_for_a_given_title[title]
|
|
||||||
|
|
||||||
if entry_type is str:
|
|
||||||
entry_type = "TextEntry"
|
|
||||||
else:
|
|
||||||
entry_type = entry_type.__name__
|
|
||||||
|
|
||||||
test_section = {
|
|
||||||
"title": title,
|
|
||||||
"entry_type": entry_type,
|
|
||||||
"entries": section_or_entries,
|
|
||||||
}
|
|
||||||
|
|
||||||
section_type.model_validate(test_section)
|
|
||||||
|
|
||||||
else:
|
|
||||||
raise ValueError(
|
|
||||||
f'The entry type for the section "{title}" is not provided!'
|
|
||||||
" Please provide an entry type."
|
|
||||||
)
|
|
||||||
|
|
||||||
return sections_input
|
|
||||||
|
|
||||||
# @pydantic.computed_field
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def sections(self) -> list[Section]:
|
def sections(self) -> list[Section]:
|
||||||
"""Return all the sections of the CV with their titles."""
|
"""Return all the sections of the CV with their titles."""
|
||||||
|
@ -797,28 +841,17 @@ class CurriculumVitae(RenderCVBaseModel):
|
||||||
for title, section_or_entries in self.sections_input.items():
|
for title, section_or_entries in self.sections_input.items():
|
||||||
title = title.replace("_", " ").title()
|
title = title.replace("_", " ").title()
|
||||||
if isinstance(section_or_entries, list):
|
if isinstance(section_or_entries, list):
|
||||||
if title in default_entry_types_for_a_given_title:
|
entry_type, section_type = get_entry_and_section_type(
|
||||||
(
|
section_or_entries[0]
|
||||||
entry_type,
|
)
|
||||||
section_type,
|
|
||||||
) = default_entry_types_for_a_given_title[title]
|
|
||||||
|
|
||||||
if entry_type is str:
|
section = section_type(
|
||||||
entry_type = "TextEntry"
|
title=title,
|
||||||
else:
|
entry_type=entry_type, # type: ignore
|
||||||
entry_type = entry_type.__name__
|
entries=section_or_entries, # type: ignore
|
||||||
|
)
|
||||||
|
sections.append(section)
|
||||||
|
|
||||||
section = section_type(
|
|
||||||
title=title,
|
|
||||||
entry_type=entry_type, # type: ignore
|
|
||||||
entries=section_or_entries, # type: ignore
|
|
||||||
)
|
|
||||||
sections.append(section)
|
|
||||||
else:
|
|
||||||
raise RuntimeError(
|
|
||||||
"This error shouldn't have been raised. Please open an"
|
|
||||||
" issue on GitHub."
|
|
||||||
)
|
|
||||||
elif hasattr(section_or_entries, "entry_type"):
|
elif hasattr(section_or_entries, "entry_type"):
|
||||||
if section_or_entries.title is None:
|
if section_or_entries.title is None:
|
||||||
section_or_entries.title = title
|
section_or_entries.title = title
|
||||||
|
|
Loading…
Reference in New Issue