diff --git a/moku/admin.py b/moku/admin.py index 6df6978..0d7e33e 100644 --- a/moku/admin.py +++ b/moku/admin.py @@ -11,6 +11,8 @@ for model_name in models.__all__: @admin.register(models.User) class UserAdmin(BaseUserAdmin): + """Admin class override for the User model.""" + fieldsets = ( (None, {"fields": ("username", "email", "password")}), ("Profile", {"fields": ("pronouns", "location", "bio")}), diff --git a/moku/config/apps.py b/moku/config/apps.py index 4c21ca9..d210ce4 100644 --- a/moku/config/apps.py +++ b/moku/config/apps.py @@ -2,6 +2,8 @@ from django.apps import AppConfig class MokuConfig(AppConfig): + """Django application configuration for moku.blog.""" + name = "moku" label = "moku" verbose_name = "moku.blog" diff --git a/moku/config/asgi.py b/moku/config/asgi.py index 116c789..75677a7 100644 --- a/moku/config/asgi.py +++ b/moku/config/asgi.py @@ -1,12 +1,3 @@ -""" -ASGI config for moku project. - -It exposes the ASGI callable as a module-level variable named ``application``. - -For more information on this file, see -https://docs.djangoproject.com/en/5.0/howto/deployment/asgi/ -""" - import os from django.core.asgi import get_asgi_application @@ -14,3 +5,7 @@ from django.core.asgi import get_asgi_application os.environ.setdefault("DJANGO_SETTINGS_MODULE", "moku.config.settings") application = get_asgi_application() +""" +ASGI application for moku.blog. +More information: https://docs.djangoproject.com/en/5.0/howto/deployment/asgi/ +""" diff --git a/moku/config/urls.py b/moku/config/urls.py index c14b236..34857a5 100644 --- a/moku/config/urls.py +++ b/moku/config/urls.py @@ -1,20 +1,3 @@ -""" -URL configuration for moku project. - -The `urlpatterns` list routes URLs to views. For more information please see: - https://docs.djangoproject.com/en/5.0/topics/http/urls/ -Examples: -Function views - 1. Add an import: from my_app import views - 2. Add a URL to urlpatterns: path('', views.home, name='home') -Class-based views - 1. Add an import: from other_app.views import Home - 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') -Including another URLconf - 1. Import the include() function: from django.urls import include, path - 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) -""" - from django.conf import settings from django.conf.urls.static import static from django.contrib import admin @@ -57,6 +40,10 @@ urlpatterns = [ name="step.delete", ), ] +""" +URL patterns, defining the routes available in moku.blog. +More information: https://docs.djangoproject.com/en/5.0/topics/http/urls/ +""" if settings.DEBUG_TOOLBAR: urlpatterns += [path("__debug__/", include("debug_toolbar.urls"))] diff --git a/moku/config/wsgi.py b/moku/config/wsgi.py index 7887322..5a0f2c1 100644 --- a/moku/config/wsgi.py +++ b/moku/config/wsgi.py @@ -1,12 +1,3 @@ -""" -WSGI config for moku project. - -It exposes the WSGI callable as a module-level variable named ``application``. - -For more information on this file, see -https://docs.djangoproject.com/en/5.0/howto/deployment/wsgi/ -""" - import os from django.core.wsgi import get_wsgi_application @@ -14,3 +5,7 @@ from django.core.wsgi import get_wsgi_application os.environ.setdefault("DJANGO_SETTINGS_MODULE", "moku.config.settings") application = get_wsgi_application() +""" +WSGI application for moku.blog. +More information: https://docs.djangoproject.com/en/5.0/howto/deployment/wsgi/ +""" diff --git a/moku/constants.py b/moku/constants.py index 02414ab..dc1035a 100644 --- a/moku/constants.py +++ b/moku/constants.py @@ -2,6 +2,8 @@ from django.utils.translation import gettext_lazy as _ class Verbs: + """Defines choices of possible verbs in posts.""" + ATE = "ate" DRANK = "drank" MADE = "made" @@ -247,3 +249,4 @@ EMOJI_CATEGORIES = [ ), (_("tools & things"), ("🥄", "🍴", "🍽️", "🥣", "🥡", "🥢", "🧂", "🔪", "🪓")), ] +"""Defines emoji that are available in the emoji picker widget.""" diff --git a/moku/filters.py b/moku/filters.py index fa5c66d..627e378 100644 --- a/moku/filters.py +++ b/moku/filters.py @@ -2,4 +2,5 @@ from emoji import demojize def unemoji(txt: str): + """Turn emoji in the given string into plain text.""" return demojize(txt, delimiters=("", "")) diff --git a/moku/forms/post.py b/moku/forms/post.py index 3efafa2..49ee112 100644 --- a/moku/forms/post.py +++ b/moku/forms/post.py @@ -5,6 +5,8 @@ from moku.models import Post class PostForm(ModelForm): + """Form for creating and updating posts.""" + class Meta: model = Post fields = ("emoji", "verb", "food", "recipe", "image") diff --git a/moku/forms/recipe.py b/moku/forms/recipe.py index f327ab9..191f80e 100644 --- a/moku/forms/recipe.py +++ b/moku/forms/recipe.py @@ -5,6 +5,8 @@ from moku.models.recipe import Recipe, RecipeStep class RecipeForm(ModelForm): + """Form for creating and updating recipes.""" + class Meta: model = Recipe fields = ("title",) @@ -12,6 +14,8 @@ class RecipeForm(ModelForm): class RecipeStepForm(ModelForm): + """Form for creating and updating steps of a recipe.""" + class Meta: model = RecipeStep fields = ("instructions",) diff --git a/moku/forms/user.py b/moku/forms/user.py index 3aeb758..8ac4eda 100644 --- a/moku/forms/user.py +++ b/moku/forms/user.py @@ -7,6 +7,8 @@ from moku.models.user import User, UserSettings class UserForm(UserCreationForm): + """Form for creating a new user account on the site.""" + captcha = ReCaptchaField(required=True) check = forms.BooleanField(required=True) @@ -30,6 +32,8 @@ class UserForm(UserCreationForm): class UserSettingsForm(forms.ModelForm): + """Form for creating or updating user settings.""" + class Meta: model = UserSettings fields = ("language",) @@ -37,6 +41,8 @@ class UserSettingsForm(forms.ModelForm): class ProfileForm(forms.ModelForm): + """Form for updating user profile information.""" + class Meta: model = User fields = ("avatar", "pronouns", "location", "bio") diff --git a/moku/images.py b/moku/images.py index 90047b3..3ef3bbd 100644 --- a/moku/images.py +++ b/moku/images.py @@ -5,6 +5,7 @@ from PIL import Image, ImageOps def _convert_image_to_webp(image_file): + """Private helper function for image conversion.""" image = Image.open(image_file) ImageOps.exif_transpose(image, in_place=True) image.convert("RGB") @@ -15,8 +16,10 @@ def _convert_image_to_webp(image_file): def process_avatar_image(image_file): + """Image conversion function for user avatars.""" return _convert_image_to_webp(image_file) def process_post_image(image_file): + """Image conversion function for post images.""" return _convert_image_to_webp(image_file) diff --git a/moku/middleware.py b/moku/middleware.py index df9b010..b36b5eb 100644 --- a/moku/middleware.py +++ b/moku/middleware.py @@ -2,6 +2,8 @@ from django.utils import translation class MokuLanguageMiddleware: + """Activates the chosen language of an authenticated user if set.""" + def __init__(self, get_response): self.get_response = get_response diff --git a/moku/models/post.py b/moku/models/post.py index 0f78ea0..b0c35c9 100644 --- a/moku/models/post.py +++ b/moku/models/post.py @@ -9,10 +9,13 @@ from moku.validators import validate_emoji def post_image_filename(instance, _): + """Returns the filename that post images should be saved to.""" return f"posts/{instance.created_by.username}__{instance.uuid}.webp" class PostManager(models.Manager): + """Manages post objects more efficiently by pre-fetching recipes and their steps.""" + def get_queryset(self): return ( super() @@ -28,6 +31,8 @@ class PostManager(models.Manager): class Post(models.Model): + """Represents a single post on the site.""" + uuid = ShortUUIDField( verbose_name=_("unique id"), max_length=22, @@ -85,6 +90,10 @@ class Post(models.Model): @property def text(self): + """ + The text of the post, with the post's chosen verb hydrated with food and user + information. + """ return self.get_verb_display() % { "user": ( f'' diff --git a/moku/models/recipe.py b/moku/models/recipe.py index 77c133b..87f36b3 100644 --- a/moku/models/recipe.py +++ b/moku/models/recipe.py @@ -5,6 +5,8 @@ from shortuuid.django_fields import ShortUUIDField class RecipeManager(models.Manager): + """Manages recipe objects more efficiently by pre-fetching steps.""" + def get_queryset(self): return ( super() @@ -16,6 +18,8 @@ class RecipeManager(models.Manager): class Recipe(models.Model): + """Represents a single recipe on the site.""" + uuid = ShortUUIDField( verbose_name=_("unique id"), max_length=22, @@ -47,6 +51,8 @@ class Recipe(models.Model): class RecipeStep(models.Model): + """Represents a single step belonging to a recipe.""" + uuid = ShortUUIDField( verbose_name=_("step id"), max_length=22, diff --git a/moku/models/user.py b/moku/models/user.py index 29a4450..aba4b45 100644 --- a/moku/models/user.py +++ b/moku/models/user.py @@ -8,10 +8,13 @@ from moku.validators import validate_username_length, validate_username_regex def user_avatar_filename(instance, _): + """Returns the filename that user avatar images should be saved to.""" return f"avatars/{instance.username}.webp" class User(AbstractUser): + """Represents a single authenticated user on the site.""" + username = models.CharField( verbose_name=_("username"), max_length=64, @@ -74,10 +77,13 @@ class User(AbstractUser): @property def email_confirmed(self): + """Whether the user has confirmed their email address.""" return self.email_confirmed_at is not None class UserSettings(models.Model): + """Represents settings for a single user.""" + user = models.OneToOneField( "User", related_name="settings", on_delete=models.CASCADE ) diff --git a/moku/validators.py b/moku/validators.py index 9aaa336..7b6a147 100644 --- a/moku/validators.py +++ b/moku/validators.py @@ -7,6 +7,7 @@ from emoji import is_emoji def validate_emoji(value): + """Validates that a given string is a single emoji.""" if not is_emoji(value): raise ValidationError(_("Must be an emoji.")) @@ -16,9 +17,11 @@ validate_username_regex = RegexValidator( _("Username may only contain letters, numbers, hyphens, underscores and dots."), "invalid", ) +"""Validates that a given string is a valid username.""" def validate_username_length(value): + """Validates the length of a given username string according to Django settings.""" if ( len(value) < settings.USERNAME_MIN_LENGTH or len(value) > settings.USERNAME_MAX_LENGTH diff --git a/moku/views/auth.py b/moku/views/auth.py index b6c04e3..e40aca3 100644 --- a/moku/views/auth.py +++ b/moku/views/auth.py @@ -9,6 +9,8 @@ from moku.views.base import View class LoginView(View, BaseLoginView): + """Allows users to log in by username and password.""" + template_name = "moku/login.jinja" def get(self, request, *args, **kwargs): @@ -27,4 +29,6 @@ class LoginView(View, BaseLoginView): class LogoutView(BaseLogoutView): + """Logs the user out and redirect them to the feed.""" + next_page = "feed" diff --git a/moku/views/base.py b/moku/views/base.py index 6a3c3c5..1fc3ed5 100644 --- a/moku/views/base.py +++ b/moku/views/base.py @@ -4,6 +4,8 @@ from django.views import generic class View(generic.TemplateView): + """Defines a common set of data to be passed to the template context.""" + def get_context_data(self, **kwargs): return { **super().get_context_data(**kwargs), @@ -14,4 +16,6 @@ class View(generic.TemplateView): class FormView(View, generic.FormView): + """Functions the same as `moku.views.base.View` but for rendering forms.""" + pass diff --git a/moku/views/post.py b/moku/views/post.py index 022fb8e..fe9e217 100644 --- a/moku/views/post.py +++ b/moku/views/post.py @@ -17,6 +17,8 @@ from moku.views.base import FormView class FeedView(FormView): + """Allows users to see recent posts and create a new post.""" + template_name = "moku/feed.jinja" form_class = PostForm @@ -70,6 +72,8 @@ class FeedView(FormView): class LatestPostJSONView(BaseView): + """Renders the latest post from a specific user as JSON.""" + def get(self, request, *args, **kwargs): post = ( Post.objects.prefetch_related("recipe__steps") diff --git a/moku/views/recipe.py b/moku/views/recipe.py index b056491..d91922d 100644 --- a/moku/views/recipe.py +++ b/moku/views/recipe.py @@ -10,6 +10,8 @@ from moku.views.base import FormView, View class DeleteRecipeView(LoginRequiredMixin, UserPassesTestMixin, View): + """Deletes a recipe from the database if it belongs to the authenticated user.""" + def get(self, request, *args, **kwargs): self.recipe.delete() messages.success(self.request, _("recipe deleted successfully!")) @@ -24,6 +26,10 @@ class DeleteRecipeView(LoginRequiredMixin, UserPassesTestMixin, View): class DeleteStepView(LoginRequiredMixin, UserPassesTestMixin, View): + """ + Deletes a recipe step from the database if it belongs to the authenticated user. + """ + def get(self, request, *args, **kwargs): if self.step.order != (self.step.recipe.steps.count() - 1): messages.error(self.request, _("sorry! you can only delete the last step.")) @@ -45,6 +51,8 @@ class DeleteStepView(LoginRequiredMixin, UserPassesTestMixin, View): class EditStepView(LoginRequiredMixin, UserPassesTestMixin, FormView): + """Allows users to edit steps of a recipe they created.""" + template_name = "moku/recipe/edit_step.jinja" form_class = RecipeStepForm @@ -69,6 +77,8 @@ class EditStepView(LoginRequiredMixin, UserPassesTestMixin, FormView): class IndexRecipeView(LoginRequiredMixin, View): + """Shows a list of recipes created by the authenticated user.""" + template_name = "moku/recipe/index.jinja" def get_context_data(self, **kwargs): @@ -81,6 +91,8 @@ class IndexRecipeView(LoginRequiredMixin, View): class NewRecipeView(LoginRequiredMixin, FormView): + """Allows users to create a new recipe.""" + template_name = "moku/recipe/form.jinja" form_class = RecipeForm @@ -95,6 +107,11 @@ class NewRecipeView(LoginRequiredMixin, FormView): class ShowRecipeView(FormView): + """ + Shows users details about a recipe, and allows steps to be created for it if they + are logged in as the recipe creator. + """ + template_name = "moku/recipe/show.jinja" form_class = RecipeStepForm diff --git a/moku/views/static.py b/moku/views/static.py index 49d9da7..b1993cd 100644 --- a/moku/views/static.py +++ b/moku/views/static.py @@ -2,12 +2,18 @@ from moku.views.base import View class ChangelogView(View): + """Displays the static changelog page.""" + template_name = "moku/changelog.jinja" class PrivacyView(View): + """Displays the static privacy policy page.""" + template_name = "moku/privacy.jinja" class TermsView(View): + """Displays the static terms of use page.""" + template_name = "moku/terms.jinja" diff --git a/moku/views/user.py b/moku/views/user.py index 6e215ce..89f7843 100644 --- a/moku/views/user.py +++ b/moku/views/user.py @@ -11,6 +11,8 @@ from moku.views.base import FormView, View class EditProfileView(LoginRequiredMixin, FormView): + """Allows a user to edit information within their user profile.""" + template_name = "moku/profile/edit.jinja" form_class = ProfileForm @@ -26,6 +28,8 @@ class EditProfileView(LoginRequiredMixin, FormView): class EditSettingsView(LoginRequiredMixin, FormView): + """Allows a user to edit information within their user settings.""" + template_name = "moku/settings.jinja" form_class = UserSettingsForm @@ -50,6 +54,8 @@ class EditSettingsView(LoginRequiredMixin, FormView): class ProfileView(View): + """Shows a user's profile along with a list of their recent posts.""" + template_name = "moku/profile/show.jinja" def get_context_data(self, **kwargs): @@ -62,6 +68,8 @@ class ProfileView(View): class SignupView(FormView): + """Allows non-authenticated users to create an account on the site.""" + template_name = "moku/signup.jinja" form_class = UserForm