Source code for sphinx_hosting.forms
from typing import Final, cast
from crispy_forms.bootstrap import FieldWithButtons
from crispy_forms.helper import FormHelper
from crispy_forms.layout import HTML, ButtonHolder, Field, Fieldset, Layout, Submit
from django import forms
from django.db.models import Model
from django.forms import Widget
from django.urls import reverse, reverse_lazy
from haystack.forms import SearchForm
from .logging import logger
from .models import Project, ProjectRelatedLink, Version
from .search_indexes import SphinxPageIndex
[docs]class GlobalSearchForm(SearchForm):
"""
The search form at the top of the sidebar, underneath the logo. It is a
subclass of :py:class:`haystack.forms.SearchForm`, and does a search of our
haystack backend.
"""
[docs] def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_class = "form-horizontal px-3"
self.helper.form_method = "get"
self.helper.form_show_labels = False
# This is a reverse_lazy on purpose because the form gets instantiated as
# the urlpatterns are being made
self.helper.form_action = reverse_lazy("sphinx_hosting:search")
self.helper.layout = Layout(
FieldWithButtons(
Field("q", css_class="text-dark", placeholder="Search"),
HTML(
'<button type="submit" class="btn btn-primary"><span class="bi bi-search"></span></button>' # noqa: E501
),
)
)
[docs]class ProjectCreateForm(forms.ModelForm):
"""
The form we use to create a new :py:class:`sphinx_hosting.models.Project`.
The difference between this and
:py:class:`sphinx_hosting.forms.ProjectUpdateForm` is that the user can set
:py:attr:`sphinx_hosting.models.Project.machine_name` here, but can't in
:py:class:`sphinx_hosting.forms.ProjectUpdateForm`. ``machine_name`` should
not change after the project is created.
"""
[docs] def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_class = "form-horizontal"
self.helper.label_class = "col-lg-3"
self.helper.field_class = "col"
self.helper.form_method = "post"
self.helper.form_action = reverse("sphinx_hosting:project--create")
self.helper.layout = Layout(
Fieldset(
"",
Field("title"),
Field("machine_name"),
Field("description"),
),
ButtonHolder(
Submit("submit", "Save", css_class="btn btn-primary"),
css_class="d-flex flex-row justify-content-end button-holder",
),
)
class Meta:
model: type[Model] = Project
exclude: Final[tuple[str, ...]] = (
# These are relational fields that will be handled separately
"versions",
"classifiers",
"permission_groups",
# These are maintained automatically
"created",
"modified",
)
widgets: Final[dict[str, Widget]] = {
"description": forms.Textarea(attrs={"cols": 50, "rows": 3}),
}
[docs]class ProjectUpdateForm(forms.ModelForm):
"""
The form we use to update an existing
:py:class:`sphinx_hosting.models.Project`. The difference between this and
:py:class:`.ProjectCreateForm` is that the user cannot change
:py:attr:`sphinx_hosting.models.Project.machine_name` here, but can in
:py:class:`ProjectCreateForm`. ``machine_name`` should not change after the
project is created.
"""
[docs] def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_class = "form-horizontal"
self.helper.label_class = "col-lg-3"
self.helper.field_class = "col"
self.helper.form_method = "post"
self.helper.form_action = reverse(
"sphinx_hosting:project--update", args=[self.instance.machine_name]
)
self.helper.layout = Layout(
Fieldset(
"",
Field("title"),
Field("description"),
),
ButtonHolder(
Submit("submit", "Save", css_class="btn btn-primary"),
css_class="d-flex flex-row justify-content-end button-holder",
),
)
class Meta:
model: type[Model] = Project
fields: Final[tuple[str, ...]] = (
"title",
"description",
)
widgets: Final[dict[str, Widget]] = {
"description": forms.Textarea(attrs={"cols": 50, "rows": 3}),
}
[docs]class ProjectReadonlyUpdateForm(forms.ModelForm):
"""
The form we use to on the :py:class:`sphinx_hosting.views.ProjectDetailView`
to show the viewer the project title and description. The difference
between this and :py:class:`ProjectUpdateForm` is that all the fields are
readonly, and there are no submit buttons. We're doing it this way instead
of just rendering a non-form widget so that we can ensure that the page
looks the same.
"""
[docs] def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["title"].widget.attrs["readonly"] = True
self.helper = FormHelper()
self.helper.form_class = "form-horizontal"
self.helper.label_class = "col-lg-3"
self.helper.field_class = "col"
self.helper.form_method = "get"
self.helper.layout = Layout(
Fieldset(
"",
Field("title"),
Field("description"),
)
)
class Meta:
model: type[Model] = Project
fields: Final[tuple[str, ...]] = (
"title",
"description",
)
widgets: Final[dict[str, Widget]] = {
"description": forms.Textarea(
attrs={
"cols": 50,
"rows": 3,
"readonly": "readonly",
}
),
}
class ProjectRelatedLinkBaseForm(forms.ModelForm):
"""
The base form for creating and updating a
:py:class:`sphinx_hosting.models.ProjectRelatedLink`. This form is used by
both :py:class:`sphinx_hosting.forms.ProjectRelatedLinkCreateForm` and
:py:class:`sphinx_hosting.forms.ProjectRelatedLinkUpdateForm`.
Keyword Args:
project_machine_name: the machine name of the project to which this link
is related. This is used to generate the form action URL.
"""
def __init__(self, *args, project_machine_name: str | None = None, **kwargs): # noqa: ARG002
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_class = "form-horizontal"
self.helper.label_class = "col-lg-3"
self.helper.field_class = "col"
self.helper.form_method = "post"
self.helper.layout = Layout(
Fieldset(
"",
Field("title"),
Field("uri"),
),
ButtonHolder(
Submit("submit", "Save", css_class="btn btn-primary"),
css_class="d-flex flex-row justify-content-end button-holder",
),
)
class Meta:
model: type[Model] = ProjectRelatedLink
fields = (
"title",
"uri",
)
class ProjectRelatedLinkCreateForm(ProjectRelatedLinkBaseForm):
"""
The form we use to create a new
:py:class:`sphinx_hosting.models.ProjectRelatedLink`.
Keyword Args:
project: the project to which this link is related. This is used to
generate the form action URL.
"""
def __init__(self, *args, project: Project | None = None, **kwargs):
super().__init__(*args, **kwargs)
self.helper.form_action = reverse(
"sphinx_hosting:projectrelatedlink--create",
args=[cast("Project", project).machine_name],
)
class ProjectRelatedLinkUpdateForm(ProjectRelatedLinkBaseForm):
"""
The form we use to update an existing
:py:class:`sphinx_hosting.models.ProjectRelatedLink`.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper.form_action = self.instance.get_update_url()
[docs]class VersionUploadForm(forms.Form):
"""
The form on :py:class:`sphinx_hosting.views.ProjectDetailView` that
allows the user to upload a new documentation set.
Keyword Args:
project: the project to which this documentation set should be
associated
"""
file: Field = forms.FileField(label="")
[docs] def __init__(self, *args, project: Project = None, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_class = "form"
self.helper.form_method = "post"
if project:
self.helper.form_action = reverse(
"sphinx_hosting:version--upload", kwargs={"slug": project.machine_name}
)
self.helper.layout = Layout(
Fieldset(
"",
Field("file"),
),
ButtonHolder(
Submit("submit", "Import", css_class="btn btn-primary"),
css_class="d-flex flex-row justify-content-end button-holder",
),
)
class VersionMakeLatestForm(forms.Form):
"""
The form we use to force a version to be the latest version of a project.
"""
version: Field = forms.IntegerField(widget=forms.HiddenInput())
def clean_version(self):
"""
Ensure that the version exists.
"""
version_id = self.cleaned_data["version"]
try:
_ = Version.objects.get(pk=version_id)
except Version.DoesNotExist as e:
msg = "The specified version does not exist."
raise forms.ValidationError(msg) from e
return version_id
def save(self):
"""
Make the version the latest version.
"""
version = Version.objects.get(pk=self.cleaned_data["version"])
# Remove the old latest version from the search index
SphinxPageIndex().remove_version(version.project.latest_version)
version.project.latest_version = version
version.project.save()
# Add the new latest version to the search index
SphinxPageIndex().reindex_project(version.project)
logger.info(
"version.make-latest.success project_id=%s project_title=%s version=%s",
version.project.id,
version.project.title,
version.version,
)
return version