Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
89 commits
Select commit Hold shift + click to select a range
e26957f
Fix invite url docs
MattyTheHacker Aug 23, 2024
9a23142
move terminology to new file
MattyTheHacker Aug 23, 2024
e99b152
start contributing guides
MattyTheHacker Aug 23, 2024
36bcb41
formatting
MattyTheHacker Aug 23, 2024
f228d3d
Merge branch 'main' into docs-updates
MattyTheHacker Aug 23, 2024
3cf257e
add some stuff
MattyTheHacker Aug 23, 2024
a60e460
add
MattyTheHacker Aug 23, 2024
36097b3
add links
MattyTheHacker Aug 23, 2024
1f0b222
add example
MattyTheHacker Aug 26, 2024
a085679
add missing cogs from list
MattyTheHacker Aug 26, 2024
37c545e
Merge branch 'main' into docs-updates
MattyTheHacker Aug 26, 2024
4e3d8a7
add link
MattyTheHacker Aug 26, 2024
da2f219
fix link
MattyTheHacker Aug 26, 2024
c007fbe
add backtick
MattyTheHacker Aug 26, 2024
b6f7362
add some stuff
MattyTheHacker Aug 26, 2024
1d5dd11
Merge branch 'main' into docs-updates
MattyTheHacker Sep 25, 2024
bb15019
Merge branch 'main' into docs-updates
MattyTheHacker Dec 23, 2024
c5baccc
Merge branch 'main' into docs-updates
CarrotManMatt Mar 5, 2025
9092bde
Merge branch 'main' into docs-updates
MattyTheHacker Mar 5, 2025
7b7d944
Merge branch 'main' into docs-updates
MattyTheHacker Mar 18, 2025
e60baa3
Merge branch 'main' into docs-updates
MattyTheHacker Apr 2, 2025
22ebb49
Add some stuff
MattyTheHacker Apr 2, 2025
826ac38
Merge branch 'main' into docs-updates
MattyTheHacker Apr 9, 2025
51d79e7
fix warnings
MattyTheHacker Apr 9, 2025
9bf2ebe
remove empty whitespace
MattyTheHacker Apr 9, 2025
8a53cf4
Merge branch 'main' into docs-updates
MattyTheHacker May 10, 2025
a5423b3
more terms
MattyTheHacker May 10, 2025
149c811
Add example cog
MattyTheHacker May 10, 2025
0c24b54
env example
MattyTheHacker May 10, 2025
ef4615d
response button
MattyTheHacker May 10, 2025
b9810d6
Merge branch 'main' into docs-updates
MattyTheHacker May 12, 2025
64207e9
Merge branch 'main' into docs-updates
MattyTheHacker May 13, 2025
dd61e51
blah blah
MattyTheHacker May 13, 2025
a8ab5c8
db stuff first run
MattyTheHacker May 13, 2025
1e5355b
add gdpr warning
MattyTheHacker May 15, 2025
581fb4c
Merge branch 'main' into docs-updates
MattyTheHacker May 18, 2025
762d7f8
Merge branch 'main' into docs-updates
MattyTheHacker May 20, 2025
e0c23a1
Merge branch 'main' into docs-updates
MattyTheHacker May 23, 2025
1a1c6c9
Merge branch 'main' into docs-updates
MattyTheHacker May 25, 2025
9523681
Merge branch 'main' into docs-updates
MattyTheHacker May 27, 2025
88e2ab5
Merge branch 'main' into docs-updates
MattyTheHacker May 27, 2025
84430ff
Apply suggestions from code review
MattyTheHacker Jun 3, 2025
c2087b5
[pre-commit.ci lite] apply automatic fixes
pre-commit-ci-lite[bot] Jun 3, 2025
d1a247e
Merge branch 'main' into docs-updates
MattyTheHacker Jun 3, 2025
03c02fe
Merge main into docs-updates
cssbhamdev Jun 12, 2025
eea9b23
Merge main into docs-updates
cssbhamdev Jun 13, 2025
5edaf39
Fix minor docs errors
CarrotManMatt Jun 13, 2025
c7016f6
Merge main into docs-updates
cssbhamdev Jun 13, 2025
61671d3
Merge main into docs-updates
cssbhamdev Jun 14, 2025
1a6ff75
Allow committee-elect to update actions (and appear in auto-complete)…
Thatsmusic99 Jun 15, 2025
06ee380
Merge main into docs-updates
cssbhamdev Jun 15, 2025
a8ab0d3
Merge main into docs-updates
cssbhamdev Jun 15, 2025
8a669f9
Merge main into docs-updates
cssbhamdev Jun 15, 2025
e116a5a
Merge main into docs-updates
cssbhamdev Jun 15, 2025
6cb7e66
Merge main into docs-updates
cssbhamdev Jun 16, 2025
6afec1e
Merge main into docs-updates
cssbhamdev Jun 16, 2025
428a7a4
Merge main into docs-updates
cssbhamdev Jun 17, 2025
a9e3edb
Merge main into docs-updates
cssbhamdev Jun 19, 2025
125cd01
Merge main into docs-updates
cssbhamdev Jun 19, 2025
451d226
Merge main into docs-updates
cssbhamdev Jun 22, 2025
af03fd9
Merge main into docs-updates
cssbhamdev Jun 24, 2025
546bb76
Merge main into docs-updates
cssbhamdev Jun 24, 2025
f989c5f
Merge main into docs-updates
cssbhamdev Jun 24, 2025
32e7170
Merge main into docs-updates
cssbhamdev Jun 25, 2025
4c7f321
Merge main into docs-updates
cssbhamdev Jun 30, 2025
9d4d4de
Merge main into docs-updates
automatic-pr-updater[bot] Jun 30, 2025
0f1a935
Merge main into docs-updates
automatic-pr-updater[bot] Jul 2, 2025
377b675
Merge main into docs-updates
automatic-pr-updater[bot] Jul 2, 2025
36f7c3f
Merge main into docs-updates
automatic-pr-updater[bot] Jul 3, 2025
2c2381b
Merge main into docs-updates
automatic-pr-updater[bot] Jul 3, 2025
434f1f5
Merge main into docs-updates
automatic-pr-updater[bot] Jul 3, 2025
8a29e7e
Merge main into docs-updates
automatic-pr-updater[bot] Jul 4, 2025
62377c6
Merge main into docs-updates
automatic-pr-updater[bot] Jul 4, 2025
af7aefd
Merge main into docs-updates
automatic-pr-updater[bot] Jul 4, 2025
0bec33f
Merge main into docs-updates
automatic-pr-updater[bot] Jul 4, 2025
548c205
Merge main into docs-updates
automatic-pr-updater[bot] Jul 4, 2025
45017c3
Merge main into docs-updates
automatic-pr-updater[bot] Jul 4, 2025
83bea74
Merge main into docs-updates
automatic-pr-updater[bot] Jul 5, 2025
edcd5d0
Merge main into docs-updates
automatic-pr-updater[bot] Jul 5, 2025
d7be59c
Merge main into docs-updates
automatic-pr-updater[bot] Jul 5, 2025
8c31a1f
Merge main into docs-updates
automatic-pr-updater[bot] Jul 5, 2025
08b22be
Merge main into docs-updates
automatic-pr-updater[bot] Jul 5, 2025
1942666
Merge main into docs-updates
automatic-pr-updater[bot] Jul 6, 2025
8920378
Merge main into docs-updates
automatic-pr-updater[bot] Jul 7, 2025
6a4b9bc
Merge main into docs-updates
automatic-pr-updater[bot] Jul 9, 2025
e065577
Merge main into docs-updates
automatic-pr-updater[bot] Jul 9, 2025
d8bd8b7
Merge main into docs-updates
automatic-pr-updater[bot] Jul 11, 2025
a98f5a9
Merge main into docs-updates
automatic-pr-updater[bot] Jul 14, 2025
23580ea
Merge main into docs-updates
automatic-pr-updater[bot] Jul 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
334 changes: 327 additions & 7 deletions CONTRIBUTING.md
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add inline hyperlinks from any defined terms to their definitions within TERMINOLOGY.md.
I have provided one example in a suggestion.

Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ We recommend also reading the following if you're unsure or not confident:
* [Contributing To An Open Source Project For The First Time](https://firsttimersonly.com)

TeX-Bot is written in [Python](https://python.org) using [Pycord](https://pycord.dev) and uses Discord's [slash-commands](https://support.discord.com/hc/articles/1500000368501-Slash-Commands-FAQ) & [user-commands](https://guide.pycord.dev/interactions/application-commands/context-menus).
We would recommend being somewhat familiar with the [Pycord library](https://docs.pycord.dev), [Python language](https://docs.python.org/3/reference/index) & [project terminology](README.md#terminology) before contributing.
We would recommend being somewhat familiar with the [Pycord library](https://docs.pycord.dev), [Python language](https://docs.python.org/3/reference/index) & [project terminology](TERMINOLOGY.md) before contributing.

## Using the Issue Tracker

Expand All @@ -52,13 +52,13 @@ If you are submitting a feature request, please include the steps to implement t
### Top level files

* [`main.py`](main.py): is the main entrypoint to instantiate the [`Bot` object](https://docs.pycord.dev/stable/api/clients.html#discord.Bot) & run it
* [`exceptions.py`](exceptions.py): contains common [exception](https://docs.python.org/3/tutorial/errors) subclasses that may be raised when certain errors occur
* [`config.py`](config.py): retrieves the [environment variables](README.md#setting-environment-variables) & populates the correct values into the `settings` object
* [`exceptions/`](exceptions): contains common [exception](https://docs.python.org/3/tutorial/errors) subclasses that may be raised when certain errors occur
* [`config.py`](config.py): retrieves the [environment variables](README.md#setting-environment-variables) and populates the correct values into the `settings` object

### Other significant directories

* [`cogs/`](cogs): contains all the [cogs](https://guide.pycord.dev/popular-topics/cogs) within this project, see [below](#cogs) for more information
* [`utils/`](utils): contains common utility classes & functions used by the top-level modules & cogs
* [`utils/`](utils): contains common utility classes and functions used by the top-level modules and cogs
* [`db/core/models/`](db/core/models): contains all the [database ORM models](https://docs.djangoproject.com/en/stable/topics/db/models) to interact with storing information longer-term (between individual command events)
* [`tests/`](tests): contains the complete test suite for this project, based on the [Pytest framework](https://pytest.org)

Expand All @@ -73,6 +73,8 @@ There are separate cog files for each activity, and one [`__init__.py`](cogs/__i

* [`cogs/__init__.py`](cogs/__init__.py): instantiates all the cog classes within this directory

* [`cogs/annual_handover_and_reset.py`](cogs/annual_handover_and_reset.py): cogs for annual handover procedures and role resets

* [`cogs/archive.py`](cogs/archive.py): cogs for archiving categories of channels within your group's Discord guild

* [`cogs/command_error.py`](cogs/command_error.py): cogs for sending error messages when commands fail to complete/execute
Expand All @@ -81,10 +83,14 @@ There are separate cog files for each activity, and one [`__init__.py`](cogs/__i

* [`cogs/edit_message.py`](cogs/edit_message.py): cogs for editing messages that were previously sent by TeX-Bot

* [`cogs/get_token_authorisation.py`](cogs/get_token_authorisation.py): cogs for retrieving the current status of the supplied authentication token

* [`cogs/induct.py`](cogs/induct.py): cogs for inducting people into your group's Discord guild

* [`cogs/kill.py`](cogs/kill.py): cogs related to the shutdown of TeX-Bot

* [`cogs/make_applicant`](cogs/make_applicant.py): cogs related to making users into applicants

* [`cogs/make_member.py`](cogs/make_member.py): cogs related to making guests into members

* [`cogs/ping.py`](cogs/ping.py): cog to request a [ping](https://wikipedia.org/wiki/Ping-pong_scheme#Internet) response
Expand All @@ -101,11 +107,11 @@ There are separate cog files for each activity, and one [`__init__.py`](cogs/__i

* [`cogs/startup.py`](cogs/startup.py): cogs for startup & bot initialisation

* [`cogs/stats.py`](cogs/stats.py): cogs for displaying stats about your group's Discord guild, as well as its channels & Discord members
* [`cogs/stats/`](cogs/stats): cogs for displaying stats about your group's Discord guild, as well as its channels & Discord members

* [`cogs/strike.py`](cogs/strike.py): cogs for applying moderation actions to Discord members

* [`cogs/write_roles.py`](cogs/write_roles.py): cogs relating to sending the message that contains all the opt-in roles, into the "#**roles**" channel
* [`cogs/write_roles.py`](cogs/write_roles.py): cogs relating to sending the message, that contains all the opt-in roles, into the "#**roles**" channel

## Making Your First Contribution

Expand Down Expand Up @@ -149,7 +155,7 @@ It can be run with the following command:
uv run mypy .
```

Although there is [a PyCharm plugin](https://github.com/leinardi/mypy-pycharm#mypy-pycharm) to provide GUI control & inline warnings for [mypy](https://mypy-lang.org), it has been rather temperamental recently.
Although there is [a PyCharm plugin](https://github.com/leinardi/mypy-pycharm#mypy-pycharm) to provide GUI control and inline warnings for [mypy](https://mypy-lang.org), it has been rather temperamental recently.
So it is suggested to avoid using it, and run [mypy](https://mypy-lang.org) from the command-line instead.

#### PyMarkdown
Expand Down Expand Up @@ -200,3 +206,317 @@ If you see an error, we encourage you to **be bold** and fix it yourself, rather
If you are stuck, need help, or have a question, the best place to ask is on our Discord.

Happy contributing!

## Guides

### Creating a New Cog

Cogs are modular components of TeX-Bot that group related commands and listeners into a single class. To create a new cog, follow these steps:

1. **Create the Cog File**
- Navigate to the `cogs/` directory.
- Create a new Python file with a name that reflects the purpose of the cog (e.g., `example_cog.py`).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't suggest an example file name that includes the word cog.


2. **Define the Cog Class**
- Import the necessary modules, including `TeXBotBaseCog` from `utils`.
- Define a class that inherits from `TeXBotBaseCog`.
- Add a docstring to describe the purpose of the cog.

Example:
```python
from utils import TeXBotBaseCog

class ExampleCog(TeXBotBaseCog):
"""A cog for demonstrating functionality."""

def do_something(arguments):
print("do something")
```

3. **Add Commands and Listeners**
- Define methods within the class for commands and event listeners.
- Use decorators like `@discord.slash_command()` or `@TeXBotBaseCog.listener()` to register the callback method to their [interaction](TERMINOLOGY.md#Interactions) type.
- Include any necessary checks using `CommandChecks` decorators.

Example:
```python
import discord
from utils import CommandChecks

__all__: "Sequence[str]" = (
"ExampleCog",
)

class ExampleCog(TeXBotBaseCog):
"""A cog for demonstrating functionality."""

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing methods with decorators in example.

```

4. **Register the Cog**
- Edit `cogs/__init__.py` to add your new cog class to the list of cogs in the `setup` function.
- Also, include the cog class in the `__all__` sequence to ensure it is properly exported.

Example:
```python
from .example_cog import ExampleCog

__all__: "Sequence[str]" = (
...existing cogs...
"ExampleCog",
)

def setup(bot: "TeXBot") -> None:
"""Add all the cogs to the bot, at bot startup."""
cogs: Iterable[type[TeXBotBaseCog]] = (
...existing cogs...
ExampleCog,
)
Cog: type[TeXBotBaseCog]
for Cog in cogs:
bot.add_cog(Cog(bot))
```

5. **Test the Cog**
- Run the bot with your changes and ensure the new cog is loaded without errors.
- Test the commands and listeners to verify they work as expected.

6. **Document the Cog**
- Add comments and docstrings to explain the functionality of the cog.
- Update the `CONTRIBUTING.md` file or other relevant documentation if necessary.

### Creating a New Environment Variable

To add a new environment variable to the project, follow these steps:

1. **Define the Variable in development `.env`**
- Open the `.env` file in the project root directory (or create one if it doesn't exist).
- Add the new variable in the format `VARIABLE_NAME=value`.
- Ensure the variable name is descriptive and uses uppercase letters with underscores.

2. **Update `config.py`**
- Open the `config.py` file.
- Add a new private setup method in the `Settings` class to validate and load the variable. For example:
```python
@classmethod
def _setup_new_variable(cls) -> None:
raw_value: str | None = os.getenv("NEW_VARIABLE")

if not raw_value or not re.fullmatch(r"<validation_regex>", raw_value):
raise ImproperlyConfiguredError("NEW_VARIABLE is invalid or missing.")

cls._settings["NEW_VARIABLE"] = raw_value
```
- Replace `<validation_regex>` with a regular expression to validate the variable's format, if applicable.

3. **Call the Setup Method**
- Add the new setup method to the `_setup_env_variables` method in `config.py`:
```python
@classmethod
def _setup_env_variables(cls) -> None:
if cls._is_env_variables_setup:
logger.warning("Environment variables have already been set up.")
return

cls._settings = {}

cls._setup_new_variable()
# Add other setup methods here

cls._is_env_variables_setup = True
```

4. **Document the Variable**
- Update the `README.md` file under the "Setting Environment Variables" section to include the new variable, its purpose and any valid values.

5. **Test the Variable**
- Run the bot with your changes and ensure the new variable is loaded correctly.
- Test edge cases, such as missing, blank or invalid values in the `.env` file, to confirm that error handling functions correctly.

### Creating a Response Button

Response buttons are interactive UI components that allow users to respond to bot messages with predefined actions. To create a response button, follow these steps:

1. **Define the Button Class**
- Create a new class in your cog file that inherits from `discord.ui.View`.
- Add button callback response methods using the `@discord.ui.button` decorator.
- Each button method should define the button's label, a custom response ID and style.

Example:
```python
from discord.ui import View

class ConfirmActionView(View):
"""A discord.View containing buttons to confirm or cancel an action."""

@discord.ui.button(
label="Yes",
style=discord.ButtonStyle.green,
custom_id="confirm_yes",
)
async def confirm_yes(self, button: discord.Button, interaction: discord.Interaction) -> None:
# Handle the 'Yes' button click
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Use docstrings rather than comments
  • Use double speech marks

await interaction.response.send_message("Action confirmed.", ephemeral=True)

@discord.ui.button(
label="No",
style=discord.ButtonStyle.red,
custom_id="confirm_no",
)
async def confirm_no(self, button: discord.Button, interaction: discord.Interaction) -> None:
# Handle the 'No' button click
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Use docstrings rather than comments
  • Use double speech marks

await interaction.response.send_message("Action cancelled.", ephemeral=True)
```

2. **Send the View with a Message**
- Use the `view` parameter of the `send` or `respond` method to attach the button view to a message. This could be sent in response to a command, event handler or scheduled task.

Example:
```python
await ctx.send(
content="Do you want to proceed?",
view=ConfirmActionView(),
)
```

3. **Handle Button Interactions**
- Define logic within each button method to handle user interactions.
- Use `interaction.response` to send feedback or perform actions based on the button clicked.
Comment on lines +382 to +383
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add example code of handling user interaction in a button method


Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discuss alternative interaction handling where processing continues inside the original cog method that called the button, rather than inside the button handling function.

E.g., the strike commands create have buttons where the callback methods only functionality is to remove the button from the current message, and processing continues inside the command callback with self.bot.wait_for() and button_interaction.data["custom_id"] ==:

async def yes_strike_member_button_callback( # type: ignore[misc]
self, _: discord.Button, interaction: discord.Interaction
) -> None:
"""
Delete the message associated with the view when the Yes button is pressed.
This function is attached as a button's callback, so will run whenever the button
is pressed.
The actual handling of the event is done by the command that sent the view,
so all that is required is to delete the original message that sent this view.
"""
logger.debug('"Yes" button pressed. %s', interaction)
await interaction.response.edit_message(
view=None
) # NOTE: Despite removing the view within the normal command processing loop, the view also needs to be removed here to prevent an Unknown Webhook error
@discord.ui.button(
label="No",
style=discord.ButtonStyle.grey,
custom_id="no_strike_member",
)
async def no_strike_member_button_callback( # type: ignore[misc]
self, _: discord.Button, interaction: discord.Interaction
) -> None:
"""
Delete the message associated with the view when the No button is pressed.
This function is attached as a button's callback, so will run whenever the button
is pressed.
The actual handling of the event is done by the command that sent the view,
so all that is required is to delete the original message that sent this view.
"""
logger.debug('"No" button pressed. %s', interaction)
await interaction.response.edit_message(
view=None
) # NOTE: Despite removing the view within the normal command processing loop, the view also needs to be removed here to prevent an Unknown Webhook error

await message_sender_component.send(
content=confirm_strike_message,
view=ConfirmStrikeMemberView(),
)
button_interaction: discord.Interaction = await self.bot.wait_for(
"interaction",
check=lambda interaction: (
interaction.type == discord.InteractionType.component
and interaction.user == interaction_user
and interaction.channel == button_callback_channel
and "custom_id" in interaction.data
and interaction.data["custom_id"] in {"yes_strike_member", "no_strike_member"}
),
)
if button_interaction.data["custom_id"] == "no_strike_member": # type: ignore[index, typeddict-item]
await button_interaction.edit_original_response(
content=(
"Aborted performing "
f"{self.SUGGESTED_ACTIONS[actual_strike_amount]} action "
f"on {strike_user.mention}."
),
view=None,
)
return
if button_interaction.data["custom_id"] == "yes_strike_member": # type: ignore[index, typeddict-item]

4. **Test the Button**
- Run the bot with your changes and ensure the buttons appear and function as expected.
- Test edge cases, such as multiple users interacting with the buttons simultaneously.

5. **Document the Button**
- Add comments and docstrings to explain the purpose and functionality of the button.
- Update relevant documentation if necessary.

### Creating and Interacting with Django Models

#### Data Protection Consideration

When making changes to the database model, it is essential to consider the data protection implications of these changes. If personal data is being collected, stored or processed, it is essential that this is in compliance with the law. In the UK, the relevant law is the [Data Protection Act 2018](https://www.legislation.gov.uk/ukpga/2018/12/contents). As a general rule, any changes that have data protection implications should be checked and approved by the organisation responsible for running the application.
Django models are used to interact with the database in this project. They allow you to define the structure of your data and provide an API to query and manipulate it. To create and interact with Django models, follow these steps:

1. **Define a Model**
- Navigate to the `db/core/models/` directory.
- Create a new Python file with a name that reflects the purpose of the model (e.g., `example_model.py`).
- If your model is a new property related to each Discord member (E.g. the number of smiley faces of each Discord member, then define a class that inherits from `BaseDiscordMemberWrapper` (found within the `.utils` module within the `db/core/models/` directory).
- If your model is unrelated to Discord members, then define a class that inherits from `AsyncBaseModel` (also found within the `.utils` module within the `db/core/models/` directory).
- Add your Django fields to the class to represent the model's data structure.
- If your model inherits from `BaseDiscordMemberWrapper` then you *must* declare a field called `discord_member` which must be either a Django `ForeignKey` field or a `OneToOneField`, depending upon the relationship between your model and each Discord member.
- Define the static class string holding the display name for multiple instances of your class (E.g., `INSTANCES_NAME_PLURAL: str = "Members' Smiley Faces"`)

Example:
```python
from django.db import models

from .utils import AsyncBaseModel, BaseDiscordMemberWrapper

class ExampleModel(AsyncBaseModel):
"""A model for demonstrating functionality."""

INSTANCES_NAME_PLURAL: str = "Example Model objects"

name = models.CharField(max_length=255)
created_at = models.DateTimeField(auto_now_add=True)
class MemberSmileyFaces(BaseDiscordMemberWrapper):
"""Model to represent the number of smiley faces of each Discord member."""

INSTANCES_NAME_PLURAL: str = "Discord Members' Smiley Faces"

discord_member = models.OneToOneField(
DiscordMember,
on_delete=models.CASCADE,
related_name="smiley_faces",
verbose_name="Discord Member",
blank=False,
null=False,
primary_key=True,
)
count = models.IntegerField(
"Number of smiley faces",
null=False,
blank=True,
default=0,
)

2. **Apply Migrations**
- Run the following commands to create and apply migrations for your new model:
```shell
uv run manage.py makemigrations
uv run manage.py migrate
```

3. **Query the Model**
- Use Django's ORM to interact with the model. For example:
```python
from db.core.models.example_model import ExampleModel
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rename model to not include the term "model"


class ExampleCog(TeXBotBaseCog):
"""A cog for demonstrating model access."""

async def create_example(self, name: str) -> None:
"""Create a new instance of ExampleModel."""
await ExampleModel.objects.acreate(name=name)

async def retrieve_examples(self) -> list[ExampleModel]:
"""Retrieve all instances of ExampleModel."""
return await ExampleModel.objects.all()

async def filter_examples(self, name: str) -> list[ExampleModel]:
"""Filter instances of ExampleModel by name."""
return await ExampleModel.objects.filter(name=name)

async def update_example(self, example: ExampleModel, new_name: str) -> None:
"""Update the name of an ExampleModel instance."""
example.name = new_name
await example.asave()

async def delete_example(self, example: ExampleModel) -> None:
"""Delete an ExampleModel instance."""
await example.adelete()
Comment on lines +458 to +477
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These examples are a bit too simplistic to correctly guide the usage of the ORM query methods. This could work better as a single example cog command, which creates, filters, updates and deletes based on some command options.

```

4. **Document the Model**
- Add comments and docstrings to explain the purpose and functionality of your new model.

### Member Retrieval DB Queries via Hashed Discord ID

To retrieve members from the database using their hashed Discord ID, follow these steps:

1. **Hash the Discord ID**
- Use a consistent hashing algorithm to hash the Discord ID before storing or querying it in the database.

Example:
```python
import hashlib

def hash_discord_id(discord_id: str) -> str:
return hashlib.sha256(discord_id.encode()).hexdigest()
```
Comment on lines +487 to +496
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hashing algorithm of Discord IDs is already implemented and will not need to be changed in new models. This section can be removed from the guide because editing the functionality of the hashing algorithm presumes a level of knowledge beyond these starter guides.


2. **Query the Database**
- A custom filter field is implemented for models that inherit from `BaseDiscordMemberWrapper`, this allows you to filter using the unhashed Discord ID, despite only the hashed Discord ID being stored in the database.

Example:
```python
from db.core.models.member_smiley_faces import MemberSmileyFaces

class MyExampleCommandCog(TeXBotBaseCog):
@tasks.loop(minutes=5)
async def check_member_smiley_faces(self) -> None:
member_smiley_faces = await MemberSmileyFaces.objects.filter(discord_id=1234567).afirst()

if member_smiley_faces:
print(f"Member's smiley faces found: {member_smiley_faces.discord_member.name}")
else:
print("Member's smiley facesnot found.")
```

It is unlikely that you will need to query the `DiscordMember` model directly. Instead, the attributes of the member can be accessed by the relationship between each new Django model to the `DiscordMember` model.

3. **Test the Query**
- Ensure the query works as expected by testing it with valid and invalid Discord IDs.

4. **Document the Query**
- Add comments and docstrings to explain the purpose and functionality of the query.
Comment on lines +521 to +522
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unnecessary suggestion for documentation. (Code should be self commenting and only require comments when functionality is unexpected or surprising.)

Loading