From patchwork Wed Sep 1 16:57:53 2021 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Stephen Finucane X-Patchwork-Id: 1523346 Return-Path: X-Original-To: incoming@patchwork.ozlabs.org Delivered-To: patchwork-incoming@bilbo.ozlabs.org Authentication-Results: ozlabs.org; dkim=fail reason="key not found in DNS" header.d=that.guru header.i=@that.guru header.a=rsa-sha256 header.s=x header.b=RR4REWea; dkim-atps=neutral Authentication-Results: ozlabs.org; spf=pass (sender SPF authorized) smtp.mailfrom=lists.ozlabs.org (client-ip=112.213.38.117; helo=lists.ozlabs.org; envelope-from=patchwork-bounces+incoming=patchwork.ozlabs.org@lists.ozlabs.org; receiver=) Received: from lists.ozlabs.org (lists.ozlabs.org [112.213.38.117]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (4096 bits)) (No client certificate requested) by ozlabs.org (Postfix) with ESMTPS id 4H09j209xVz9sW8 for ; Thu, 2 Sep 2021 03:17:10 +1000 (AEST) Received: from boromir.ozlabs.org (localhost [IPv6:::1]) by lists.ozlabs.org (Postfix) with ESMTP id 4H09j15x3Jz2yN4 for ; Thu, 2 Sep 2021 03:17:09 +1000 (AEST) Authentication-Results: lists.ozlabs.org; dkim=fail reason="key not found in DNS" header.d=that.guru header.i=@that.guru header.a=rsa-sha256 header.s=x header.b=RR4REWea; dkim-atps=neutral X-Original-To: patchwork@lists.ozlabs.org Delivered-To: patchwork@lists.ozlabs.org Authentication-Results: lists.ozlabs.org; spf=none (no SPF record) smtp.mailfrom=that.guru (client-ip=136.175.108.50; helo=mail-108-mta50.mxroute.com; envelope-from=stephen@that.guru; receiver=) Authentication-Results: lists.ozlabs.org; dkim=fail reason="key not found in DNS" header.d=that.guru header.i=@that.guru header.a=rsa-sha256 header.s=x header.b=RR4REWea; dkim-atps=neutral Received: from mail-108-mta50.mxroute.com (mail-108-mta50.mxroute.com [136.175.108.50]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by lists.ozlabs.org (Postfix) with ESMTPS id 4H09hg50shz301g for ; Thu, 2 Sep 2021 03:16:51 +1000 (AEST) Received: from filter004.mxroute.com ([149.28.56.236] filter004.mxroute.com) (Authenticated sender: mN4UYu2MZsgR) by mail-108-mta50.mxroute.com (ZoneMTA) with ESMTPSA id 17ba25cead300074ba.001 for (version=TLSv1/SSLv3 cipher=ECDHE-RSA-AES128-GCM-SHA256); Wed, 01 Sep 2021 17:16:43 +0000 X-Zone-Loop: a103851ae152a4507f0c86fa042ca4068e102c13c754 X-Originating-IP: [149.28.56.236] DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=that.guru; s=x; h=Content-Transfer-Encoding:MIME-Version:References:In-Reply-To: Message-Id:Date:Subject:Cc:To:From:Sender:Reply-To:Content-Type:Content-ID: Content-Description:Resent-Date:Resent-From:Resent-Sender:Resent-To:Resent-Cc :Resent-Message-ID:List-Id:List-Help:List-Unsubscribe:List-Subscribe: List-Post:List-Owner:List-Archive; bh=XvSUNYliikOU+/WAJFFcEscdSbGuOYxe8OAUUT8bkxc=; b=RR4REWeaotHX9M8WssNitUiJWu K1NMT9Dt0Fxgd+SIeAIbxqF1tn15stxxEMUwMLGlA65pG4k5lFBP6Ab7tsjqV79ZyB49ahBiFNXf3 ngjdwk38ioVYj87TtTwUYHuTmxmJkuS1CnPhzwDk6DjvrevRKRPhFuqi8uC7Kd2zAdirAHlmxEWtR qt3HQbxT/tjTXjxvYIxmS8iaQXby3OJnbnZVA2OLWp4tAFYGNXQPQwKq/bX88E/B6nO7jY9YoGVhk adk50dyrXoJ4r0TjtoaeloxQdAfUvvnmRv2D1V83Usgl4TcY/vojAx03KbFN4qb3LQPJVL4EfS8BW rjm9S/ew==; From: Stephen Finucane To: patchwork@lists.ozlabs.org Subject: [RFC PATCH v2 16/19] templates: Convert project view Date: Wed, 1 Sep 2021 17:57:53 +0100 Message-Id: <20210901165756.181192-17-stephen@that.guru> X-Mailer: git-send-email 2.31.1 In-Reply-To: <20210901165756.181192-1-stephen@that.guru> References: <20210901165756.181192-1-stephen@that.guru> MIME-Version: 1.0 X-AuthUser: stephen@that.guru X-BeenThere: patchwork@lists.ozlabs.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: Patchwork development List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: patchwork-bounces+incoming=patchwork.ozlabs.org@lists.ozlabs.org Sender: "Patchwork" Signed-off-by: Stephen Finucane --- 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 %} -

About {{ project.name }}

- - - - - - - - - -{% if project.list_archive_url %} - - - - +{% for message in messages %} +{% if message.tags == 'success' %} +
+{% elif message.tags == 'warning' %} +
+{% elif message.tags == 'error' %} +
+{% else %} +
{% endif %} -
- - - - - - - -{% if project.web_url %} - - - - + {{ message }} + + +{% endfor %} + +
+
+

+ About {{ project.name }} +

+

+ {{ project.listemail }} +

+
+ +
+ +
+

+ # + Overview +

+ +
+
+
+

+ {{ n_patches }} +

+

Patches

+
+
+ + + + + + +
+
+ +
+

+ # + Maintainers +

+ +{% for maintainer in maintainers %} +
+
+
+
+ {{ maintainer.username }} +{% 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 %} +
+
+ {{ maintainer.email }} +
+{% if maintainers|length > 1 and maintainer.username != user.username %} +
+
+ {% csrf_token %} + + + + + + +
{% endif %} -{% if project.webscm_url %} -
- - - + + + +{% empty %} +

This project has no maintainers.

+{% endfor %} +{% if project in user.profile.maintainer_projects.all %} +
+
+
+ {% csrf_token %} + + +
+
+ +{% for error in add_maintainer_form.username.errors %} +

{{ error }}

+{% endfor %} +
+
+ +
+
+ +
{% endif %} -{% if project.scm_url %} - - - - + + +{% if pwclientrc %} +
+

+ # + pwclientrc configuration +

+ +
+

+ pwclient is the command-line client for Patchwork. Currently, + it provides access to some read-only features of Patchwork, such as + downloading and applying patches. +

+ +

To use pwclient, you will need:

+ +
    +
  • + The pwclient + program. +
  • +
  • + (Optional) A .pwclientrc file for this project, + which should be stored in your home directory. +
  • +
+ +

A sample pwclientrc config file is provided below.

+ +
{{ pwclientrc }}
+
+
{% endif %} -
Name{{ project.name }} -
List address{{ project.listemail }}
List archive{{ project.list_archive_url }}
Maintainer{{ maintainers|length|pluralize }} - {% for maintainer in maintainers %} - {{ maintainer.profile.name }} - <{{ maintainer.email }}> -
- {% endfor %} -
Patches {{ n_patches }} (+ {{ n_archived_patches }} archived)
Website{{ project.web_url }}
Source Code Web Interface{{ project.webscm_url }}
Source Code Manager URL{{ project.scm_url }}
- -{% if enable_xmlrpc %} -

pwclient

- -

- pwclient is the command-line client for Patchwork. Currently, - it provides access to some read-only features of Patchwork, such as - downloading and applying patches. -

- -

To use pwclient, you will need:

-
    -
  • - The pwclient - program. -
  • -
  • - (Optional) A .pwclientrc - file for this project, which should be stored in your home directory. -
  • -
+ +{% if project in user.profile.maintainer_projects.all %} +
+

+ # + Settings +

+ +{% if project_settings_form.non_field_errors %} +
+ + {{ project_settings_form.non_field_errors }} +
{% endif %} +
+ {% csrf_token %} + +
+ +
+ +
+

+ Patchwork project ID. +

+
+
+ +
+ +
+

+ Mailing list email. +

+
+
+ +
+ +
+

+ Mailing list ID. +

+
+
+ +
+ +
+

+ Name of project. +

+{% for error in project_settings_form.name.errors %} +

{{ error }}

+{% endfor %} +
+
+ +
+ +
+

+ Homepage of project. +

+{% for error in project_settings_form.web_url.errors %} +

{{ error }}

+{% endfor %} +
+
+ +
+ +
+

+ Checkout or clone URL for project source code. +

+{% for error in project_settings_form.scm_url.errors %} +

{{ error }}

+{% endfor %} +
+
+ +
+ +
+

+ Website for browing project source code. +

+{% for error in project_settings_form.webscm_url.errors %} +

{{ error }}

+{% endfor %} +
+
+ +
+ +
+

+ URL for accessing list archives. +

+{% for error in project_settings_form.list_archive_url.errors %} +

{{ error }}

+{% endfor %} +
+
+ +
+ +
+

+ URL format for the list archive's Message-ID redirector. + {} will be replaced by the Message-ID. +

+{% for error in project_settings_form.list_archive_url_format.errors %} +

{{ error }}

+{% endfor %} +
+
+ +
+ +
+

+ URL format for a particular commit. + {} will be replaced by the commit SHA. +

+{% for error in project_settings_form.commit_url_format.errors %} +

{{ error }}

+{% endfor %} +
+
+ +
+
+
+{% endif %} + + + {% 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)