[RFC PATCH 16/19] templates: Convert project view
Stephen Finucane
stephen at that.guru
Thu Aug 12 07:37:02 AEST 2021
Signed-off-by: Stephen Finucane <stephen at that.guru>
---
patchwork/forms.py | 83 +++++
patchwork/templates/patchwork/project.html | 407 +++++++++++++++++----
patchwork/views/project.py | 81 +++-
3 files changed, 500 insertions(+), 71 deletions(-)
diff --git patchwork/forms.py patchwork/forms.py
index 5f8dff96..a975db18 100644
--- patchwork/forms.py
+++ patchwork/forms.py
@@ -244,6 +244,89 @@ class UserProfileForm(forms.ModelForm):
labels = {'show_ids': 'Show Patch IDs:'}
+class AddProjectMaintainerForm(forms.Form):
+
+ name = 'add-maintainer'
+
+ username = forms.RegexField(
+ regex=r'^\w+$', max_length=30, label='Username'
+ )
+
+ def __init__(self, project, *args, **kwargs):
+ self.project = project
+ super().__init__(*args, **kwargs)
+
+ def clean_username(self):
+ value = self.cleaned_data['username']
+
+ try:
+ user = User.objects.get(username__iexact=value)
+ except User.DoesNotExist:
+ raise forms.ValidationError(
+ 'That username is not valid. Please choose another.'
+ )
+
+ if self.project in user.profile.maintainer_projects.all():
+ raise forms.ValidationError(
+ 'That user is already a maintainer of this project.'
+ )
+
+ return value
+
+
+class RemoveProjectMaintainerForm(forms.Form):
+
+ name = 'remove-maintainer'
+
+ username = forms.RegexField(
+ regex=r'^\w+$', max_length=30, label='Username'
+ )
+
+ def __init__(self, project, *args, **kwargs):
+ self.project = project
+ super().__init__(*args, **kwargs)
+
+ def clean_username(self):
+ value = self.cleaned_data['username']
+
+ try:
+ user = User.objects.get(username__iexact=value)
+ except User.DoesNotExist:
+ raise forms.ValidationError(
+ 'That username is not valid. Please choose another.'
+ )
+
+ maintainers = User.objects.filter(
+ profile__maintainer_projects=self.project,
+ ).select_related('profile')
+
+ if user not in maintainers:
+ raise forms.ValidationError(
+ 'That user is not a maintainer of this project.'
+ )
+
+ # TODO(stephenfin): Should we prevent users removing themselves?
+
+ if maintainers.count() <= 1:
+ raise forms.ValidationError(
+ 'You cannot remove the only maintainer of the project.'
+ )
+
+ return value
+
+
+class ProjectSettingsForm(forms.ModelForm):
+
+ name = 'project-settings'
+
+ class Meta:
+ model = models.Project
+ fields = [
+ 'name', 'web_url', 'scm_url', 'webscm_url', 'list_archive_url',
+ 'list_archive_url_format', 'commit_url_format',
+ ]
+
+
def _get_delegate_qs(project, instance=None):
if instance and not project:
project = instance.project
diff --git patchwork/templates/patchwork/project.html patchwork/templates/patchwork/project.html
index cad372f7..1b25bbe6 100644
--- patchwork/templates/patchwork/project.html
+++ patchwork/templates/patchwork/project.html
@@ -1,79 +1,348 @@
-{% extends "base.html" %}
+{% extends "base2.html" %}
{% block title %}{{ project.name }}{% endblock %}
-{% block info_active %}active{% endblock %}
{% block body %}
-<h1>About {{ project.name }}</h1>
-
-<table class="horizontal">
- <tr>
- <th>Name</th>
- <td>{{ project.name }}
- </tr>
- <tr>
- <th>List address</th>
- <td>{{ project.listemail }}</td>
- </tr>
-{% if project.list_archive_url %}
- <tr>
- <th>List archive</th>
- <td><a href="{{ project.list_archive_url }}">{{ project.list_archive_url }}</a></td>
- </tr>
+{% for message in messages %}
+{% if message.tags == 'success' %}
+<div class="notification is-success">
+{% elif message.tags == 'warning' %}
+<div class="notification is-warning">
+{% elif message.tags == 'error' %}
+<div class="notification is-danger">
+{% else %}
+<div class="notification">
{% endif %}
- <tr>
- <th>Maintainer{{ maintainers|length|pluralize }}</th>
- <td>
- {% for maintainer in maintainers %}
- {{ maintainer.profile.name }}
- <<a href="mailto:{{ maintainer.email }}">{{ maintainer.email }}</a>>
- <br />
- {% endfor %}
- </td>
- </tr>
- <tr>
- <th>Patches </th>
- <td>{{ n_patches }} (+ {{ n_archived_patches }} archived)</td>
- </tr>
-{% if project.web_url %}
- <tr>
- <th>Website</th>
- <td><a href="{{ project.web_url }}">{{ project.web_url }}</a></td>
- </tr>
+ {{ message }}
+ <button class="delete" onclick="dismiss(this);"></button>
+</div>
+{% endfor %}
+
+<div class="container" style="margin-top: 1rem;">
+ <section class="block">
+ <h1 class="title">
+ About {{ project.name }}
+ </h1>
+ <p class="subtitle">
+ {{ project.listemail }}
+ </p>
+ </section>
+
+ <div class="block"></div>
+
+ <section class="block">
+ <h2 id="overview" class="title is-4">
+ <a href="#overview" title="Permalink to this section">#</a>
+ Overview
+ </h2>
+
+ <div class="tile is-ancestor has-text-centered">
+ <div class="tile is-parent">
+ <div class="tile is-child is-primary box">
+ <p class="title">
+ {{ n_patches }}
+ </p>
+ <p class="subtitle">Patches</p>
+ </div>
+ </div>
+
+ <div class="tile is-parent">
+ <a href="{{ project.web_url }}" class="tile is-child box">
+ <p class="title">
+ <span class="icon">
+ <i class="fas fa-home"></i>
+ </span>
+ </p>
+ <p class="subtitle">Website</p>
+ </a>
+ </div>
+
+ <div class="tile is-parent">
+ <a href="{{ project.list_archive_url }}" class="tile is-child box">
+ <p class="title">
+ <span class="icon">
+ <i class="fas fa-envelope"></i>
+ </span>
+ </p>
+ <p class="subtitle">List Archives</p>
+ </a>
+ </div>
+
+ <div class="tile is-parent">
+ <a href="{{ project.webscm_url }}" class="tile is-child box">
+ <p class="title">
+ <span class="icon">
+ <i class="fas fa-code"></i>
+ </span>
+ </p>
+ <p class="subtitle">Source Code</p>
+ </a>
+ </div>
+ </div>
+ </section>
+
+ <section class="block">
+ <h2 id="maintainers" class="title is-4">
+ <a href="#maintainers" title="Permalink to this section">#</a>
+ Maintainers
+ </h2>
+
+{% for maintainer in maintainers %}
+ <div class="card">
+ <div class="card-content">
+ <div class="columns">
+ <div class="column">
+ <span class="has-text-weight-bold">{{ maintainer.username }}</span>
+{% if maintainer.first_name and maintainer.last_name %}
+ ({{ maintainer.first_name }} {{ maintainer.last_name }})
+{% elif maintainer.first_name %}
+ ({{ maintainer.first_name }})
+{% elif maintainer.last_name %}
+ ({{ maintainer.last_name }})
+{% endif %}
+ </div>
+ <div class="column">
+ <span>{{ maintainer.email }}</span>
+ </div>
+{% if maintainers|length > 1 and maintainer.username != user.username %}
+ <div class="column is-narrow">
+ <form method="post">
+ {% csrf_token %}
+ <input type="hidden" name="form_name" value="remove-maintainer">
+ <input type="hidden" name="email" value="{{ maintainer.username }}">
+ <span class="icon">
+ <i class="fas fa-trash"></i>
+ </span>
+ </form>
+ </div>
{% endif %}
-{% if project.webscm_url %}
- <tr>
- <th>Source Code Web Interface</th>
- <td><a href="{{ project.webscm_url }}">{{ project.webscm_url }}</a></td>
- </tr>
+ </div>
+ </div>
+ </div>
+{% empty %}
+ <p>This project has no maintainers.</p>
+{% endfor %}
+{% if project in user.profile.maintainer_projects.all %}
+ <div class="block"></div>
+ <div class="block">
+ <form class="block" method="post">
+ {% csrf_token %}
+ <input type="hidden" name="form_name" value="add-maintainer">
+ <label for="id_username" class="label">
+ Add maintainer
+ </label>
+ <div class="field is-grouped">
+ <div class="control">
+ <input id="id_username" type="text" name="username" placeholder="e.g. bobsmith" class="input" value="{{ add_maintainer_form.username.value|default:'' }}" required>
+{% for error in add_maintainer_form.username.errors %}
+ <p class="help is-danger">{{ error }}</p>
+{% endfor %}
+ </div>
+ <div class="control">
+ <button class="button is-info">
+ Add maintainer
+ </button>
+ </div>
+ </div>
+ </form>
+ </div>
{% endif %}
-{% if project.scm_url %}
- <tr>
- <th>Source Code Manager URL</th>
- <td><a href="{{ project.scm_url }}">{{ project.scm_url }}</a></td>
- </tr>
+ </section>
+
+{% if pwclientrc %}
+ <section class="block">
+ <h2 id="pwclient" class="title is-4">
+ <a href="#pwclient" title="Permalink to this section">#</a>
+ <code>pwclientrc</code> configuration
+ </h2>
+
+ <div class="content">
+ <p>
+ <code>pwclient</code> is the command-line client for Patchwork. Currently,
+ it provides access to some read-only features of Patchwork, such as
+ downloading and applying patches.
+ </p>
+
+ <p>To use pwclient, you will need:</p>
+
+ <ul>
+ <li>
+ The <a href="https://github.com/getpatchwork/pwclient">pwclient</a>
+ program.
+ </li>
+ <li>
+ (Optional) A <code>.pwclientrc</code> file for this project,
+ which should be stored in your home directory.
+ </li>
+ </ul>
+
+ <p>A sample <code>pwclientrc</code> config file is provided below.</p>
+
+ <pre><code>{{ pwclientrc }}</code></pre>
+ </div>
+ </section>
{% endif %}
-</table>
-
-{% if enable_xmlrpc %}
-<h2>pwclient</h2>
-
-<p>
- <code>pwclient</code> is the command-line client for Patchwork. Currently,
- it provides access to some read-only features of Patchwork, such as
- downloading and applying patches.
-</p>
-
-<p>To use pwclient, you will need:</p>
-<ul>
- <li>
- The <a href="https://github.com/getpatchwork/pwclient">pwclient</a>
- program.
- </li>
- <li>
- (Optional) A <code><a href="{% url 'pwclientrc' project.linkname %}">.pwclientrc</a></code>
- file for this project, which should be stored in your home directory.
- </li>
-</ul>
+
+{% if project in user.profile.maintainer_projects.all %}
+ <section class="block">
+ <h2 id="settings" class="title is-4">
+ <a href="#settings" title="Permalink to this section">#</a>
+ Settings
+ </h2>
+
+{% if project_settings_form.non_field_errors %}
+ <div class="notification is-danger is-light">
+ <button class="delete" onclick="dismiss(this);"></button>
+ {{ project_settings_form.non_field_errors }}
+ </div>
{% endif %}
+ <form class="block" method="post">
+ {% csrf_token %}
+ <input type="hidden" name="form_name" value="project-settings">
+ <div class="field">
+ <label for="linkname" class="label">
+ Linkname
+ </label>
+ <div class="control">
+ <input id="id_linkname" type="text" name="linkname" class="input" value="{{ project.linkname }}" disabled>
+ </div>
+ <p class="help">
+ Patchwork project ID.
+ </p>
+ </div>
+ <div class="field">
+ <label for="listemail" class="label">
+ List email
+ </label>
+ <div class="control">
+ <input id="id_listemail" type="text" name="listemail" class="input" value="{{ project.listemail }}" disabled>
+ </div>
+ <p class="help">
+ Mailing list email.
+ </p>
+ </div>
+ <div class="field">
+ <label for="listid" class="label">
+ List ID
+ </label>
+ <div class="control">
+ <input id="id_listid" type="text" name="listid" class="input" value="{{ project.listid }}" disabled>
+ </div>
+ <p class="help">
+ Mailing list ID.
+ </p>
+ </div>
+ <div class="field">
+ <label for="name" class="label">
+ Name
+ </label>
+ <div class="control">
+ <input id="id_name" type="text" name="name" class="input" value="{{ project.name }}" required>
+ </div>
+ <p class="help">
+ Name of project.
+ </p>
+{% for error in project_settings_form.name.errors %}
+ <p class="help is-danger">{{ error }}</p>
+{% endfor %}
+ </div>
+ <div class="field">
+ <label for="web_url" class="label">
+ Website
+ </label>
+ <div class="control">
+ <input id="id_web_url" type="text" name="web_url" class="input" value="{{ project.web_url }}">
+ </div>
+ <p class="help">
+ Homepage of project.
+ </p>
+{% for error in project_settings_form.web_url.errors %}
+ <p class="help is-danger">{{ error }}</p>
+{% endfor %}
+ </div>
+ <div class="field">
+ <label for="scm_url" class="label">
+ Source code manager URL
+ </label>
+ <div class="control">
+ <input id="id_scm_url" type="text" name="scm_url" class="input" value="{{ project.scm_url }}">
+ </div>
+ <p class="help">
+ Checkout or clone URL for project source code.
+ </p>
+{% for error in project_settings_form.scm_url.errors %}
+ <p class="help is-danger">{{ error }}</p>
+{% endfor %}
+ </div>
+ <div class="field">
+ <label for="webscm_url" class="label">
+ Source code website
+ </label>
+ <div class="control">
+ <input id="id_webscm_url" type="text" name="webscm_url" class="input" value="{{ project.webscm_url }}">
+ </div>
+ <p class="help">
+ Website for browing project source code.
+ </p>
+{% for error in project_settings_form.webscm_url.errors %}
+ <p class="help is-danger">{{ error }}</p>
+{% endfor %}
+ </div>
+ <div class="field">
+ <label for="list_archive_url" class="label">
+ List archives
+ </label>
+ <div class="control">
+ <input id="id_list_archive_url" type="text" name="list_archive_url" class="input" value="{{ project.list_archive_url }}">
+ </div>
+ <p class="help">
+ URL for accessing list archives.
+ </p>
+{% for error in project_settings_form.list_archive_url.errors %}
+ <p class="help is-danger">{{ error }}</p>
+{% endfor %}
+ </div>
+ <div class="field">
+ <label for="list_archive_url_format" class="label">
+ List archive URL format
+ </label>
+ <div class="control">
+ <input id="id_list_archive_url_format" type="text" name="list_archive_url_format" class="input" value="{{ project.list_archive_url_format }}">
+ </div>
+ <p class="help">
+ URL format for the list archive's Message-ID redirector.
+ <code>{}</code> will be replaced by the Message-ID.
+ </p>
+{% for error in project_settings_form.list_archive_url_format.errors %}
+ <p class="help is-danger">{{ error }}</p>
+{% endfor %}
+ </div>
+ <div class="field">
+ <label for="commit_url_format" class="label">
+ Commit URL format
+ </label>
+ <div class="control">
+ <input id="id_commit_url_format" type="text" name="commit_url_format" class="input" value="{{ project.commit_url_format }}">
+ </div>
+ <p class="help">
+ URL format for a particular commit.
+ <code>{}</code> will be replaced by the commit SHA.
+ </p>
+{% for error in project_settings_form.commit_url_format.errors %}
+ <p class="help is-danger">{{ error }}</p>
+{% endfor %}
+ </div>
+ <div class="control">
+ <button class="button is-primary is-disabled">Update settings</button>
+ </div>
+ </form>
+ </section>
+{% endif %}
+</div>
+
+<script>
+function dismiss(el){
+ el.parentNode.style.display = 'none';
+};
+</script>
{% endblock %}
diff --git patchwork/views/project.py patchwork/views/project.py
index a993618a..788662fb 100644
--- patchwork/views/project.py
+++ patchwork/views/project.py
@@ -5,11 +5,15 @@
from django.conf import settings
from django.contrib.auth.models import User
+from django.contrib import messages
+from django.contrib.sites.shortcuts import get_current_site
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.shortcuts import render
+from django.template.loader import render_to_string
from django.urls import reverse
+from patchwork import forms
from patchwork.models import Patch
from patchwork.models import Project
@@ -32,13 +36,86 @@ def project_detail(request, project_id):
project = get_object_or_404(Project, linkname=project_id)
patches = Patch.objects.filter(project=project)
+ add_maintainer_form = forms.AddProjectMaintainerForm(project),
+ remove_maintainer_form = forms.RemoveProjectMaintainerForm(project)
+ project_settings_form = forms.ProjectSettingsForm(instance=project)
+
+ if request.method == 'POST':
+ form_name = request.POST.get('form_name', '')
+ if form_name == forms.AddProjectMaintainerForm.name:
+ add_maintainer_form = forms.AddProjectMaintainerForm(
+ project, data=request.POST)
+ if add_maintainer_form.is_valid():
+ messages.success(
+ request,
+ 'Added new maintainer.',
+ )
+ return HttpResponseRedirect(
+ reverse(
+ 'project-detail',
+ kwargs={'project_id': project.linkname},
+ ),
+ )
+ messages.error(request, 'Error adding project maintainer.')
+ elif form_name == forms.RemoveProjectMaintainerForm.name:
+ remove_maintainer_form = forms.RemoveProjectMaintainerForm(
+ project, data=request.POST)
+ if remove_maintainer_form.is_valid():
+ messages.success(
+ request,
+ 'Removed maintainer.',
+ )
+ return HttpResponseRedirect(
+ reverse(
+ 'project-detail',
+ kwargs={'project_id': project.linkname},
+ ),
+ )
+ messages.error(request, 'Error removing project maintainer.')
+ elif form_name == forms.ProjectSettingsForm.name:
+ project_settings_form = forms.ProjectSettingsForm(
+ instance=project, data=request.POST)
+ if project_settings_form.is_valid():
+ project_settings_form.save()
+ messages.success(
+ request,
+ 'Updated project settings.',
+ )
+ return HttpResponseRedirect(
+ reverse(
+ 'project-detail',
+ kwargs={'project_id': project.linkname},
+ ),
+ )
+ messages.error(request, 'Error updating project settings.')
+ else:
+ messages.error(request, 'Unrecognized request')
+
context = {
'project': project,
'maintainers': User.objects.filter(
profile__maintainer_projects=project
).select_related('profile'),
'n_patches': patches.filter(archived=False).count(),
- 'n_archived_patches': patches.filter(archived=True).count(),
- 'enable_xmlrpc': settings.ENABLE_XMLRPC,
+ 'add_maintainer_form': add_maintainer_form,
+ 'remove_maintainer_form': remove_maintainer_form,
+ 'project_settings_form': project_settings_form,
}
+
+ if settings.ENABLE_XMLRPC:
+ if settings.FORCE_HTTPS_LINKS or request.is_secure():
+ scheme = 'https'
+ else:
+ scheme = 'http'
+
+ context['pwclientrc'] = render_to_string(
+ 'patchwork/pwclientrc',
+ {
+ 'project': project,
+ 'scheme': scheme,
+ 'user': request.user,
+ 'site': get_current_site(request),
+ },
+ ).strip()
+
return render(request, 'patchwork/project.html', context)
--
2.31.1
More information about the Patchwork
mailing list