[RFC PATCH v2 16/19] templates: Convert project view

Stephen Finucane stephen at that.guru
Thu Sep 2 02:57:53 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