chore: Added __init__ files, added fallback for callbacks, refactoring & improvements

This commit is contained in:
ITQ
2024-05-19 16:24:19 +03:00
parent b51c135b14
commit 4bc3025ab4
12 changed files with 291 additions and 138 deletions
+14 -22
View File
@@ -6,17 +6,8 @@ from aiogram import Bot, Dispatcher
from aiogram.enums import ParseMode from aiogram.enums import ParseMode
from aiogram.fsm.storage.redis import RedisStorage from aiogram.fsm.storage.redis import RedisStorage
from app.callbacks import location, menu, notes, profile, travel from app import callbacks, handlers, middlewares
from app.config import Config from app.config import Config
from app.handlers import (
create_travel_command,
help_command,
menu_command,
profile_command,
start_command,
travels_command,
)
from app.middlewares.throttling import ThrottlingMiddleware
async def main() -> None: async def main() -> None:
@@ -29,20 +20,21 @@ async def main() -> None:
dp = Dispatcher(storage=storage) dp = Dispatcher(storage=storage)
bot = Bot(bot_token, parse_mode=ParseMode.HTML) bot = Bot(bot_token, parse_mode=ParseMode.HTML)
dp.message.middleware(ThrottlingMiddleware(0.5)) dp.message.middleware(middlewares.throttling.ThrottlingMiddleware(0.5))
dp.include_routers( dp.include_routers(
start_command.router, handlers.start_command.router,
help_command.router, handlers.help_command.router,
menu_command.router, handlers.menu_command.router,
profile_command.router, handlers.profile_command.router,
create_travel_command.router, handlers.create_travel_command.router,
travels_command.router, handlers.travels_command.router,
menu.router, callbacks.menu.router,
profile.router, callbacks.profile.router,
travel.router, callbacks.travel.router,
location.router, callbacks.location.router,
notes.router, callbacks.notes.router,
callbacks.fallback.router,
) )
await bot.delete_webhook(drop_pending_updates=True) await bot.delete_webhook(drop_pending_updates=True)
+3
View File
@@ -0,0 +1,3 @@
__all__ = ("fallback", "location", "menu", "profile", "travel", "notes")
from app.callbacks import fallback, location, menu, notes, profile, travel
+17
View File
@@ -0,0 +1,17 @@
__all__ = ("router",)
from aiogram import Router
from aiogram.filters import StateFilter
from aiogram.types import CallbackQuery
router = Router(name="fallback_callback")
@router.callback_query(~StateFilter(None))
async def in_state_callback(callback: CallbackQuery):
await callback.answer("Fallback text in state", show_alert=True)
@router.callback_query()
async def fallback_callback(callback: CallbackQuery):
await callback.answer("Fallback text", show_alert=True)
+29 -7
View File
@@ -17,7 +17,7 @@ from app.states.travel import (
) )
router = Router(name="menu_callback") router = Router(name="notes_callback")
@router.callback_query( @router.callback_query(
@@ -110,12 +110,26 @@ async def create_note_file_id(message: Message, state: FSMContext):
data["author_id"] = message.from_user.id data["author_id"] = message.from_user.id
session.add(Note(**data)) note = Note(**data)
session.add(note)
session.commit() session.commit()
if not note or note == []:
return
if note.file_type == "photo":
file_name = f"Photo ID: {note.id}"
elif note.file_type == "video":
file_name = f"Video ID: {note.id}"
elif note.file_type == "voice":
file_name = f"Voice ID: {note.id}"
else:
file_name = note.file_name
await message.answer( await message.answer(
messages.NOTE_ADDED.format(file_name=data["file_name"]), messages.NOTE_ADDED.format(file_name=file_name),
) )
await state.clear() await state.clear()
@@ -299,13 +313,21 @@ async def travel_notedelete_callback(
note = Note().get_note_queryset_by_id(note_id) note = Note().get_note_queryset_by_id(note_id)
note_first = note.first()
file_name = note_first.file_name
travel = note_first.travel
if not note or note == []: if not note or note == []:
return return
note_first = note.first()
travel = note_first.travel
if note_first.file_type == "photo":
file_name = f"Photo ID: {note_first.id}"
elif note_first.file_type == "video":
file_name = f"Video ID: {note_first.id}"
elif note_first.file_type == "voice":
file_name = f"Voice ID: {note_first.id}"
else:
file_name = note_first.file_name
note.delete() note.delete()
session.commit() session.commit()
+34 -1
View File
@@ -11,6 +11,7 @@ from app.config import Config
from app.filters.user import Registered, RegisteredCallback from app.filters.user import Registered, RegisteredCallback
from app.keyboards.builders import travels_keyboard from app.keyboards.builders import travels_keyboard
from app.keyboards.travel import get as travel_get from app.keyboards.travel import get as travel_get
from app.keyboards.routes import get as routes_get
from app.models.travel import Travel from app.models.travel import Travel
from app.models.user import User from app.models.user import User
from app.states.travel import ( from app.states.travel import (
@@ -19,7 +20,7 @@ from app.states.travel import (
from app.utils.states import delete_message_from_state, handle_validation_error from app.utils.states import delete_message_from_state, handle_validation_error
router = Router(name="menu_callback") router = Router(name="travel_callback")
@router.callback_query( @router.callback_query(
@@ -301,3 +302,35 @@ async def delete_travel_callback(
) )
await callback.answer() await callback.answer()
@router.callback_query(
F.data.startswith("travel_routes"),
RegisteredCallback(),
StateFilter(None),
)
async def travel_routes_callback(
callback: CallbackQuery,
):
if (
callback.message is None
or callback.data is None
or not isinstance(callback.message, Message)
):
return
travel_id = int(callback.data.replace("travel_routes_", ""))
travel = Travel().get_travel_by_id(travel_id)
if not travel:
await callback.answer()
return
await callback.message.edit_text(
travel.get_travel_text(),
reply_markup=routes_get(travel),
)
await callback.answer()
+17
View File
@@ -0,0 +1,17 @@
__all__ = (
"start_command",
"help_command",
"menu_command",
"profile_command",
"create_travel_command",
"travels_command",
)
from app.handlers import (
create_travel_command,
help_command,
menu_command,
profile_command,
start_command,
travels_command,
)
View File
+63
View File
@@ -0,0 +1,63 @@
__all__ = ("get",)
from aiogram import types
from aiogram.utils.keyboard import InlineKeyboardBuilder
from app.models.travel import Travel
from app.utils.geo import get_location_by_name
from app.utils.map import get_url_map
def get(travel: Travel):
locations = Travel().get_sorted_locations(travel, asc=False)
coordinats = []
for location in locations:
geocode = get_location_by_name(location.location)
coordinats.append(
[geocode[1].raw.get("lat"), geocode[1].raw.get("lon")],
)
builder = InlineKeyboardBuilder()
builder.row(
types.InlineKeyboardButton(
text="🗺️ Route by car",
web_app=types.WebAppInfo(
url=get_url_map(
coordinats=coordinats,
profile="car",
),
),
),
)
builder.row(
types.InlineKeyboardButton(
text="🗺️ Route on foot",
web_app=types.WebAppInfo(
url=get_url_map(
coordinats=coordinats,
profile="foot",
),
),
),
)
builder.row(
types.InlineKeyboardButton(
text="🗺️ Route by bike",
web_app=types.WebAppInfo(
url=get_url_map(
coordinats=coordinats,
profile="bike",
),
),
),
)
builder.row(
types.InlineKeyboardButton(
text="⬅️",
callback_data=f"travel_detail_{travel.id}",
),
)
return builder.as_markup()
+4 -78
View File
@@ -4,20 +4,9 @@ from aiogram import types
from aiogram.utils.keyboard import InlineKeyboardBuilder from aiogram.utils.keyboard import InlineKeyboardBuilder
from app.models.travel import Travel from app.models.travel import Travel
from app.utils.geo import get_location_by_name
from app.utils.map import get_url_map
def get(travel: Travel): def get(travel: Travel):
locations = Travel().get_sorted_locations(travel, asc=False)
coordinats = []
for location in locations:
geocode = get_location_by_name(location.location)
coordinats.append(
[geocode[1].raw.get("lat"), geocode[1].raw.get("lon")],
)
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
builder.row( builder.row(
@@ -62,35 +51,8 @@ def get(travel: Travel):
) )
builder.row( builder.row(
types.InlineKeyboardButton( types.InlineKeyboardButton(
text="🗺️ Route by car", text="🗺️ Routes",
web_app=types.WebAppInfo( callback_data=f"travel_routes_{travel.id}",
url=get_url_map(
coordinats=coordinats,
profile="car",
),
),
),
)
builder.row(
types.InlineKeyboardButton(
text="🗺️ Route on foot",
web_app=types.WebAppInfo(
url=get_url_map(
coordinats=coordinats,
profile="foot",
),
),
),
)
builder.row(
types.InlineKeyboardButton(
text="🗺️ Route by bike",
web_app=types.WebAppInfo(
url=get_url_map(
coordinats=coordinats,
profile="bike",
),
),
), ),
) )
builder.row( builder.row(
@@ -110,15 +72,6 @@ def get(travel: Travel):
def get_public(travel: Travel): def get_public(travel: Travel):
locations = Travel().get_sorted_locations(travel, asc=False)
coordinats = []
for location in locations:
geocode = get_location_by_name(location.location)
coordinats.append(
[geocode[1].raw.get("lat"), geocode[1].raw.get("lon")],
)
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
builder.row( builder.row(
@@ -145,35 +98,8 @@ def get_public(travel: Travel):
) )
builder.row( builder.row(
types.InlineKeyboardButton( types.InlineKeyboardButton(
text="🗺️ Route by car", text="🗺️ Routes",
web_app=types.WebAppInfo( callback_data=f"travel_routes_{travel.id}",
url=get_url_map(
coordinats=coordinats,
profile="car",
),
),
),
)
builder.row(
types.InlineKeyboardButton(
text="🗺️ Route on foot",
web_app=types.WebAppInfo(
url=get_url_map(
coordinats=coordinats,
profile="foot",
),
),
),
)
builder.row(
types.InlineKeyboardButton(
text="🗺️ Route by bike",
web_app=types.WebAppInfo(
url=get_url_map(
coordinats=coordinats,
profile="bike",
),
),
), ),
) )
builder.row( builder.row(
+106 -29
View File
@@ -1,10 +1,24 @@
# flake8: noqa # flake8: noqa
# Menu
MENU = "<b>Menu:</b>" MENU = "<b>Menu:</b>"
# Notes
NOTES = "📝 <b>Notes:</b>\n" NOTES = "📝 <b>Notes:</b>\n"
NOTE_DETAIL = "📝 <b>Note detail:</b>\n\n\tFile name: <b>{file_name}</b>\n\tFile type: <b>{file_type}</b>\n\tPublic: <b>{public}</b>" NOTE_DETAIL = (
"📝 <b>Note detail:</b>\n\n"
"\tFile name: <b>{file_name}</b>\n"
"\tFile type: <b>{file_type}</b>\n"
"\tPublic: <b>{public}</b>"
)
ADD_NOTE = (
"✏️ Send me a file, photo, video, or voice message to add a note."
"\n<i>Enter /cancel to cancel creating.</i>"
)
NOTE_ADDED = "✅ Note <b>{file_name}</b> added successfully." NOTE_ADDED = "✅ Note <b>{file_name}</b> added successfully."
NOTE_DELETED = "❌ Note <b>{file_name}</b> deleted." NOTE_DELETED = "❌ Note <b>{file_name}</b> deleted."
# Locations
LOCATIONS = "🗺️ <b>Locations:</b>" LOCATIONS = "🗺️ <b>Locations:</b>"
LOCATION_DELETED = "❌ Location deleted." LOCATION_DELETED = "❌ Location deleted."
LOCATION_DETAIL = ( LOCATION_DETAIL = (
@@ -16,48 +30,77 @@ LOCATION_DETAIL = (
LOCATION_WEATHER = ( LOCATION_WEATHER = (
"🌤️ <b>{location} weather:</b>\n\n" "🌤️ <b>{location} weather:</b>\n\n"
"\t<b>☁️ Weather:</b> {weather_main}\n" "\t<b>☁️ Weather:</b> {weather_main}\n"
"\t<b>🌡️ Current tempurature:</b> {temp} °C\n" "\t<b>🌡️ Current temperature:</b> {temp} °C\n"
"\t<b>🤗 Feels like:</b> {feels_like} °C\n" "\t<b>🤗 Feels like:</b> {feels_like} °C\n"
"\t<b>❄️ Min. tempurature:</b> {temp_min} °C\n" "\t<b>❄️ Min. temperature:</b> {temp_min} °C\n"
"\t<b>🔥 Max. tempurature:</b> {temp_max} °C\n" "\t<b>🔥 Max. temperature:</b> {temp_max} °C\n"
"\t<b>⬇️ Pressure:</b> {pressure} hektopascals\n" "\t<b>⬇️ Pressure:</b> {pressure} hektopascals\n"
"\t<b>💨 Humidity:</b> {humidity}%\n" "\t<b>💨 Humidity:</b> {humidity}%\n"
) )
# Sights
SIGHTS_HEADER = "🗺️ <b>Sights:</b>\n" SIGHTS_HEADER = "🗺️ <b>Sights:</b>\n"
SIGHTS_FOOTER = "Found {sights_count} sights within {distance} m from: <b>{location}</b>." SIGHTS_FOOTER = (
"Found {sights_count} sights within {distance} m from: <b>{location}</b>."
)
NO_SIGHTS_FOUND = ( NO_SIGHTS_FOUND = (
"No sights found within {distance} m from: <b>{location}</b>." "No sights found within {distance} m from: <b>{location}</b>."
) )
SIGHT_DETAIL = "🗺️ <b>Sight detail:</b>\n\n" SIGHT_DETAIL = "🗺️ <b>Sight detail:</b>\n\n"
CREATE_LOCATION = "✈️ Lets create new location!"
ENTER_LOCATION = "Enter location:\n<i>Format: country, city, ... etc</i>\n<i>Example: Kremlin, Moscow, Russia</i>\n<i>Enter /cancel to cancel creating.</i>" # Location Creation
CREATE_LOCATION = "✈️ Lets create a new location!"
ENTER_LOCATION = (
"Enter location:\n"
"<i>Format: country, city, ... etc</i>\n"
"<i>Example: Kremlin, Moscow, Russia</i>\n"
"<i>Enter /cancel to cancel creating.</i>"
)
CONFIRM_LOCATION = "Is this location correct: <b>{location}</b>?" CONFIRM_LOCATION = "Is this location correct: <b>{location}</b>?"
CONFIRMATION_REEJECTED = ( CONFIRMATION_REJECTED = (
"❌ Confirmation rejected. Please re-enter the location." "❌ Confirmation rejected. Please re-enter the location."
) )
OVERLAPPING_LOCATION = "Dates overlap with another location in the same travel(enter /cancel if you cant fix this)." OVERLAPPING_LOCATION = "Dates overlap with another location in the same travel(enter /cancel if you can't fix this)."
ENTER_LOCATION_DATE_START = "Enter location start datetime(in UTC) in this format:\n<i>Format: YYYY-MM-DD HH:MM</i>\n<i>Example: 2022-01-01 00:00</i>" ENTER_LOCATION_DATE_START = (
ENTER_LOCATION_DATE_END = "Enter location end datetime(in UTC) in this format:\n<i>Format: YYYY-MM-DD HH:MM</i>\n<i>Example: 2022-01-01 00:00</i>" "Enter location start datetime(in UTC) in this format:\n"
"<i>Format: YYYY-MM-DD HH:MM</i>\n"
"<i>Example: 2022-01-01 00:00</i>"
)
ENTER_LOCATION_DATE_END = (
"Enter location end datetime(in UTC) in this format:\n"
"<i>Format: YYYY-MM-DD HH:MM</i>\n"
"<i>Example: 2022-01-01 00:00>"
)
INVALID_DATE_END = "End date can't be earlier or equal to start date." INVALID_DATE_END = "End date can't be earlier or equal to start date."
LOCATION_ADDED = "✅ Location added" LOCATION_ADDED = "✅ Location added"
ADD_NOTE = "✏️ Send me file, photo, video or voice nessage to add note.\n<i>Enter /cancel to cancel creating.</i>" # Travel Management
DELETED_TRAVEL = "✅ Travel deleted" DELETED_TRAVEL = "✅ Travel deleted"
TRAVELS = "📃 <b>Travels:</b>\n<i>👑 - owner</i>" TRAVELS = (
"📃 <b>Travels:</b>"
"\n<i>👑 - owner</i>"
)
NO_TRAVELS = "No travels yet. You can create one with /create_travel command." NO_TRAVELS = "No travels yet. You can create one with /create_travel command."
CREATE_TRAVEL = ( CREATE_TRAVEL = (
"🧳 Let's create new travel!\n<i>Enter /cancel to cancel creating.</i>" "🧳 Let's create a new travel!\n"
"<i>Enter /cancel to cancel creating.</i>"
) )
TRAVEL_UPDATED = "✅ Travel updated" TRAVEL_UPDATED = "✅ Travel updated"
EDIT_TRAVEL_DESCRIPTION = "Enter travel description (enter /skip if you want to set it to None):\n<i>Maximum length: 100 characters</i>" EDIT_TRAVEL_DESCRIPTION = (
"Enter travel description (enter /skip if you want to set it to None):\n"
"<i>Maximum length: 100 characters</i>"
)
INPUT_TRAVEL_TITLE = ( INPUT_TRAVEL_TITLE = (
"Enter travel title:\n<i>Maximum length: 30 characters</i>" "Enter travel title:\n"
"<i>Maximum length: 30 characters</i>"
) )
INPUT_TRAVEL_CALLBACK = ( INPUT_TRAVEL_CALLBACK = (
"All right, travel <b>{key}</b> is set to: <b>{value}</b>" "All right, travel <b>{key}</b> is set to: <b>{value}</b>"
) )
INPUT_TRAVEL_DESCRIPTION = "Enter travel description (enter /skip if you want to skip this step):\n<i>Maximum length: 100 characters</i>" INPUT_TRAVEL_DESCRIPTION = (
"Enter travel description (enter /skip if you want to skip this step):\n"
"<i>Maximum length: 100 characters</i>"
)
INPUT_TRAVEL_DESCRIPTION_SKIPPED = "✅ Sure. You can always fill it later." INPUT_TRAVEL_DESCRIPTION_SKIPPED = "✅ Sure. You can always fill it later."
TRAVEL_CREATED = "Travel <b>{title}</b> successfully created! You can now view and edit it in the travels list (/travels command)." TRAVEL_CREATED = "Travel <b>{title}</b> successfully created! You can now view and edit it in the travels list (/travels command)."
ACTION_CANCELED = "❌ Action canceled" ACTION_CANCELED = "❌ Action canceled"
@@ -68,11 +111,12 @@ TRAVEL_DETAIL = (
"\tDescription: <b>{description}</b>\n" "\tDescription: <b>{description}</b>\n"
) )
# User Interaction
WELCOME_MESSAGE = "Hello, <b>{name}</b>! Welcome to the ✈️ Travel Agent bot! Let's start our journey by filling out some information about you." WELCOME_MESSAGE = "Hello, <b>{name}</b>! Welcome to the ✈️ Travel Agent bot! Let's start our journey by filling out some information about you."
WELCOME_AGAIN_MESSAGE = "Hello, <b>{name}</b>! Welcome back to the ✈️ Travel Agent bot! If you get lost, you can always call the /help command for assistance." WELCOME_AGAIN_MESSAGE = "Hello, <b>{name}</b>! Welcome back to the ✈️ Travel Agent bot! If you get lost, you can always call the /help command for assistance."
HELP_MESSAGE = ( HELP_MESSAGE = (
"Welcome to the ✈️ Travel Agent bot! Here is list of commands you can use:\n\n" "Welcome to the ✈️ Travel Agent bot! Here is a list of commands you can use:\n\n"
"/start - Start the bot\n" "/start - Start the bot\n"
"/help - Show this message\n" "/help - Show this message\n"
"/menu - Show the main menu\n" "/menu - Show the main menu\n"
@@ -83,18 +127,39 @@ HELP_MESSAGE = (
"❓ If you have any questions/issues, feel free to contact us via @itq_travel_agent_support_bot on Telegram." "❓ If you have any questions/issues, feel free to contact us via @itq_travel_agent_support_bot on Telegram."
) )
REGISTERED_MESSAGE = "You have successfully registered. Welcome to the ✈️ Travel Agent bot! \nYou can view and edit your profile using the /profile command." REGISTERED_MESSAGE = (
"You have successfully registered. Welcome to the ✈️ Travel Agent bot!\n"
"You can view and edit your profile using the /profile command."
)
INPUT_USERNAME = "Enter your username (this will be used to interact with other users):\n<i>Allowed characters: a-z, A-Z, 0-9, _</i>\n<i>Length: 5-20 characters</i>" INPUT_USERNAME = (
INPUT_AGE = "Enter your age:\n<i>Range: 13-120</i>" "Enter your username (this will be used to interact with other users):\n"
INPUT_SEX = "Enter your sex:\n<i>Options: Male or Female</i>" "<i>Allowed characters: a-z, A-Z, 0-9, _</i>\n"
INPUT_BIO = "Enter your bio (enter /skip if you want to skip this step):\n<i>Maximum length: 100 characters</i>" "<i>Length: 5-20 characters</i>"
)
INPUT_AGE = (
"Enter your age:\n"
"<i>Range: 13-120</i>"
)
INPUT_SEX = (
"Enter your sex:\n"
"<i>Options: Male or Female</i>"
)
INPUT_BIO = (
"Enter your bio (enter /skip if you want to skip this step):\n"
"<i>Maximum length: 100 characters</i>"
)
INPUT_BIO_SKIPPED = "✅ Sure. You can always fill it later." INPUT_BIO_SKIPPED = "✅ Sure. You can always fill it later."
INPUT_LOCATION = "Enter your location in this format:\n<i>Format: country, city</i>\n<i>Example: Russia, Moscow</i>" INPUT_LOCATION = (
"Enter your location in this format:\n"
"<i>Format: country, city</i>\n"
"<i>Example: Russia, Moscow</i>"
)
INPUT_CALLBACK = "✅ All right, your <b>{key}</b> is set to: <b>{value}</b>" INPUT_CALLBACK = "✅ All right, your <b>{key}</b> is set to: <b>{value}</b>"
VALIDATION_ERROR = "❌ Invalid input. Please try again." VALIDATION_ERROR = "❌ Invalid input. Please try again."
CANCEL_CHANGE = "<i>Enter /cancel to cancel change.</i>" CANCEL_CHANGE = "<i>Enter /cancel to cancel change.</i>"
# User Profile
PROFILE = ( PROFILE = (
"<b>👤 Your profile:</b>\n\n" "<b>👤 Your profile:</b>\n\n"
"\tUsername: <b>{username}</b>\n" "\tUsername: <b>{username}</b>\n"
@@ -106,9 +171,21 @@ PROFILE = (
"\tDate joined: <b>{date_joined} UTC</b>\n" "\tDate joined: <b>{date_joined} UTC</b>\n"
) )
NOT_SET = "<i>Not set</i>" NOT_SET = "<i>Not set</i>"
EDIT_USERNAME = "Enter your username:\n<i>Allowed characters: a-z, A-Z, 0-9, _</i>\n<i>Length: 5-20 characters</i>" EDIT_USERNAME = (
EDIT_BIO = "Enter your bio (enter /skip if you want to set it to None):\n<i>Maximum length: 100 characters</i>" "Enter your username:\n"
PROFILE_UPDATED = "✅ Profile {key} updated\n\t<i>Old value: {old_value}</i>\n\t<i>New value: {new_value}</i>" "<i>Allowed characters: a-z, A-Z, 0-9, _</i>\n"
"<i>Length: 5-20 characters</i>"
)
EDIT_BIO = (
"Enter your bio (enter /skip if you want to set it to None):\n"
"<i>Maximum length: 100 characters</i>"
)
PROFILE_UPDATED = (
"✅ Profile {key} updated\n"
"\t<i>Old value: {old_value}</i>\n"
"\t<i>New value: {new_value}</i>"
)
CHANGE_CANCELED = "❌ Change canceled" CHANGE_CANCELED = "❌ Change canceled"
PROCCESSING = "⌛️ Processing..." # Processing
PROCESSING = "⌛️ Processing..."
+3
View File
@@ -0,0 +1,3 @@
__all__ = ("throttling",)
from app.middlewares import throttling
+1 -1
View File
@@ -243,7 +243,7 @@ class Note(Base):
def get_note_text(self): def get_note_text(self):
return messages.NOTE_DETAIL.format( return messages.NOTE_DETAIL.format(
file_name=self.file_name, file_name=self.file_name,
file_type=self.file_type, file_type=self.file_type.capitalize(),
public="Yes" if self.public else "No", public="Yes" if self.public else "No",
) )