diff --git a/README.rst b/README.rst index 4a970ec5a..94ac32c3b 100644 --- a/README.rst +++ b/README.rst @@ -45,9 +45,9 @@ Requirements - Django (1.8 - 1.11) -- Django REST Framework (3.2 - 3.6) +- Django REST Framework (3.4 - 3.8) -- Django Filters (1.0) +- Django Filters (1.0 - 1.1) Development Installation ------------------------ diff --git a/docs/deployment/configuration.rst b/docs/deployment/configuration.rst index 347485636..e599522a4 100644 --- a/docs/deployment/configuration.rst +++ b/docs/deployment/configuration.rst @@ -88,7 +88,13 @@ Enable the :doc:`REST API <../api/rest>`. The number of items to include in REST API responses by default. This can be overridden by the ``per_page`` parameter for some endpoints. -.. versionadded:: 2.0 +``MAX_REST_RESULTS_PER_PAGE`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The maximum number of items that can be requested in a REST API request using +the ``per_page`` parameter. + +.. versionadded:: 2.2 ``COMPAT_REDIR`` ~~~~~~~~~~~~~~~~ diff --git a/docs/development/installation.rst b/docs/development/installation.rst index 7f1788181..940f73036 100644 --- a/docs/development/installation.rst +++ b/docs/development/installation.rst @@ -3,8 +3,8 @@ Installation This document describes the necessary steps to configure Patchwork in a development environment. If you are interested in deploying Patchwork in a -production environment, refer to `the deployment guide -`__ instead. +production environment, refer to :doc:`the deployment guide +` instead. To begin, you should clone Patchwork: diff --git a/lib/sql/grant-all.postgres.sql b/lib/sql/grant-all.postgres.sql index 27f55c96d..7fb3ac833 100644 --- a/lib/sql/grant-all.postgres.sql +++ b/lib/sql/grant-all.postgres.sql @@ -18,7 +18,7 @@ GRANT SELECT, UPDATE, INSERT, DELETE ON patchwork_check, patchwork_comment, patchwork_coverletter, - patchwork_delegationrule + patchwork_delegationrule, patchwork_emailconfirmation, patchwork_emailoptout, patchwork_patch, @@ -33,7 +33,7 @@ GRANT SELECT, UPDATE, INSERT, DELETE ON patchwork_submission, patchwork_tag, patchwork_userprofile, - patchwork_userprofile_maintainer_projects, + patchwork_userprofile_maintainer_projects TO "www-data"; GRANT SELECT, UPDATE ON auth_group_id_seq, @@ -49,7 +49,7 @@ GRANT SELECT, UPDATE ON patchwork_bundlepatch_id_seq, patchwork_check_id_seq, patchwork_comment_id_seq, - patchwork_delegationrule_id_seq + patchwork_delegationrule_id_seq, patchwork_emailconfirmation_id_seq, patchwork_patch_id_seq, patchwork_patchtag_id_seq, @@ -61,7 +61,7 @@ GRANT SELECT, UPDATE ON patchwork_state_id_seq, patchwork_tag_id_seq, patchwork_userprofile_id_seq, - patchwork_userprofile_maintainer_projects_id_seq, + patchwork_userprofile_maintainer_projects_id_seq TO "www-data"; -- allow the mail user (in this case, 'nobody') to add submissions (patches, @@ -69,32 +69,32 @@ TO "www-data"; GRANT INSERT, SELECT ON patchwork_comment, patchwork_coverletter, - patchwork_event + patchwork_event, patchwork_seriespatch, patchwork_seriesreference, - patchwork_submission, + patchwork_submission TO "nobody"; GRANT INSERT, SELECT, UPDATE, DELETE ON patchwork_patch, patchwork_patchtag, - patchwork_person - patchwork_series, + patchwork_person, + patchwork_series TO "nobody"; GRANT SELECT ON - patchwork_delegationrule + patchwork_delegationrule, patchwork_project, patchwork_state, - patchwork_tag, + patchwork_tag TO "nobody"; GRANT UPDATE, SELECT ON patchwork_comment_id_seq, - patchwork_event_id_seq + patchwork_event_id_seq, patchwork_patch_id_seq, patchwork_patchtag_id_seq, patchwork_person_id_seq, patchwork_series_id_seq, patchwork_seriespatch_id_seq, - patchwork_seriesreference_id_seq, + patchwork_seriesreference_id_seq TO "nobody"; COMMIT; diff --git a/patchwork/__init__.py b/patchwork/__init__.py index c0a4a8615..132c0d123 100644 --- a/patchwork/__init__.py +++ b/patchwork/__init__.py @@ -19,7 +19,7 @@ from patchwork.version import get_latest_version -VERSION = (2, 2, 0, 'alpha', 0) +VERSION = (2, 1, 7, 'alpha', 0) __version__ = get_latest_version(VERSION) diff --git a/patchwork/api/base.py b/patchwork/api/base.py index 8c38d5a1d..bf452f78b 100644 --- a/patchwork/api/base.py +++ b/patchwork/api/base.py @@ -36,7 +36,8 @@ class LinkHeaderPagination(PageNumberPagination): https://tools.ietf.org/html/rfc5988#section-5 https://developer.github.com/guides/traversing-with-pagination """ - page_size = max_page_size = settings.REST_RESULTS_PER_PAGE + page_size = settings.REST_RESULTS_PER_PAGE + max_page_size = settings.MAX_REST_RESULTS_PER_PAGE page_size_query_param = 'per_page' def get_paginated_response(self, data): diff --git a/patchwork/api/check.py b/patchwork/api/check.py index 8753c7de4..1498abbbf 100644 --- a/patchwork/api/check.py +++ b/patchwork/api/check.py @@ -17,12 +17,16 @@ # along with Patchwork; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +from django.http import Http404 +from django.shortcuts import get_object_or_404 +from django.http.request import QueryDict from rest_framework.exceptions import PermissionDenied from rest_framework.generics import ListCreateAPIView from rest_framework.generics import RetrieveAPIView from rest_framework.serializers import CurrentUserDefault from rest_framework.serializers import HiddenField from rest_framework.serializers import HyperlinkedModelSerializer +from rest_framework.serializers import ValidationError from patchwork.api.base import CheckHyperlinkedIdentityField from patchwork.api.base import MultipleFieldLookupMixin @@ -44,11 +48,17 @@ class CheckSerializer(HyperlinkedModelSerializer): url = CheckHyperlinkedIdentityField('api-check-detail') patch = HiddenField(default=CurrentPatchDefault()) - user = UserSerializer(read_only=True, default=CurrentUserDefault()) + user = UserSerializer(default=CurrentUserDefault()) def run_validation(self, data): + if 'state' not in data or data['state'] == '': + raise ValidationError({'state': ["A check must have a state."]}) + for val, label in Check.STATE_CHOICES: - if label == data['state']: + if label != data['state']: + continue + + if isinstance(data, QueryDict): # form-data request # NOTE(stephenfin): 'data' is essentially 'request.POST', which # is immutable by default. However, there's no good reason for # this to be this way [1], so temporarily unset that mutability @@ -59,7 +69,10 @@ def run_validation(self, data): data._mutable = True # noqa data['state'] = val data._mutable = mutable # noqa - break + else: # json request + data['state'] = val + + break return super(CheckSerializer, self).run_validation(data) def to_representation(self, instance): @@ -83,7 +96,12 @@ class CheckMixin(object): filter_class = CheckFilterSet def get_queryset(self): - return Check.objects.prefetch_related('patch', 'user') + patch_id = self.kwargs['patch_id'] + + if not Patch.objects.filter(pk=self.kwargs['patch_id']).exists(): + raise Http404 + + return Check.objects.prefetch_related('user').filter(patch=patch_id) class CheckListCreate(CheckMixin, ListCreateAPIView): @@ -93,7 +111,7 @@ class CheckListCreate(CheckMixin, ListCreateAPIView): ordering = 'id' def create(self, request, patch_id, *args, **kwargs): - p = Patch.objects.get(id=patch_id) + p = get_object_or_404(Patch, id=patch_id) if not p.is_editable(request.user): raise PermissionDenied() request.patch = p diff --git a/patchwork/api/comment.py b/patchwork/api/comment.py index 5a5adb1d0..e0068353e 100644 --- a/patchwork/api/comment.py +++ b/patchwork/api/comment.py @@ -19,6 +19,7 @@ import email.parser +from django.http import Http404 from rest_framework.generics import ListAPIView from rest_framework.serializers import SerializerMethodField @@ -26,6 +27,7 @@ from patchwork.api.base import PatchworkPermission from patchwork.api.embedded import PersonSerializer from patchwork.models import Comment +from patchwork.models import Submission class CommentListSerializer(BaseHyperlinkedModelSerializer): @@ -78,6 +80,9 @@ class CommentList(ListAPIView): lookup_url_kwarg = 'pk' def get_queryset(self): + if not Submission.objects.filter(pk=self.kwargs['pk']).exists(): + raise Http404 + return Comment.objects.filter( submission=self.kwargs['pk'] ).select_related('submitter') diff --git a/patchwork/api/cover.py b/patchwork/api/cover.py index b497fd853..edc002982 100644 --- a/patchwork/api/cover.py +++ b/patchwork/api/cover.py @@ -103,8 +103,10 @@ class CoverLetterList(ListAPIView): ordering = 'id' def get_queryset(self): - return CoverLetter.objects.all().prefetch_related('series')\ - .select_related('project', 'submitter')\ + return CoverLetter.objects.all().prefetch_related('series', + 'project', + 'series__project')\ + .select_related('submitter')\ .defer('content', 'headers') diff --git a/patchwork/api/embedded.py b/patchwork/api/embedded.py index 1d5aba84a..bc4fd0fe5 100644 --- a/patchwork/api/embedded.py +++ b/patchwork/api/embedded.py @@ -23,14 +23,54 @@ nested fields. """ +from collections import OrderedDict + from rest_framework.serializers import CharField from rest_framework.serializers import SerializerMethodField +from rest_framework.serializers import PrimaryKeyRelatedField from patchwork.api.base import BaseHyperlinkedModelSerializer from patchwork.api.base import CheckHyperlinkedIdentityField from patchwork import models +class SerializedRelatedField(PrimaryKeyRelatedField): + """ + A read-write field that expects a primary key for writes and returns a + serialized version of the underlying field on reads. + """ + + def use_pk_only_optimization(self): + # We're using embedded serializers so we want the whole object + return False + + def get_queryset(self): + return self._Serializer.Meta.model.objects.all() + + def get_choices(self, cutoff=None): + # Override this so we don't call 'to_representation', which no longer + # returns a flat value + queryset = self.get_queryset() + if queryset is None: + # Ensure that field.choices returns something sensible + # even when accessed with a read-only field. + return {} + + if cutoff is not None: + queryset = queryset[:cutoff] + + return OrderedDict([ + ( + item.pk, + self.display_value(item) + ) + for item in queryset + ]) + + def to_representation(self, data): + return self._Serializer(context=self.context).to_representation(data) + + class MboxMixin(BaseHyperlinkedModelSerializer): """Embed a link to the mbox URL. @@ -55,132 +95,151 @@ def get_web_url(self, instance): return request.build_absolute_uri(instance.get_absolute_url()) -class BundleSerializer(MboxMixin, WebURLMixin, BaseHyperlinkedModelSerializer): - - class Meta: - model = models.Bundle - fields = ('id', 'url', 'web_url', 'name', 'mbox') - read_only_fields = fields - versioned_field = { - '1.1': ('web_url', ), - } - extra_kwargs = { - 'url': {'view_name': 'api-bundle-detail'}, - } +class BundleSerializer(SerializedRelatedField): + + class _Serializer(MboxMixin, WebURLMixin, BaseHyperlinkedModelSerializer): + + class Meta: + model = models.Bundle + fields = ('id', 'url', 'web_url', 'name', 'mbox') + read_only_fields = fields + versioned_fields = { + '1.1': ('web_url', ), + } + extra_kwargs = { + 'url': {'view_name': 'api-bundle-detail'}, + } + + +class CheckSerializer(SerializedRelatedField): + + class _Serializer(BaseHyperlinkedModelSerializer): + + url = CheckHyperlinkedIdentityField('api-check-detail') + + def to_representation(self, instance): + data = super(CheckSerializer._Serializer, self).to_representation( + instance) + data['state'] = instance.get_state_display() + return data + + class Meta: + model = models.Check + fields = ('id', 'url', 'date', 'state', 'target_url', 'context') + read_only_fields = fields + extra_kwargs = { + 'url': {'view_name': 'api-check-detail'}, + } + + +class CoverLetterSerializer(SerializedRelatedField): + + class _Serializer(MboxMixin, WebURLMixin, BaseHyperlinkedModelSerializer): + + class Meta: + model = models.CoverLetter + fields = ('id', 'url', 'web_url', 'msgid', 'date', 'name', 'mbox') + read_only_fields = fields + versioned_fields = { + '1.1': ('web_url', 'mbox', ), + } + extra_kwargs = { + 'url': {'view_name': 'api-cover-detail'}, + } + + +class PatchSerializer(SerializedRelatedField): + + class _Serializer(MboxMixin, WebURLMixin, BaseHyperlinkedModelSerializer): + + class Meta: + model = models.Patch + fields = ('id', 'url', 'web_url', 'msgid', 'date', 'name', 'mbox') + read_only_fields = fields + versioned_fields = { + '1.1': ('web_url', ), + } + extra_kwargs = { + 'url': {'view_name': 'api-patch-detail'}, + } -class CheckSerializer(BaseHyperlinkedModelSerializer): +class PersonSerializer(SerializedRelatedField): - url = CheckHyperlinkedIdentityField('api-check-detail') + class _Serializer(BaseHyperlinkedModelSerializer): - def to_representation(self, instance): - data = super(CheckSerializer, self).to_representation(instance) - data['state'] = instance.get_state_display() - return data + class Meta: + model = models.Person + fields = ('id', 'url', 'name', 'email') + read_only_fields = fields + extra_kwargs = { + 'url': {'view_name': 'api-person-detail'}, + } - class Meta: - model = models.Check - fields = ('id', 'url', 'date', 'state', 'target_url', 'context') - read_only_fields = fields - extra_kwargs = { - 'url': {'view_name': 'api-check-detail'}, - } +class ProjectSerializer(SerializedRelatedField): + class _Serializer(BaseHyperlinkedModelSerializer): -class CoverLetterSerializer(MboxMixin, WebURLMixin, - BaseHyperlinkedModelSerializer): + link_name = CharField(max_length=255, source='linkname') + list_id = CharField(max_length=255, source='listid') + list_email = CharField(max_length=200, source='listemail') - class Meta: - model = models.CoverLetter - fields = ('id', 'url', 'web_url', 'msgid', 'date', 'name', 'mbox') - read_only_fields = fields - versioned_field = { - '1.1': ('web_url', 'mbox', ), - } - extra_kwargs = { - 'url': {'view_name': 'api-cover-detail'}, - } + class Meta: + model = models.Project + fields = ('id', 'url', 'name', 'link_name', 'list_id', + 'list_email', 'web_url', 'scm_url', 'webscm_url') + read_only_fields = fields + extra_kwargs = { + 'url': {'view_name': 'api-project-detail'}, + } -class PatchSerializer(MboxMixin, WebURLMixin, BaseHyperlinkedModelSerializer): +class SeriesSerializer(SerializedRelatedField): - class Meta: - model = models.Patch - fields = ('id', 'url', 'web_url', 'msgid', 'date', 'name', 'mbox') - read_only_fields = fields - versioned_field = { - '1.1': ('web_url', ), - } - extra_kwargs = { - 'url': {'view_name': 'api-patch-detail'}, - } + class _Serializer(MboxMixin, WebURLMixin, BaseHyperlinkedModelSerializer): + class Meta: + model = models.Series + fields = ('id', 'url', 'web_url', 'date', 'name', 'version', + 'mbox') + read_only_fields = fields + versioned_fields = { + '1.1': ('web_url', ), + } + extra_kwargs = { + 'url': {'view_name': 'api-series-detail'}, + } -class PersonSerializer(BaseHyperlinkedModelSerializer): - class Meta: - model = models.Person - fields = ('id', 'url', 'name', 'email') - read_only_fields = fields - extra_kwargs = { - 'url': {'view_name': 'api-person-detail'}, - } +class UserSerializer(SerializedRelatedField): + class _Serializer(BaseHyperlinkedModelSerializer): -class ProjectSerializer(BaseHyperlinkedModelSerializer): + class Meta: + model = models.User + fields = ('id', 'url', 'username', 'first_name', 'last_name', + 'email') + read_only_fields = fields + extra_kwargs = { + 'url': {'view_name': 'api-user-detail'}, + } - link_name = CharField(max_length=255, source='linkname') - list_id = CharField(max_length=255, source='listid') - list_email = CharField(max_length=200, source='listemail') - class Meta: - model = models.Project - fields = ('id', 'url', 'name', 'link_name', 'list_id', 'list_email', - 'web_url', 'scm_url', 'webscm_url') - read_only_fields = fields - extra_kwargs = { - 'url': {'view_name': 'api-project-detail'}, - } +class UserProfileSerializer(SerializedRelatedField): + class _Serializer(BaseHyperlinkedModelSerializer): -class SeriesSerializer(MboxMixin, WebURLMixin, - BaseHyperlinkedModelSerializer): + username = CharField(source='user.username') + first_name = CharField(source='user.first_name') + last_name = CharField(source='user.last_name') + email = CharField(source='user.email') - class Meta: - model = models.Series - fields = ('id', 'url', 'date', 'name', 'version', 'mbox') - read_only_fields = fields - versioned_field = { - '1.1': ('web_url', ), - } - extra_kwargs = { - 'url': {'view_name': 'api-series-detail'}, - } - - -class UserSerializer(BaseHyperlinkedModelSerializer): - - class Meta: - model = models.User - fields = ('id', 'url', 'username', 'first_name', 'last_name', 'email') - read_only_fields = fields - extra_kwargs = { - 'url': {'view_name': 'api-user-detail'}, - } - - -class UserProfileSerializer(BaseHyperlinkedModelSerializer): - - username = CharField(source='user.username') - first_name = CharField(source='user.first_name') - last_name = CharField(source='user.last_name') - email = CharField(source='user.email') - - class Meta: - model = models.UserProfile - fields = ('id', 'url', 'username', 'first_name', 'last_name', 'email') - read_only_fields = fields - extra_kwargs = { - 'url': {'view_name': 'api-user-detail'}, - } + class Meta: + model = models.UserProfile + fields = ('id', 'url', 'username', 'first_name', 'last_name', + 'email') + read_only_fields = fields + extra_kwargs = { + 'url': {'view_name': 'api-user-detail'}, + } diff --git a/patchwork/api/event.py b/patchwork/api/event.py index cce25a75e..e354ae2c0 100644 --- a/patchwork/api/event.py +++ b/patchwork/api/event.py @@ -96,7 +96,7 @@ class EventList(ListAPIView): def get_queryset(self): return Event.objects.all()\ - .prefetch_related('project', 'patch', 'series', 'cover', - 'previous_state', 'current_state', + .prefetch_related('project', 'patch__project', 'series__project', + 'cover', 'previous_state', 'current_state', 'previous_delegate', 'current_delegate', 'created_check') diff --git a/patchwork/api/filters.py b/patchwork/api/filters.py index 73353d900..886f06ee7 100644 --- a/patchwork/api/filters.py +++ b/patchwork/api/filters.py @@ -26,6 +26,7 @@ from django.forms import ModelMultipleChoiceField as BaseMultipleChoiceField from django.forms.widgets import MultipleHiddenInput +from patchwork.compat import NAME_FIELD from patchwork.models import Bundle from patchwork.models import Check from patchwork.models import CoverLetter @@ -151,14 +152,14 @@ class UserFilter(ModelMultipleChoiceFilter): class TimestampMixin(FilterSet): # TODO(stephenfin): These should filter on a 'updated_at' field instead - before = IsoDateTimeFilter(name='date', lookup_expr='lt') - since = IsoDateTimeFilter(name='date', lookup_expr='gte') + before = IsoDateTimeFilter(lookup_expr='lt', **{NAME_FIELD: 'date'}) + since = IsoDateTimeFilter(lookup_expr='gte', **{NAME_FIELD: 'date'}) class SeriesFilterSet(TimestampMixin, FilterSet): - submitter = PersonFilter(queryset=Person.objects.all()) - project = ProjectFilter(queryset=Project.objects.all()) + submitter = PersonFilter(queryset=Person.objects.all(), distinct=False) + project = ProjectFilter(queryset=Project.objects.all(), distinct=False) class Meta: model = Series @@ -167,12 +168,12 @@ class Meta: class CoverLetterFilterSet(TimestampMixin, FilterSet): - project = ProjectFilter(queryset=Project.objects.all()) + project = ProjectFilter(queryset=Project.objects.all(), distinct=False) # NOTE(stephenfin): We disable the select-based HTML widgets for these # filters as the resulting query is _huge_ series = BaseFilter(queryset=Project.objects.all(), - widget=MultipleHiddenInput) - submitter = PersonFilter(queryset=Person.objects.all()) + widget=MultipleHiddenInput, distinct=False) + submitter = PersonFilter(queryset=Person.objects.all(), distinct=False) class Meta: model = CoverLetter @@ -181,14 +182,15 @@ class Meta: class PatchFilterSet(TimestampMixin, FilterSet): - project = ProjectFilter(queryset=Project.objects.all()) + project = ProjectFilter(queryset=Project.objects.all(), distinct=False, + name='patch_project') # NOTE(stephenfin): We disable the select-based HTML widgets for these # filters as the resulting query is _huge_ series = BaseFilter(queryset=Series.objects.all(), - widget=MultipleHiddenInput) - submitter = PersonFilter(queryset=Person.objects.all()) - delegate = UserFilter(queryset=User.objects.all()) - state = StateFilter(queryset=State.objects.all()) + widget=MultipleHiddenInput, distinct=False) + submitter = PersonFilter(queryset=Person.objects.all(), distinct=False) + delegate = UserFilter(queryset=User.objects.all(), distinct=False) + state = StateFilter(queryset=State.objects.all(), distinct=False) class Meta: model = Patch @@ -198,7 +200,7 @@ class Meta: class CheckFilterSet(TimestampMixin, FilterSet): - user = UserFilter(queryset=User.objects.all()) + user = UserFilter(queryset=User.objects.all(), distinct=False) class Meta: model = Check @@ -211,13 +213,17 @@ class EventFilterSet(TimestampMixin, FilterSet): # filters as the resulting query is _huge_ # TODO(stephenfin): We should really use an AJAX widget of some form here project = ProjectFilter(queryset=Project.objects.all(), - widget=MultipleHiddenInput) + widget=MultipleHiddenInput, + distinct=False) series = BaseFilter(queryset=Series.objects.all(), - widget=MultipleHiddenInput) + widget=MultipleHiddenInput, + distinct=False) patch = BaseFilter(queryset=Patch.objects.all(), - widget=MultipleHiddenInput) + widget=MultipleHiddenInput, + distinct=False) cover = BaseFilter(queryset=CoverLetter.objects.all(), - widget=MultipleHiddenInput) + widget=MultipleHiddenInput, + distinct=False) class Meta: model = Event @@ -226,8 +232,8 @@ class Meta: class BundleFilterSet(FilterSet): - project = ProjectFilter(queryset=Project.objects.all()) - owner = UserFilter(queryset=User.objects.all()) + project = ProjectFilter(queryset=Project.objects.all(), distinct=False) + owner = UserFilter(queryset=User.objects.all(), distinct=False) class Meta: model = Bundle diff --git a/patchwork/api/patch.py b/patchwork/api/patch.py index 9d890eb10..f593316d9 100644 --- a/patchwork/api/patch.py +++ b/patchwork/api/patch.py @@ -25,6 +25,7 @@ from rest_framework.relations import RelatedField from rest_framework.reverse import reverse from rest_framework.serializers import SerializerMethodField +from rest_framework.serializers import ValidationError from patchwork.api.base import BaseHyperlinkedModelSerializer from patchwork.api.base import PatchworkPermission @@ -81,7 +82,7 @@ class PatchListSerializer(BaseHyperlinkedModelSerializer): project = ProjectSerializer(read_only=True) state = StateField() submitter = PersonSerializer(read_only=True) - delegate = UserSerializer() + delegate = UserSerializer(allow_null=True) mbox = SerializerMethodField() series = SeriesSerializer(many=True, read_only=True) comments = SerializerMethodField() @@ -113,6 +114,17 @@ def get_tags(self, instance): # model return {} + def validate_delegate(self, value): + """Check that the delgate is a maintainer of the patch's project.""" + if not value: + return value + + if not value.profile.maintainer_projects.only('id').filter( + id=self.instance.project.id).exists(): + raise ValidationError("User '%s' is not a maintainer for project " + "'%s'" % (value, self.instance.project)) + return value + class Meta: model = Patch fields = ('id', 'url', 'web_url', 'project', 'msgid', 'date', 'name', @@ -175,8 +187,9 @@ class PatchList(ListAPIView): def get_queryset(self): return Patch.objects.all()\ - .prefetch_related('series', 'check_set')\ - .select_related('project', 'state', 'submitter', 'delegate')\ + .prefetch_related('series', 'check_set', 'project', + 'delegate', 'series__project')\ + .select_related('state', 'submitter')\ .defer('content', 'diff', 'headers') diff --git a/patchwork/api/project.py b/patchwork/api/project.py index 6f1affad9..3609f7336 100644 --- a/patchwork/api/project.py +++ b/patchwork/api/project.py @@ -30,9 +30,9 @@ class ProjectSerializer(BaseHyperlinkedModelSerializer): - link_name = CharField(max_length=255, source='linkname') - list_id = CharField(max_length=255, source='listid') - list_email = CharField(max_length=200, source='listemail') + link_name = CharField(max_length=255, source='linkname', read_only=True) + list_id = CharField(max_length=255, source='listid', read_only=True) + list_email = CharField(max_length=200, source='listemail', read_only=True) maintainers = UserProfileSerializer(many=True, read_only=True, source='maintainer_project') @@ -41,7 +41,8 @@ class Meta: fields = ('id', 'url', 'name', 'link_name', 'list_id', 'list_email', 'web_url', 'scm_url', 'webscm_url', 'maintainers', 'subject_match') - read_only_fields = ('name', 'maintainers', 'subject_match') + read_only_fields = ('name', 'link_name', 'list_id', 'list_email', + 'maintainers', 'subject_match') versioned_fields = { '1.1': ('subject_match', ), } diff --git a/patchwork/api/series.py b/patchwork/api/series.py index 14768efbc..6b95310e3 100644 --- a/patchwork/api/series.py +++ b/patchwork/api/series.py @@ -69,8 +69,9 @@ class SeriesMixin(object): serializer_class = SeriesSerializer def get_queryset(self): - return Series.objects.all().prefetch_related('patches',)\ - .select_related('submitter', 'cover_letter', 'project') + return Series.objects.all()\ + .prefetch_related('patches__project', 'cover_letter__project')\ + .select_related('submitter', 'project') class SeriesList(SeriesMixin, ListAPIView): diff --git a/patchwork/bin/pwclient b/patchwork/bin/pwclient index 79137b0d0..2020a8c0d 100755 --- a/patchwork/bin/pwclient +++ b/patchwork/bin/pwclient @@ -327,8 +327,8 @@ def action_apply(rpc, patch_id, apply_cmd=None): print('Applying patch #%d to current directory' % patch_id) apply_cmd = ['patch', '-p1'] else: - print('Applying patch #%d using %s' % - (patch_id, repr(' '.join(apply_cmd)))) + print('Applying patch #%d using "%s"' % + (patch_id, ' '.join(apply_cmd))) print('Description: %s' % patch['name']) s = rpc.patch_get_mbox(patch_id) diff --git a/patchwork/compat.py b/patchwork/compat.py index 38caa4e86..3bbff447d 100644 --- a/patchwork/compat.py +++ b/patchwork/compat.py @@ -41,6 +41,22 @@ from rest_framework.filters import DjangoFilterBackend # noqa +# NAME_FIELD +# +# The django-filter library renamed 'Filter.name' to 'Filter.field_name' in +# 1.1. +# +# https://django-filter.readthedocs.io/en/master/guide/migration.html#migrating-to-2-0 + +if settings.ENABLE_REST_API: + import django_filters # noqa + + if django_filters.VERSION >= (1, 1): + NAME_FIELD = 'field_name' + else: + NAME_FIELD = 'name' + + # reverse, reverse_lazy # # The reverse and reverse_lazy functions have been moved to django.urls in diff --git a/patchwork/filters.py b/patchwork/filters.py index 8d0f82f2d..1358cc0be 100644 --- a/patchwork/filters.py +++ b/patchwork/filters.py @@ -19,6 +19,8 @@ from __future__ import absolute_import +import collections + from django.contrib.auth.models import User from django.utils.html import escape from django.utils.safestring import mark_safe @@ -252,7 +254,7 @@ def _form(self): selected = ' selected="true"' out += '' % ( - state.id, selected, state.name) + state.id, selected, escape(state.name)) out += '' return mark_safe(out) @@ -373,6 +375,7 @@ def url_without_me(self): class DelegateFilter(Filter): param = 'delegate' + no_delegate_str = 'Nobody' AnyDelegate = 1 def __init__(self, filters): @@ -389,6 +392,11 @@ def _set_key(self, key): if not key: return + if key == self.no_delegate_str: + self.delegate_match = key + self.applied = True + return + try: self.delegate = User.objects.get(id=int(key)) except (ValueError, User.DoesNotExist): @@ -408,6 +416,9 @@ def kwargs(self): if self.delegate: return {'delegate': self.delegate} + if self.delegate_match == self.no_delegate_str: + return {'delegate__username__isnull': True} + if self.delegate_match: return {'delegate__username__icontains': self.delegate_match} return {} @@ -420,8 +431,31 @@ def condition(self): return '' def _form(self): - return mark_safe('') + delegates = User.objects.filter( + profile__maintainer_projects__isnull=False) + + out = '' + return mark_safe(out) def key(self): if self.delegate: @@ -459,7 +493,7 @@ def set_project(self, project): self.project = project def filter_conditions(self): - kwargs = {} + kwargs = collections.OrderedDict() for f in self._filters: if f.applied: kwargs.update(f.kwargs()) @@ -472,11 +506,11 @@ def apply(self, queryset): return queryset.filter(**kwargs) def params(self): - return [(f.param, f.key()) for f in self._filters - if f.key() is not None] + return collections.OrderedDict([ + (f.param, f.key()) for f in self._filters if f.key() is not None]) def querystring(self, remove=None): - params = dict(self.params()) + params = self.params() for (k, v) in self.values.items(): if k not in params: @@ -497,7 +531,8 @@ def querystring_without_filter(self, filter): return self.querystring(filter) def applied_filters(self): - return [x for x in self._filters if x.applied] + return collections.OrderedDict([ + (x.param, x) for x in self._filters if x.applied]) def available_filters(self): return self._filters diff --git a/patchwork/management/commands/parsearchive.py b/patchwork/management/commands/parsearchive.py index 5468d35ee..51951cb46 100644 --- a/patchwork/management/commands/parsearchive.py +++ b/patchwork/management/commands/parsearchive.py @@ -26,6 +26,7 @@ from patchwork import models from patchwork.parser import parse_mail +from patchwork.parser import DuplicateMailError logger = logging.getLogger(__name__) @@ -48,6 +49,7 @@ def handle(self, *args, **options): models.CoverLetter: 0, models.Comment: 0, } + duplicates = 0 dropped = 0 errors = 0 @@ -90,10 +92,12 @@ def handle(self, *args, **options): results[type(obj)] += 1 else: dropped += 1 - except ValueError: - # TODO(stephenfin): Perhaps we should store the broken patch - # somewhere for future reference? + except DuplicateMailError as exc: + duplicates += 1 + logger.warning('Duplicate mail for message ID %s', exc.msgid) + except (ValueError, Exception) as exc: errors += 1 + logger.warning('Invalid mail: %s', repr(exc)) if (i % 10) == 0: self.stdout.write('%06d/%06d\r' % (i, count), ending='') @@ -104,6 +108,7 @@ def handle(self, *args, **options): ' %(covers)4d cover letters\n' ' %(patches)4d patches\n' ' %(comments)4d comments\n' + ' %(duplicates)4d duplicates\n' ' %(dropped)4d dropped\n' ' %(errors)4d errors\n' 'Total: %(new)s new entries' % { @@ -111,8 +116,9 @@ def handle(self, *args, **options): 'covers': results[models.CoverLetter], 'patches': results[models.Patch], 'comments': results[models.Comment], + 'duplicates': duplicates, 'dropped': dropped, 'errors': errors, - 'new': count - dropped - errors, + 'new': count - duplicates - dropped - errors, }) mbox.close() diff --git a/patchwork/management/commands/parsemail.py b/patchwork/management/commands/parsemail.py index f62fb4f51..f31f533e3 100644 --- a/patchwork/management/commands/parsemail.py +++ b/patchwork/management/commands/parsemail.py @@ -25,6 +25,7 @@ from django.utils import six from patchwork.parser import parse_mail +from patchwork.parser import DuplicateMailError logger = logging.getLogger(__name__) @@ -79,7 +80,10 @@ def handle(self, *args, **options): result = parse_mail(mail, options['list_id']) if result is None: logger.warning('Nothing added to database') - except Exception: - logger.exception('Error when parsing incoming email', + except DuplicateMailError as exc: + logger.warning('Duplicate mail for message ID %s', exc.msgid) + except (ValueError, Exception) as exc: + logger.exception('Error when parsing incoming email: %s', + repr(exc), extra={'mail': mail.as_string()}) sys.exit(1) diff --git a/patchwork/models.py b/patchwork/models.py index 6268f5b72..1a2de60a5 100644 --- a/patchwork/models.py +++ b/patchwork/models.py @@ -212,6 +212,7 @@ def __str__(self): def _user_saved_callback(sender, created, instance, **kwargs): try: profile = instance.profile + profile.user = instance except UserProfile.DoesNotExist: profile = UserProfile(user=instance) profile.save() diff --git a/patchwork/notifications.py b/patchwork/notifications.py index a5f642352..dce525aba 100644 --- a/patchwork/notifications.py +++ b/patchwork/notifications.py @@ -109,7 +109,8 @@ def expire_notifications(): EmailConfirmation.objects.filter(q).delete() # remove inactive users with no pending confirmation - pending_confs = EmailConfirmation.objects.values('user') + pending_confs = (EmailConfirmation.objects + .filter(user__isnull=False).values('user')) users = User.objects.filter(is_active=False).exclude(id__in=pending_confs) # delete users diff --git a/patchwork/parser.py b/patchwork/parser.py index 8f9af8116..705c0a07f 100644 --- a/patchwork/parser.py +++ b/patchwork/parser.py @@ -52,9 +52,24 @@ SERIES_DELAY_INTERVAL = 10 +# @see https://git-scm.com/docs/git-diff#_generating_patches_with_p +EXTENDED_HEADER_LINES = ( + 'old mode ', 'new mode ', + 'deleted file mode ', 'new file mode ', + 'copy from ', 'copy to ', + 'rename from ', 'rename to ', + 'similarity index ', 'dissimilarity index ', + 'new file mode ', 'index ') + logger = logging.getLogger(__name__) +class DuplicateMailError(Exception): + + def __init__(self, msgid): + self.msgid = msgid + + def normalise_space(value): whitespace_re = re.compile(r'\s+') return whitespace_re.sub(' ', value).strip() @@ -152,7 +167,7 @@ def clean_header(header): # on Py2, we want to do unicode(), on Py3, str(). # That gets us the decoded, un-wrapped header. if six.PY2: - header_str = unicode(sane_header) + header_str = unicode(sane_header) # noqa else: header_str = str(sane_header) @@ -486,7 +501,7 @@ def parse_version(subject, subject_prefixes): Returns: version if found, else 1 """ - regex = re.compile('^[vV](\d+)$') + regex = re.compile(r'^[vV](\d+)$') m = _find_matching_prefix(subject_prefixes, regex) if m: return int(m.group(1)) @@ -576,10 +591,13 @@ def find_comment_content(mail): """Extract content from a mail.""" commentbuf = '' - for payload, _ in _find_content(mail): + for payload, subtype in _find_content(mail): if not payload: continue + if subtype != 'plain': + continue + commentbuf += payload.strip() + '\n' commentbuf = clean_content(commentbuf) @@ -742,22 +760,22 @@ def parse_patch(content): # state specified the line we just saw, and what to expect next state = 0 # 0: text - # 1: suspected patch header (diff, ====, Index:) + # 1: suspected patch header (diff, Index:) # 2: patch header line 1 (---) # 3: patch header line 2 (+++) # 4: patch hunk header line (@@ line) # 5: patch hunk content - # 6: patch meta header (rename from/rename to) + # 6: patch meta header (rename from/rename to/new file/index) # # valid transitions: - # 0 -> 1 (diff, ===, Index:) + # 0 -> 1 (diff, Index:) # 0 -> 2 (---) # 1 -> 2 (---) # 2 -> 3 (+++) # 3 -> 4 (@@ line) # 4 -> 5 (patch content) # 5 -> 1 (run out of lines from @@-specifed count) - # 1 -> 6 (rename from / rename to) + # 1 -> 6 (extended header lines) # 6 -> 2 (---) # 6 -> 1 (other text) # @@ -773,7 +791,7 @@ def parse_patch(content): line += '\n' if state == 0: - if line.startswith('diff ') or line.startswith('===') \ + if line.startswith('diff ') \ or line.startswith('Index: '): state = 1 buf += line @@ -786,8 +804,7 @@ def parse_patch(content): buf += line if line.startswith('--- '): state = 2 - - if line.startswith(('rename from ', 'rename to ')): + if line.startswith(EXTENDED_HEADER_LINES): state = 6 elif state == 2: if line.startswith('+++ '): @@ -848,7 +865,7 @@ def fn(x): else: state = 5 elif state == 6: - if line.startswith(('rename to ', 'rename from ')): + if line.startswith(EXTENDED_HEADER_LINES): patchbuf += buf + line buf = '' elif line.startswith('--- '): @@ -873,13 +890,14 @@ def fn(x): def parse_pull_request(content): - git_re = re.compile(r'^The following changes since commit.*' + - r'^are available in the git repository at:\n' - r'^\s*([\S]+://[^\n]+)$', - re.DOTALL | re.MULTILINE | re.IGNORECASE) + git_re = re.compile( + r'^The following changes since commit.*' + r'^are available in the git repository at:\s*\n' + r'^\s*([\w+-]+(?:://|@)[\w/.@:~-]+[\s\\]*[\w/._-]*)\s*$', + re.DOTALL | re.MULTILINE | re.IGNORECASE) match = git_re.search(content) if match: - return match.group(1) + return re.sub(r'\s+', ' ', match.group(1)).strip() return None @@ -1025,8 +1043,7 @@ def parse_mail(mail, list_id=None): state=find_state(mail)) logger.debug('Patch saved') except IntegrityError: - logger.error("Duplicate mail for message ID %s" % msgid) - return None + raise DuplicateMailError(msgid=msgid) # if we don't have a series marker, we will never have an existing # series to match against. @@ -1132,15 +1149,18 @@ def parse_mail(mail, list_id=None): logger.error("Multiple SeriesReferences for %s" " in project %s!" % (msgid, project.name)) - cover_letter = CoverLetter( - msgid=msgid, - project=project, - name=name[:255], - date=date, - headers=headers, - submitter=author, - content=message) - cover_letter.save() + try: + cover_letter = CoverLetter.objects.create( + msgid=msgid, + project=project, + name=name[:255], + date=date, + headers=headers, + submitter=author, + content=message) + except IntegrityError: + raise DuplicateMailError(msgid=msgid) + logger.debug('Cover letter saved') series.add_cover_letter(cover_letter) @@ -1156,14 +1176,17 @@ def parse_mail(mail, list_id=None): author = get_or_create_author(mail) - comment = Comment( - submission=submission, - msgid=msgid, - date=date, - headers=headers, - submitter=author, - content=message) - comment.save() + try: + comment = Comment.objects.create( + submission=submission, + msgid=msgid, + date=date, + headers=headers, + submitter=author, + content=message) + except IntegrityError: + raise DuplicateMailError(msgid=msgid) + logger.debug('Comment saved') return comment diff --git a/patchwork/settings/base.py b/patchwork/settings/base.py index 4b0d55138..23b9fc95b 100644 --- a/patchwork/settings/base.py +++ b/patchwork/settings/base.py @@ -193,7 +193,7 @@ 'level': 'DEBUG', 'propagate': False, }, - 'patchwork.management.commands': { + 'patchwork.management.commands.parsemail': { 'handlers': ['console', 'mail_admins'], 'level': 'INFO', 'propagate': True, @@ -220,6 +220,7 @@ ENABLE_REST_API = True REST_RESULTS_PER_PAGE = 30 +MAX_REST_RESULTS_PER_PAGE = 250 # Set to True to enable redirections or URLs from previous versions # of patchwork diff --git a/patchwork/templates/patchwork/bundles.html b/patchwork/templates/patchwork/bundles.html index 749aaedcf..1bb3b0da7 100644 --- a/patchwork/templates/patchwork/bundles.html +++ b/patchwork/templates/patchwork/bundles.html @@ -14,7 +14,7 @@

Bundles

Bundle Project Public - Patches + Patches Download Delete diff --git a/patchwork/templates/patchwork/filters.html b/patchwork/templates/patchwork/filters.html index 5331ac855..a689b2eb5 100644 --- a/patchwork/templates/patchwork/filters.html +++ b/patchwork/templates/patchwork/filters.html @@ -16,8 +16,6 @@ form.style['display'] = 'block'; filterform_displayed = true; } - - } Selectize.define('enter_key_submit', function (options) { @@ -39,9 +37,6 @@ $(document).ready(function() { $('#submitter_input').selectize({ - valueField: 'pk', - labelField: 'name', - searchField: ['name', 'email'], plugins: ['enter_key_submit'], maxItems: 1, persist: false, @@ -52,73 +47,31 @@ this.$input.closest('form').submit(); }, this); }, - render: { - option: function(item, escape) { - if (item.name) - return '
' + escape(item.name) + ' <' + - escape(item.email) + '>' + '
'; - return '
' + escape(item.email) + '
'; +{% if "submitter" in filters.applied_filters %} +{% with submitter_filter=filters.applied_filters.submitter %} + options: [ + { + value: "{{ submitter_filter.key }}", + text: "{{ submitter_filter.condition }}", }, - item: function(item, escape) { - if (item.name) - return '
' + escape(item.name) + '
'; - return '
' + escape(item.email) + '
'; - } - }, + ], + items: ["{{ submitter_filter.key }}"], +{% endwith %} +{% endif %} load: function(query, callback) { if (query.length < 4) return callback(); req = $.ajax({ - url: '{% url 'api-submitters' %}?q=' + - encodeURIComponent(query) + '&l=10', + url: "{% url 'api-submitters' %}", + data: {q: query, l: 10}, error: function() { callback(); }, success: function(res) { - callback(res); - } - }); - } - }); -}); - - -$(document).ready(function() { - $('#delegate_input').selectize({ - valueField: 'pk', - labelField: 'name', - searchField: ['name'], - plugins: ['enter_key_submit'], - maxItems: 1, - persist: false, - onInitialize: function() { - this.on('submit', function() { - if (!this.items.length) - this.$input.val(this.lastValue); - this.$input.closest('form').submit(); - }, this); - }, - render: { - option: function(item, escape) { - if (item.email) - return '
' + escape(item.name) + ' <' + - escape(item.email) + '>' + '
'; - return '
' + escape(item.name) + '
'; - }, - item: function(item, escape) { - return '
' + escape(item.name) + '
'; - } - }, - load: function(query, callback) { - req = $.ajax({ - url: '{% url 'api-delegates' %}?q=' + - encodeURIComponent(query) + '&l=10', - error: function() { - callback(); - }, - success: function(res) { - callback(res); + callback($.map(res, function (obj) { + return {value: obj.pk, text: `${obj.name} <${obj.email}>`}; + })); } }); } @@ -130,7 +83,7 @@
Show patches with: {% if filters.applied_filters %} - {% for filter in filters.applied_filters %} + {% for filter in filters.applied_filters.values %} {{ filter.name }} = {{ filter.condition }} {% if not filter.forced %}
- - diff --git a/patchwork/templates/patchwork/patch-list.html b/patchwork/templates/patchwork/patch-list.html index 71c1ba927..f0f12cc81 100644 --- a/patchwork/templates/patchwork/patch-list.html +++ b/patchwork/templates/patchwork/patch-list.html @@ -41,9 +41,9 @@ $('#check-all').change(function(e) { if(this.checked) { - $('#patchlist').checkboxes('check'); + $('#patchlist > tbody').checkboxes('check'); } else { - $('#patchlist').checkboxes('uncheck'); + $('#patchlist > tbody').checkboxes('uncheck'); } e.preventDefault(); }); diff --git a/patchwork/templates/patchwork/pwclientrc b/patchwork/templates/patchwork/pwclientrc index 96464c1be..7d466d890 100644 --- a/patchwork/templates/patchwork/pwclientrc +++ b/patchwork/templates/patchwork/pwclientrc @@ -8,8 +8,8 @@ # default={{ project.linkname }} [{{ project.linkname }}] -url= {{scheme}}://{{site.domain}}{% url 'xmlrpc' %} +url = {{ scheme }}://{{ site.domain }}{% url 'xmlrpc' %} {% if user.is_authenticated %} -username: {{ user.username }} -password: +username = {{ user.username }} +password = {% endif %} diff --git a/patchwork/templatetags/patch.py b/patchwork/templatetags/patch.py index 4350e092e..577c78375 100644 --- a/patchwork/templatetags/patch.py +++ b/patchwork/templatetags/patch.py @@ -21,6 +21,7 @@ from __future__ import absolute_import from django import template +from django.utils.html import escape from django.utils.safestring import mark_safe from django.template.defaultfilters import stringfilter @@ -65,4 +66,4 @@ def state_class(state): @register.filter @stringfilter def msgid(value): - return mark_safe(value.strip('<>')) + return escape(value.strip('<>')) diff --git a/patchwork/tests/api/test_check.py b/patchwork/tests/api/test_check.py index 43181af3d..e3ad099cf 100644 --- a/patchwork/tests/api/test_check.py +++ b/patchwork/tests/api/test_check.py @@ -54,9 +54,9 @@ def setUp(self): self.user = create_maintainer(project) self.patch = create_patch(project=project) - def _create_check(self): + def _create_check(self, patch=None): values = { - 'patch': self.patch, + 'patch': patch if patch else self.patch, 'user': self.user, } return create_check(**values) @@ -67,6 +67,7 @@ def assertSerialized(self, check_obj, check_json): self.assertEqual(check_obj.target_url, check_json['target_url']) self.assertEqual(check_obj.context, check_json['context']) self.assertEqual(check_obj.description, check_json['description']) + self.assertEqual(check_obj.user.id, check_json['user']['id']) def test_list(self): """Validate we can list checks on a patch.""" @@ -75,6 +76,7 @@ def test_list(self): self.assertEqual(0, len(resp.data)) check_obj = self._create_check() + self._create_check(create_patch()) # second, unrelated patch resp = self.client.get(self.api_url()) self.assertEqual(status.HTTP_200_OK, resp.status_code) @@ -89,6 +91,12 @@ def test_list(self): resp = self.client.get(self.api_url(), {'user': 'otheruser'}) self.assertEqual(0, len(resp.data)) + def test_list_invalid_patch(self): + """Ensure we get a 404 for a non-existent patch.""" + resp = self.client.get( + reverse('api-check-list', kwargs={'patch_id': '99999'})) + self.assertEqual(status.HTTP_404_NOT_FOUND, resp.status_code) + def test_detail(self): """Validate we can get a specific check.""" check = self._create_check() @@ -111,12 +119,21 @@ def test_create(self): self.assertEqual(1, Check.objects.all().count()) self.assertSerialized(Check.objects.first(), resp.data) + def test_create_no_permissions(self): + """Ensure creations are rejected by standard users.""" + check = { + 'state': 'success', + 'target_url': 'http://t.co', + 'description': 'description', + 'context': 'context', + } + user = create_user() self.client.force_authenticate(user=user) resp = self.client.post(self.api_url(), check) self.assertEqual(status.HTTP_403_FORBIDDEN, resp.status_code) - def test_create_invalid(self): + def test_create_invalid_state(self): """Ensure we handle invalid check states.""" check = { 'state': 'this-is-not-a-valid-state', @@ -130,6 +147,36 @@ def test_create_invalid(self): self.assertEqual(status.HTTP_400_BAD_REQUEST, resp.status_code) self.assertEqual(0, Check.objects.all().count()) + def test_create_missing_state(self): + """Create a check using invalid values. + + Ensure we handle the state being absent. + """ + check = { + 'target_url': 'http://t.co', + 'description': 'description', + 'context': 'context', + } + + self.client.force_authenticate(user=self.user) + resp = self.client.post(self.api_url(), check) + self.assertEqual(status.HTTP_400_BAD_REQUEST, resp.status_code) + self.assertEqual(0, Check.objects.all().count()) + + def test_create_invalid_patch(self): + """Ensure we handle non-existent patches.""" + check = { + 'state': 'success', + 'target_url': 'http://t.co', + 'description': 'description', + 'context': 'context', + } + + self.client.force_authenticate(user=self.user) + resp = self.client.post( + reverse('api-check-list', kwargs={'patch_id': '99999'}), check) + self.assertEqual(status.HTTP_404_NOT_FOUND, resp.status_code) + def test_update_delete(self): """Ensure updates and deletes aren't allowed""" check = self._create_check() diff --git a/patchwork/tests/api/test_comment.py b/patchwork/tests/api/test_comment.py index f79ea4695..5fcb9463b 100644 --- a/patchwork/tests/api/test_comment.py +++ b/patchwork/tests/api/test_comment.py @@ -75,6 +75,12 @@ def test_list(self): with self.assertRaises(NoReverseMatch): self.client.get(self.api_url(cover_obj, version='1.0')) + def test_list_invalid_cover(self): + """Ensure we get a 404 for a non-existent cover letter.""" + resp = self.client.get( + reverse('api-cover-comment-list', kwargs={'pk': '99999'})) + self.assertEqual(status.HTTP_404_NOT_FOUND, resp.status_code) + @unittest.skipUnless(settings.ENABLE_REST_API, 'requires ENABLE_REST_API') class TestPatchComments(APITestCase): @@ -113,3 +119,9 @@ def test_list(self): # check we can't access comments using the old version of the API with self.assertRaises(NoReverseMatch): self.client.get(self.api_url(patch_obj, version='1.0')) + + def test_list_invalid_patch(self): + """Ensure we get a 404 for a non-existent patch.""" + resp = self.client.get( + reverse('api-patch-comment-list', kwargs={'pk': '99999'})) + self.assertEqual(status.HTTP_404_NOT_FOUND, resp.status_code) diff --git a/patchwork/tests/api/test_patch.py b/patchwork/tests/api/test_patch.py index 27b992484..f183ca9a1 100644 --- a/patchwork/tests/api/test_patch.py +++ b/patchwork/tests/api/test_patch.py @@ -72,73 +72,96 @@ def assertSerialized(self, patch_obj, patch_json): self.assertEqual(patch_obj.project.id, patch_json['project']['id']) - def test_list(self): - """Validate we can list a patch.""" + def test_list_empty(self): + """List patches when none are present.""" resp = self.client.get(self.api_url()) self.assertEqual(status.HTTP_200_OK, resp.status_code) self.assertEqual(0, len(resp.data)) + def _create_patch(self): person_obj = create_person(email='test@example.com') project_obj = create_project(linkname='myproject') state_obj = create_state(name='Under Review') patch_obj = create_patch(state=state_obj, project=project_obj, submitter=person_obj) - # anonymous user + return patch_obj + + def test_list_anonymous(self): + """List patches as anonymous user.""" + patch = self._create_patch() + resp = self.client.get(self.api_url()) self.assertEqual(status.HTTP_200_OK, resp.status_code) self.assertEqual(1, len(resp.data)) patch_rsp = resp.data[0] - self.assertSerialized(patch_obj, patch_rsp) + self.assertSerialized(patch, patch_rsp) self.assertNotIn('headers', patch_rsp) self.assertNotIn('content', patch_rsp) self.assertNotIn('diff', patch_rsp) - # authenticated user + def test_list_authenticated(self): + """List patches as an authenticated user.""" + patch = self._create_patch() user = create_user() + self.client.force_authenticate(user=user) resp = self.client.get(self.api_url()) self.assertEqual(status.HTTP_200_OK, resp.status_code) self.assertEqual(1, len(resp.data)) patch_rsp = resp.data[0] - self.assertSerialized(patch_obj, patch_rsp) + self.assertSerialized(patch, patch_rsp) - # test filtering by state - resp = self.client.get(self.api_url(), {'state': 'under-review'}) - self.assertEqual([patch_obj.id], [x['id'] for x in resp.data]) - resp = self.client.get(self.api_url(), {'state': 'missing-state'}) - self.assertEqual(0, len(resp.data)) + def test_list_filter_state(self): + """Filter patches by state.""" + self._create_patch() + user = create_user() + + state_obj_b = create_state(name='New') + create_patch(state=state_obj_b) + state_obj_c = create_state(name='RFC') + create_patch(state=state_obj_c) + + self.client.force_authenticate(user=user) + resp = self.client.get(self.api_url(), [('state', 'under-review'), + ('state', 'new')]) + self.assertEqual(2, len(resp.data)) + + def test_list_filter_project(self): + """Filter patches by project.""" + patch = self._create_patch() + user = create_user() + + self.client.force_authenticate(user=user) - # test filtering by project resp = self.client.get(self.api_url(), {'project': 'myproject'}) - self.assertEqual([patch_obj.id], [x['id'] for x in resp.data]) + self.assertEqual([patch.id], [x['id'] for x in resp.data]) + resp = self.client.get(self.api_url(), {'project': 'invalidproject'}) self.assertEqual(0, len(resp.data)) + def test_list_filter_submitter(self): + """Filter patches by submitter.""" + patch = self._create_patch() + submitter = patch.submitter + user = create_user() + + self.client.force_authenticate(user=user) + # test filtering by submitter, both ID and email - resp = self.client.get(self.api_url(), {'submitter': person_obj.id}) - self.assertEqual([patch_obj.id], [x['id'] for x in resp.data]) + resp = self.client.get(self.api_url(), {'submitter': submitter.id}) + self.assertEqual([patch.id], [x['id'] for x in resp.data]) + resp = self.client.get(self.api_url(), { 'submitter': 'test@example.com'}) - self.assertEqual([patch_obj.id], [x['id'] for x in resp.data]) + self.assertEqual([patch.id], [x['id'] for x in resp.data]) + resp = self.client.get(self.api_url(), { 'submitter': 'test@example.org'}) self.assertEqual(0, len(resp.data)) - state_obj_b = create_state(name='New') - create_patch(state=state_obj_b) - state_obj_c = create_state(name='RFC') - create_patch(state=state_obj_c) - - resp = self.client.get(self.api_url()) - self.assertEqual(3, len(resp.data)) - resp = self.client.get(self.api_url(), [('state', 'under-review')]) - self.assertEqual(1, len(resp.data)) - resp = self.client.get(self.api_url(), [('state', 'under-review'), - ('state', 'new')]) - self.assertEqual(2, len(resp.data)) - def test_list_version_1_0(self): + """List patches using API v1.0.""" create_patch() resp = self.client.get(self.api_url(version='1.0')) @@ -148,7 +171,7 @@ def test_list_version_1_0(self): self.assertNotIn('web_url', resp.data[0]) def test_detail(self): - """Validate we can get a specific patch.""" + """Show a specific patch.""" patch = create_patch( content='Reviewed-by: Test User \n', headers='Received: from somewhere\nReceived: from another place' @@ -199,43 +222,112 @@ def test_create(self): resp = self.client.post(self.api_url(), patch) self.assertEqual(status.HTTP_405_METHOD_NOT_ALLOWED, resp.status_code) - def test_update(self): - """Ensure updates can be performed by maintainers.""" - project = create_project() - patch = create_patch(project=project) + def test_update_anonymous(self): + """Update patch as anonymous user. + + Ensure updates can be performed by maintainers. + """ + patch = create_patch() state = create_state() - # anonymous user resp = self.client.patch(self.api_url(patch.id), {'state': state.name}) self.assertEqual(status.HTTP_403_FORBIDDEN, resp.status_code) - # authenticated user + def test_update_non_maintainer(self): + """Update patch as non-maintainer. + + Ensure updates can be performed by maintainers. + """ + patch = create_patch() + state = create_state() user = create_user() + self.client.force_authenticate(user=user) resp = self.client.patch(self.api_url(patch.id), {'state': state.name}) self.assertEqual(status.HTTP_403_FORBIDDEN, resp.status_code) - # maintainer + def test_update_maintainer(self): + """Update patch as maintainer. + + Ensure updates can be performed by maintainers. + """ + project = create_project() + patch = create_patch(project=project) + state = create_state() user = create_maintainer(project) + self.client.force_authenticate(user=user) - resp = self.client.patch(self.api_url(patch.id), {'state': state.name}) - self.assertEqual(status.HTTP_200_OK, resp.status_code) + resp = self.client.patch(self.api_url(patch.id), + {'state': state.name, 'delegate': user.id}) + self.assertEqual(status.HTTP_200_OK, resp.status_code, resp) self.assertEqual(Patch.objects.get(id=patch.id).state, state) + self.assertEqual(Patch.objects.get(id=patch.id).delegate, user) + + # (who can unset fields too) + # we need to send as JSON due to https://stackoverflow.com/q/30677216/ + resp = self.client.patch(self.api_url(patch.id), {'delegate': None}, + format='json') + self.assertEqual(status.HTTP_200_OK, resp.status_code, resp) + self.assertIsNone(Patch.objects.get(id=patch.id).delegate) - def test_update_invalid(self): - """Ensure we handle invalid Patch states.""" + def test_update_invalid_state(self): + """Update patch with invalid fields. + + Ensure we handle invalid Patch updates. + """ project = create_project() state = create_state() patch = create_patch(project=project, state=state) user = create_maintainer(project) - # invalid state self.client.force_authenticate(user=user) resp = self.client.patch(self.api_url(patch.id), {'state': 'foobar'}) self.assertEqual(status.HTTP_400_BAD_REQUEST, resp.status_code) self.assertContains(resp, 'Expected one of: %s.' % state.name, status_code=status.HTTP_400_BAD_REQUEST) + def test_update_legacy_delegate(self): + """Regression test for bug #313.""" + project = create_project() + state = create_state() + patch = create_patch(project=project, state=state) + user_a = create_maintainer(project) + + # create a user (User), then delete the associated UserProfile and save + # the user to ensure a new profile is generated + user_b = create_user() + self.assertEqual(user_b.id, user_b.profile.id) + user_b.profile.delete() + user_b.save() + user_b.profile.maintainer_projects.add(project) + user_b.profile.save() + self.assertNotEqual(user_b.id, user_b.profile.id) + + self.client.force_authenticate(user=user_a) + resp = self.client.patch(self.api_url(patch.id), + {'delegate': user_b.id}) + self.assertEqual(status.HTTP_200_OK, resp.status_code, resp) + self.assertEqual(Patch.objects.get(id=patch.id).state, state) + self.assertEqual(Patch.objects.get(id=patch.id).delegate, user_b) + + def test_update_invalid_delegate(self): + """Update patch with invalid fields. + + Ensure we handle invalid Patch updates. + """ + project = create_project() + state = create_state() + patch = create_patch(project=project, state=state) + user_a = create_maintainer(project) + user_b = create_user() + + self.client.force_authenticate(user=user_a) + resp = self.client.patch(self.api_url(patch.id), + {'delegate': user_b.id}) + self.assertEqual(status.HTTP_400_BAD_REQUEST, resp.status_code) + self.assertContains(resp, "User '%s' is not a maintainer" % user_b, + status_code=status.HTTP_400_BAD_REQUEST) + def test_delete(self): """Ensure deletions are always rejected.""" project = create_project() diff --git a/patchwork/tests/api/test_project.py b/patchwork/tests/api/test_project.py index 129cedb74..10044de4e 100644 --- a/patchwork/tests/api/test_project.py +++ b/patchwork/tests/api/test_project.py @@ -143,7 +143,7 @@ def test_create(self): def test_update(self): """Ensure updates can be performed by maintainers.""" project = create_project() - data = {'linkname': 'TEST'} + data = {'web_url': 'TEST'} # an anonymous user resp = self.client.patch(self.api_url(project.id), data) @@ -160,6 +160,15 @@ def test_update(self): self.client.force_authenticate(user=user) resp = self.client.patch(self.api_url(project.id), data) self.assertEqual(status.HTTP_200_OK, resp.status_code) + self.assertEqual(resp.data['web_url'], 'TEST') + + # ...with the exception of some read-only fields + resp = self.client.patch(self.api_url(project.id), { + 'link_name': 'test'}) + # NOTE(stephenfin): This actually returns HTTP 200 due to + # https://github.com/encode/django-rest-framework/issues/1655 + self.assertEqual(status.HTTP_200_OK, resp.status_code) + self.assertNotEqual(resp.data['link_name'], 'test') def test_delete(self): """Ensure deletions are rejected.""" diff --git a/patchwork/tests/api/test_series.py b/patchwork/tests/api/test_series.py index 11324bc3f..23d8cd28f 100644 --- a/patchwork/tests/api/test_series.py +++ b/patchwork/tests/api/test_series.py @@ -122,13 +122,19 @@ def test_list(self): def test_list_old_version(self): """Validate that newer fields are dropped for older API versions.""" - create_series() + cover_obj = create_cover() + series_obj = create_series() + series_obj.add_cover_letter(cover_obj) + create_series_patch(series=series_obj) resp = self.client.get(self.api_url(version='1.0')) self.assertEqual(status.HTTP_200_OK, resp.status_code) self.assertEqual(1, len(resp.data)) self.assertIn('url', resp.data[0]) self.assertNotIn('web_url', resp.data[0]) + self.assertNotIn('web_url', resp.data[0]['cover_letter']) + self.assertNotIn('mbox', resp.data[0]['cover_letter']) + self.assertNotIn('web_url', resp.data[0]['patches'][0]) def test_detail(self): """Validate we can get a specific series.""" @@ -141,11 +147,17 @@ def test_detail(self): self.assertSerialized(series, resp.data) def test_detail_version_1_0(self): - series = create_series() + cover_obj = create_cover() + series_obj = create_series() + series_obj.add_cover_letter(cover_obj) + create_series_patch(series=series_obj) - resp = self.client.get(self.api_url(series.id, version='1.0')) + resp = self.client.get(self.api_url(series_obj.id, version='1.0')) self.assertIn('url', resp.data) self.assertNotIn('web_url', resp.data) + self.assertNotIn('web_url', resp.data['cover_letter']) + self.assertNotIn('mbox', resp.data['cover_letter']) + self.assertNotIn('web_url', resp.data['patches'][0]) def test_create_update_delete(self): """Ensure creates, updates and deletes aren't allowed""" diff --git a/patchwork/tests/mail/0019-multipart-patch.mbox b/patchwork/tests/mail/0019-multipart-patch.mbox new file mode 100644 index 000000000..99d23a833 --- /dev/null +++ b/patchwork/tests/mail/0019-multipart-patch.mbox @@ -0,0 +1,55 @@ +From yuri.volchkov@gmail.com Wed Jun 20 12:22:05 2018 +From: Yuri Volchkov +To: patchwork@lists.ozlabs.org +Cc: stephen@that.guru +Subject: [PATCH] parsemail: ignore html part of multi-part comments +Date: Wed, 20 Jun 2018 14:21:42 +0200 +Message-Id: <20180620122142.9917-1-yuri.volchkov@gmail.com> +Content-Type: multipart/alternative; boundary="000000000000f93f23056f12c80c" + + +--000000000000f93f23056f12c80c +Content-Type: text/plain; charset="UTF-8" +Content-Transfer-Encoding: 8bit + +Currently an html-protection present only for patch-emails. If a +multi-part comment-email arrives, it messes up patchwork. In my case, +the symptom was a non intended 'Signed-off-by' in the downloaded +patches, with html-like junk. + +This patch makes parsemail skip all parts of comment which are not +text/plain. + +Of course, this will drop html-only emails completely. But they can +not be parsed anyways. + +Signed-off-by: Yuri Volchkov +--- + patchwork/parser.py | 4 +++- + 1 file changed, 3 insertions(+), 1 deletion(-) + +diff --git a/patchwork/parser.py b/patchwork/parser.py +index 8f9af811..b1fb7b9c 100644 +--- a/patchwork/parser.py ++++ b/patchwork/parser.py +@@ -576,9 +576,11 @@ def find_comment_content(mail): + """Extract content from a mail.""" + commentbuf = '' + +- for payload, _ in _find_content(mail): ++ for payload, subtype in _find_content(mail): + if not payload: + continue ++ if subtype != 'plain': ++ continue + + commentbuf += payload.strip() + '\n' + +--000000000000f93f23056f12c80c +Content-Type: text/html; charset="UTF-8" +Content-Transfer-Encoding: 8bit + +
Currently an html-protection present only for patch-emails. If a
multi-part comment-email arrives, it messes up patchwork. In my case,
the symptom was a non intended 'Signed-off-by' in the downloaded
patches, with html-like junk.

This patch makes parsemail skip all parts of comment which are not
text/plain.

Of course, this will drop html-only emails completely. But they can
not be parsed anyways.

Signed-off-by: Yuri Volchkov <
yuri.volchkov@gmail.com>
---
 patchwork/parser.py | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/patchwork/parser.py b/patchwork/parser.py
index 8f9af811..b1fb7b9c 100644
--- a/patchwork/parser.py
+++ b/patchwork/parser.py
@@ -576,9 +576,11 @@ def find_comment_content(mail):
     """Extract content from a mail."""
     commentbuf = ''
 
-    for payload, _ in _find_content(mail):
+    for payload, subtype in _find_content(mail):
         if not payload:
             continue
+        if subtype != 'plain':
+            continue
 
         commentbuf += payload.strip() + '\n'
 
--
2.17.1
+ +--000000000000f93f23056f12c80c-- + diff --git a/patchwork/tests/mail/0020-multipart-comment.mbox b/patchwork/tests/mail/0020-multipart-comment.mbox new file mode 100644 index 000000000..7a696a559 --- /dev/null +++ b/patchwork/tests/mail/0020-multipart-comment.mbox @@ -0,0 +1,49 @@ +From stephenfinucane@hotmail.com Wed Jun 20 13:35:48 2018 +From: Stephen Finucane +To: "stephen@that.guru" +Subject: Re: [PATCH] parsemail: ignore html part of multi-part comments +Date: Wed, 20 Jun 2018 13:35:37 +0000 +Message-ID: +References: <20180620122142.9917-1-yuri.volchkov@gmail.com> +In-Reply-To: <20180620122142.9917-1-yuri.volchkov@gmail.com> +Content-Type: multipart/alternative; + boundary="_000_DB5PR03MB18774049A0E62D211988EC8CA3770DB5PR03MB1877eurp_" +MIME-Version: 1.0 + + +--_000_DB5PR03MB18774049A0E62D211988EC8CA3770DB5PR03MB1877eurp_ +Content-Type: text/plain; charset="iso-8859-1" +Content-Transfer-Encoding: 8bit + +Yup, this looks sensible to me. Replying from Outlook's awful HTML editor to get +a sample comment to test with. + +Stephen + + +--_000_DB5PR03MB18774049A0E62D211988EC8CA3770DB5PR03MB1877eurp_ +Content-Type: text/html; charset="iso-8859-1" +Content-Transfer-Encoding: 8bit + + + + + + + +
+Yup, this looks sensible to me. Replying from Outlook's awful HTML editor to get a sample comment to test with.
+
+
+
+
+Stephen
+
+
+
+
+ + + +--_000_DB5PR03MB18774049A0E62D211988EC8CA3770DB5PR03MB1877eurp_-- + diff --git a/patchwork/tests/mail/0021-git-empty-new-file.mbox b/patchwork/tests/mail/0021-git-empty-new-file.mbox new file mode 100644 index 000000000..c3be48e6e --- /dev/null +++ b/patchwork/tests/mail/0021-git-empty-new-file.mbox @@ -0,0 +1,32 @@ +From andrew.donnellan@au1.ibm.com Thu Feb 28 00:37:42 2019 +Delivered-To: dja@axtens.net +Received: by 2002:a4a:2812:0:0:0:0:0 with SMTP id h18csp2242ooa; + Wed, 27 Feb 2019 16:37:59 -0800 (PST) +From: Andrew Donnellan +Subject: [snowpatch] [PATCH 1/3] Test commit; please ignore +To: Daniel Axtens +Date: Thu, 28 Feb 2019 11:37:42 +1100 +User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 + Thunderbird/60.5.1 +MIME-Version: 1.0 +Content-Language: en-AU + + +Doing some snowpatching. +--- + banana | 0 + 1 file changed, 0 insertions(+), 0 deletions(-) + create mode 100644 banana + +diff --git a/banana b/banana +new file mode 100644 +index 000000000000..e69de29bb2d1 +-- +2.11.0 + +_______________________________________________ +snowpatch mailing list +snowpatch@lists.ozlabs.org +https://lists.ozlabs.org/listinfo/snowpatch + + diff --git a/patchwork/tests/mail/0022-git-mode-change.mbox b/patchwork/tests/mail/0022-git-mode-change.mbox new file mode 100644 index 000000000..bf280bb8c --- /dev/null +++ b/patchwork/tests/mail/0022-git-mode-change.mbox @@ -0,0 +1,23 @@ +From linux-kbuild Sun Apr 07 23:09:09 2019 +From: Petr Vorel +Date: Sun, 07 Apr 2019 23:09:09 +0000 +To: linux-kbuild +Subject: [PATCH 1/1] kconfig: Make nconf-cfg.sh executable +Message-Id: <20190407230909.20668-1-pvorel@suse.cz> +X-MARC-Message: https://marc.info/?l=linux-kbuild&m=155467856208923 + +Although it's not required for the build *conf-cfg.sh scripts to be +executable (they're run by CONFIG_SHELL), let's be consistent with other +scripts. + +Signed-off-by: Petr Vorel +--- + scripts/kconfig/nconf-cfg.sh | 0 + 1 file changed, 0 insertions(+), 0 deletions(-) + mode change 100644 => 100755 scripts/kconfig/nconf-cfg.sh + +diff --git a/scripts/kconfig/nconf-cfg.sh b/scripts/kconfig/nconf-cfg.sh +old mode 100644 +new mode 100755 +-- +2.20.1 diff --git a/patchwork/tests/mail/0023-git-pull-request-newline-in-url.mbox b/patchwork/tests/mail/0023-git-pull-request-newline-in-url.mbox new file mode 100644 index 000000000..74c29ce47 --- /dev/null +++ b/patchwork/tests/mail/0023-git-pull-request-newline-in-url.mbox @@ -0,0 +1,48 @@ +From mboxrd@z Thu Jan 1 00:00:00 1970 +To: soc@kernel.org +From: Matthias Brugger +Subject: [GIT PULL] soc: updates for v5.5 +Message-ID: <294422a4-37b2-def5-5d32-8988f27c3a5b@gmail.com> +Date: Mon, 11 Nov 2019 13:23:51 +0100 + +Hi Olof and Arnd, + +Please have a look on the following updates of drivers/soc for v5.5 + +Thanks a lot, +Matthias + +--- + +The following changes since commit 54ecb8f7028c5eb3d740bb82b0f1d90f2df63c5c: + + Linux 5.4-rc1 (2019-09-30 10:35:40 -0700) + +are available in the Git repository at: + + https://git.kernel.org/pub/scm/linux/kernel/git/matthias.bgg/linux.git/ +tags/v5.4-next-soc + +for you to fetch changes up to 662c9d55c5ccb37f3920ecab9720f2ebf2a6ca18: + + soc: mediatek: Refactor bus protection control (2019-11-07 10:11:04 +0100) + +---------------------------------------------------------------- +refactor code of mtk-scpsys + +---------------------------------------------------------------- +Weiyi Lu (5): + soc: mediatek: Refactor polling timeout and documentation + soc: mediatek: Refactor regulator control + soc: mediatek: Refactor clock control + soc: mediatek: Refactor sram control + soc: mediatek: Refactor bus protection control + + drivers/soc/mediatek/mtk-scpsys.c | 214 ++++++++++++++++++++++++++------------ + 1 file changed, 146 insertions(+), 68 deletions(-) + +_______________________________________________ +linux-arm-kernel mailing list +linux-arm-kernel@lists.infradead.org +http://lists.infradead.org/mailman/listinfo/linux-arm-kernel + diff --git a/patchwork/tests/mail/0024-git-pull-request-trailing-space.mbox b/patchwork/tests/mail/0024-git-pull-request-trailing-space.mbox new file mode 100644 index 000000000..d62d070b8 --- /dev/null +++ b/patchwork/tests/mail/0024-git-pull-request-trailing-space.mbox @@ -0,0 +1,60 @@ +From mboxrd@z Thu Jan 1 00:00:00 1970 +To: Linux ARM Kernel List +From: XXX XXX +Subject: [GIT PULL] DaVinci SoC updates for v5.6 +Message-ID: <043eb5b2-a302-4de6-a3e8-8238e49483b1@ti.com> +Date: Tue, 14 Jan 2020 23:48:54 +0530 +Content-Type: text/plain; charset="us-ascii" +Content-Transfer-Encoding: 7bit + +The following changes since commit e42617b825f8073569da76dc4510bfa019b1c35a: + + Linux 5.5-rc1 (2019-12-08 14:57:55 -0800) + +are available in the Git repository at: + + git://git.kernel.org/pub/scm/linux/kernel/git/nsekhar/linux-davinci.git tags/davinci-for-v5.6/soc + +for you to fetch changes up to 5e06d19694a463a012c2589e29078196eb209448: + + ARM: davinci: dm644x-evm: Add Fixed regulators needed for tlv320aic33 (2020-01-13 17:36:26 +0530) + +---------------------------------------------------------------- +DaVinci SoC updates for v5.6 include migrating DM365 SoC to use +drivers/clocksource based driver for timer. This leads to removal +of machine specific timer driver. + +There are two patches adding missing fixed regulators for audio codecs +on DM365 and DM644x EVMs. + +---------------------------------------------------------------- +Bartosz Golaszewski (3): + clocksource: davinci: only enable clockevents once tim34 is initialized + ARM: davinci: dm365: switch to using the clocksource driver + ARM: davinci: remove legacy timer support + +Peter Ujfalusi (2): + ARM: davinci: dm365-evm: Add Fixed regulators needed for tlv320aic3101 + ARM: davinci: dm644x-evm: Add Fixed regulators needed for tlv320aic33 + + arch/arm/mach-davinci/Makefile | 3 +- + arch/arm/mach-davinci/board-dm365-evm.c | 20 ++ + arch/arm/mach-davinci/board-dm644x-evm.c | 20 ++ + arch/arm/mach-davinci/devices-da8xx.c | 1 - + arch/arm/mach-davinci/devices.c | 19 -- + arch/arm/mach-davinci/dm365.c | 22 +- + arch/arm/mach-davinci/include/mach/common.h | 17 -- + arch/arm/mach-davinci/include/mach/time.h | 33 --- + arch/arm/mach-davinci/time.c | 400 ---------------------------- + drivers/clocksource/timer-davinci.c | 8 +- + 10 files changed, 60 insertions(+), 483 deletions(-) + delete mode 100644 arch/arm/mach-davinci/include/mach/time.h + delete mode 100644 arch/arm/mach-davinci/time.c +~ +~ + +_______________________________________________ +linux-arm-kernel mailing list +linux-arm-kernel@lists.infradead.org +http://lists.infradead.org/mailman/listinfo/linux-arm-kernel + diff --git a/patchwork/tests/series/bugs-multiple-content-types.mbox b/patchwork/tests/series/bugs-multiple-content-types.mbox new file mode 100644 index 000000000..f7006b447 --- /dev/null +++ b/patchwork/tests/series/bugs-multiple-content-types.mbox @@ -0,0 +1,172 @@ +From mboxrd@z Thu Jan 1 00:00:00 1970 +Return-Path: +X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on + aws-us-west-2-korg-lkml-1.web.codeaurora.org +X-Spam-Level: +X-Spam-Status: No, score=-9.7 required=3.0 tests=HEADER_FROM_DIFFERENT_DOMAINS, + INCLUDES_PATCH,MAILING_LIST_MULTI,SIGNED_OFF_BY,SPF_HELO_NONE,SPF_PASS, + URIBL_BLOCKED,USER_AGENT_GIT autolearn=unavailable autolearn_force=no + version=3.4.0 +Received: from mail.kernel.org (mail.kernel.org [198.145.29.99]) + by smtp.lore.kernel.org (Postfix) with ESMTP id A702DC3A5A2 + for ; Tue, 20 Aug 2019 01:33:12 +0000 (UTC) +Received: from vger.kernel.org (vger.kernel.org [209.132.180.67]) + by mail.kernel.org (Postfix) with ESMTP id 8717B22DA7 + for ; Tue, 20 Aug 2019 01:33:12 +0000 (UTC) +Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand + id S1728887AbfHTBdL (ORCPT ); + Mon, 19 Aug 2019 21:33:11 -0400 +Received: from szxga05-in.huawei.com ([45.249.212.191]:4731 "EHLO huawei.com" + rhost-flags-OK-OK-OK-FAIL) by vger.kernel.org with ESMTP + id S1728627AbfHTBdL (ORCPT ); + Mon, 19 Aug 2019 21:33:11 -0400 +Received: from DGGEMS411-HUB.china.huawei.com (unknown [172.30.72.59]) + by Forcepoint Email with ESMTP id EF227A58CA1FC4ADCFA3; + Tue, 20 Aug 2019 09:33:03 +0800 (CST) +Received: from localhost.localdomain.localdomain (10.175.113.25) by + DGGEMS411-HUB.china.huawei.com (10.3.19.211) with Microsoft SMTP Server id + 14.3.439.0; Tue, 20 Aug 2019 09:32:56 +0800 +From: YueHaibing +To: , , + , , + , , , + , +CC: YueHaibing , , + , +Subject: [PATCH -next] bpf: Use PTR_ERR_OR_ZERO in xsk_map_inc() +Date: Tue, 20 Aug 2019 01:36:52 +0000 +Message-ID: <20190820013652.147041-1-yuehaibing@huawei.com> +X-Mailer: git-send-email 2.20.1 +MIME-Version: 1.0 +Content-Type: text/plain; charset=US-ASCII +Content-Transfer-Encoding: 7BIT +X-Originating-IP: [10.175.113.25] +X-CFilter-Loop: Reflected +Sender: netdev-owner@vger.kernel.org +Precedence: bulk +List-ID: +X-Mailing-List: netdev@vger.kernel.org +Archived-At: +List-Archive: +List-Post: + +Use PTR_ERR_OR_ZERO rather than if(IS_ERR(...)) + PTR_ERR + +Signed-off-by: YueHaibing +--- + kernel/bpf/xskmap.c | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/kernel/bpf/xskmap.c b/kernel/bpf/xskmap.c +index 4cc28e226398..942c662e2eed 100644 +--- a/kernel/bpf/xskmap.c ++++ b/kernel/bpf/xskmap.c +@@ -21,7 +21,7 @@ int xsk_map_inc(struct xsk_map *map) + struct bpf_map *m = &map->map; + + m = bpf_map_inc(m, false); +- return IS_ERR(m) ? PTR_ERR(m) : 0; ++ return PTR_ERR_OR_ZERO(m); + } + + void xsk_map_put(struct xsk_map *map) + + +From mboxrd@z Thu Jan 1 00:00:00 1970 +Return-Path: +X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on + aws-us-west-2-korg-lkml-1.web.codeaurora.org +X-Spam-Level: +X-Spam-Status: No, score=-8.2 required=3.0 tests=FROM_EXCESS_BASE64, + HEADER_FROM_DIFFERENT_DOMAINS,INCLUDES_PATCH,MAILING_LIST_MULTI,SIGNED_OFF_BY, + SPF_HELO_NONE,SPF_PASS,URIBL_BLOCKED,USER_AGENT_SANE_1 autolearn=unavailable + autolearn_force=no version=3.4.0 +Received: from mail.kernel.org (mail.kernel.org [198.145.29.99]) + by smtp.lore.kernel.org (Postfix) with ESMTP id 02AB1C3A59E + for ; Tue, 20 Aug 2019 07:28:35 +0000 (UTC) +Received: from vger.kernel.org (vger.kernel.org [209.132.180.67]) + by mail.kernel.org (Postfix) with ESMTP id CC7942082F + for ; Tue, 20 Aug 2019 07:28:34 +0000 (UTC) +Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand + id S1729348AbfHTH2e (ORCPT ); + Tue, 20 Aug 2019 03:28:34 -0400 +Received: from mga18.intel.com ([134.134.136.126]:58020 "EHLO mga18.intel.com" + rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP + id S1729047AbfHTH2e (ORCPT ); + Tue, 20 Aug 2019 03:28:34 -0400 +X-Amp-Result: SKIPPED(no attachment in message) +X-Amp-File-Uploaded: False +Received: from orsmga007.jf.intel.com ([10.7.209.58]) + by orsmga106.jf.intel.com with ESMTP/TLS/DHE-RSA-AES256-GCM-SHA384; 20 Aug 2019 00:28:33 -0700 +X-ExtLoop1: 1 +X-IronPort-AV: E=Sophos;i="5.64,408,1559545200"; + d="scan'208";a="169001452" +Received: from arappl-mobl2.ger.corp.intel.com (HELO btopel-mobl.ger.intel.com) ([10.252.53.140]) + by orsmga007.jf.intel.com with ESMTP; 20 Aug 2019 00:28:27 -0700 +Subject: Re: [PATCH -next] bpf: Use PTR_ERR_OR_ZERO in xsk_map_inc() +To: YueHaibing , magnus.karlsson@intel.com, + jonathan.lemon@gmail.com, ast@kernel.org, daniel@iogearbox.net, + kafai@fb.com, songliubraving@fb.com, yhs@fb.com, + john.fastabend@gmail.com +Cc: netdev@vger.kernel.org, bpf@vger.kernel.org, + kernel-janitors@vger.kernel.org +References: <20190820013652.147041-1-yuehaibing@huawei.com> +From: =?UTF-8?B?QmrDtnJuIFTDtnBlbA==?= +Message-ID: <93fafdab-8fb3-0f2b-8f36-0cf297db3cd9@intel.com> +Date: Tue, 20 Aug 2019 09:28:26 +0200 +User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 + Thunderbird/60.8.0 +MIME-Version: 1.0 +In-Reply-To: <20190820013652.147041-1-yuehaibing@huawei.com> +Content-Type: text/plain; charset=utf-8; format=flowed +Content-Language: en-US +Content-Transfer-Encoding: 8bit +Sender: netdev-owner@vger.kernel.org +Precedence: bulk +List-ID: +X-Mailing-List: netdev@vger.kernel.org +Archived-At: +List-Archive: +List-Post: + +On 2019-08-20 03:36, YueHaibing wrote: +> Use PTR_ERR_OR_ZERO rather than if(IS_ERR(...)) + PTR_ERR +> +> Signed-off-by: YueHaibing +> --- +> kernel/bpf/xskmap.c | 2 +- +> 1 file changed, 1 insertion(+), 1 deletion(-) +> +> diff --git a/kernel/bpf/xskmap.c b/kernel/bpf/xskmap.c +> index 4cc28e226398..942c662e2eed 100644 +> --- a/kernel/bpf/xskmap.c +> +++ b/kernel/bpf/xskmap.c +> @@ -21,7 +21,7 @@ int xsk_map_inc(struct xsk_map *map) +> struct bpf_map *m = &map->map; +> +> m = bpf_map_inc(m, false); +> - return IS_ERR(m) ? PTR_ERR(m) : 0; +> + return PTR_ERR_OR_ZERO(m); +> } +> +> void xsk_map_put(struct xsk_map *map) +> + +Acked-by: Björn Töpel + +Thanks for the patch! + +For future patches: Prefix AF_XDP socket work with "xsk:" and use "PATCH +bpf-next" to let the developers know what tree you're aiming for. + + + +Cheers! +Björn + + +> +> + + + diff --git a/patchwork/tests/test_detail.py b/patchwork/tests/test_detail.py index 5d8534eae..fa3207cfa 100644 --- a/patchwork/tests/test_detail.py +++ b/patchwork/tests/test_detail.py @@ -66,6 +66,23 @@ def test_series_dropdown(self): response, reverse('series-mbox', kwargs={'series_id': series_.id})) + def test_escaping(self): + # Warning: this test doesn't guarantee anything - it only tests some + # fields + unescaped_string = 'blahTESTblah' + patch = create_patch() + patch.diff = unescaped_string + patch.commit_ref = unescaped_string + patch.pull_url = unescaped_string + patch.name = unescaped_string + patch.msgid = unescaped_string + patch.headers = unescaped_string + patch.content = unescaped_string + patch.save() + requested_url = reverse('patch-detail', kwargs={'patch_id': patch.id}) + response = self.client.get(requested_url) + self.assertNotIn('TEST'.encode('utf-8'), response.content) + class CommentRedirectTest(TestCase): diff --git a/patchwork/tests/test_events.py b/patchwork/tests/test_events.py index 70d563de3..c5543bb85 100644 --- a/patchwork/tests/test_events.py +++ b/patchwork/tests/test_events.py @@ -42,7 +42,7 @@ def assertEventFields(self, event, parent_type='patch', **fields): self.assertIsNone(field) -class PatchCreateTest(_BaseTestCase): +class PatchCreatedTest(_BaseTestCase): def test_patch_created(self): """No series, so patch dependencies implicitly exist.""" @@ -170,7 +170,7 @@ def test_patch_delegated(self): self.assertEventFields(events[3], previous_delegate=delegate_b) -class CheckCreateTest(_BaseTestCase): +class CheckCreatedTest(_BaseTestCase): def test_check_created(self): check = utils.create_check() @@ -181,7 +181,7 @@ def test_check_created(self): self.assertEventFields(events[0]) -class CoverCreateTest(_BaseTestCase): +class CoverCreatedTest(_BaseTestCase): def test_cover_created(self): cover = utils.create_cover() @@ -192,7 +192,7 @@ def test_cover_created(self): self.assertEventFields(events[0]) -class SeriesCreateTest(_BaseTestCase): +class SeriesCreatedTest(_BaseTestCase): def test_series_created(self): series = utils.create_series() @@ -201,3 +201,28 @@ def test_series_created(self): self.assertEqual(events[0].category, Event.CATEGORY_SERIES_CREATED) self.assertEqual(events[0].project, series.project) self.assertEventFields(events[0]) + + +class SeriesChangedTest(_BaseTestCase): + + def test_series_completed(self): + """Validate 'series-completed' events.""" + series = utils.create_series(total=2) + + # the series has no patches associated with it so it's not yet complete + events = _get_events(series=series) + self.assertNotIn(Event.CATEGORY_SERIES_COMPLETED, + [x.category for x in events]) + + # create the second of two patches in the series; series is still not + # complete + utils.create_series_patch(series=series, number=2) + events = _get_events(series=series) + self.assertNotIn(Event.CATEGORY_SERIES_COMPLETED, + [x.category for x in events]) + + # now create the first patch, which will "complete" the series + utils.create_series_patch(series=series, number=1) + events = _get_events(series=series) + self.assertIn(Event.CATEGORY_SERIES_COMPLETED, + [x.category for x in events]) diff --git a/patchwork/tests/test_mboxviews.py b/patchwork/tests/test_mboxviews.py index 8eb3581ad..dabbb99ca 100644 --- a/patchwork/tests/test_mboxviews.py +++ b/patchwork/tests/test_mboxviews.py @@ -125,6 +125,21 @@ def test_header_passthrough_listid(self): header = 'List-Id: Patchwork development ' self._test_header_passthrough(header) + def _test_header_dropped(self, header): + patch = create_patch(headers=header + '\n') + response = self.client.get(reverse('patch-mbox', args=[patch.id])) + self.assertNotContains(response, header) + + def test_header_dropped_content_transfer_encoding(self): + """Validate dropping of 'Content-Transfer-Encoding' header.""" + header = 'Content-Transfer-Encoding: quoted-printable' + self._test_header_dropped(header) + + def test_header_dropped_content_type_multipart_signed(self): + """Validate dropping of 'Content-Type=multipart/signed' header.""" + header = 'Content-Type: multipart/signed' + self._test_header_dropped(header) + def test_patchwork_id_header(self): """Validate inclusion of generated 'X-Patchwork-Id' header.""" patch = create_patch() diff --git a/patchwork/tests/test_parser.py b/patchwork/tests/test_parser.py index 5ba06c0f3..216ab4813 100644 --- a/patchwork/tests/test_parser.py +++ b/patchwork/tests/test_parser.py @@ -36,6 +36,7 @@ from patchwork.parser import clean_subject from patchwork.parser import get_or_create_author from patchwork.parser import find_patch_content as find_content +from patchwork.parser import find_comment_content from patchwork.parser import find_project from patchwork.parser import find_series from patchwork.parser import parse_mail as _parse_mail @@ -582,6 +583,24 @@ def test_git_pull_with_diff(self): diff.startswith('diff --git a/arch/x86/include/asm/smp.h'), diff) + def test_git_pull_newline_in_url(self): + diff, message = self._find_content( + '0023-git-pull-request-newline-in-url.mbox') + pull_url = parse_pull_request(message) + self.assertEqual( + 'https://git.kernel.org/pub/scm/linux/kernel/git/matthias.bgg/' + 'linux.git/ tags/v5.4-next-soc', + pull_url) + + def test_git_pull_trailing_space(self): + diff, message = self._find_content( + '0024-git-pull-request-trailing-space.mbox') + pull_url = parse_pull_request(message) + self.assertEqual( + 'git://git.kernel.org/pub/scm/linux/kernel/git/nsekhar/' + 'linux-davinci.git tags/davinci-for-v5.6/soc', + pull_url) + def test_git_rename(self): diff, _ = self._find_content('0008-git-rename.mbox') self.assertTrue(diff is not None) @@ -596,6 +615,16 @@ def test_git_rename_with_diff(self): self.assertEqual(diff.count("\nrename to "), 2) self.assertEqual(diff.count('\n-a\n+b'), 1) + def test_git_new_empty_file(self): + diff, message = self._find_content('0021-git-empty-new-file.mbox') + self.assertTrue(diff is not None) + self.assertTrue(message is not None) + + def test_git_mode_change(self): + diff, message = self._find_content('0022-git-mode-change.mbox') + self.assertTrue(diff is not None) + self.assertTrue(message is not None) + def test_cvs_format(self): diff, message = self._find_content('0007-cvs-format-diff.mbox') self.assertTrue(diff.startswith('Index')) @@ -632,6 +661,14 @@ def test_no_subject(self): self.assertTrue(diff is not None) self.assertTrue(message is not None) + def test_html_multipart(self): + """Validate parsing a mail with multiple parts.""" + diff, message = self._find_content('0019-multipart-patch.mbox') + self.assertTrue(diff is not None) + self.assertTrue(message is not None) + self.assertFalse('`_ is + now supported. diff --git a/releasenotes/notes/django-rest-framework-3-7-bc6ad5df8bc54afc.yaml b/releasenotes/notes/django-rest-framework-3-7-bc6ad5df8bc54afc.yaml new file mode 100644 index 000000000..4bf92c999 --- /dev/null +++ b/releasenotes/notes/django-rest-framework-3-7-bc6ad5df8bc54afc.yaml @@ -0,0 +1,6 @@ +--- +upgrade: + - | + `Django REST Framework 3.7 + `_ is now + supported. diff --git a/releasenotes/notes/django-rest-framework-3-8-23865db833b4d188.yaml b/releasenotes/notes/django-rest-framework-3-8-23865db833b4d188.yaml new file mode 100644 index 000000000..dc2d2c8f8 --- /dev/null +++ b/releasenotes/notes/django-rest-framework-3-8-23865db833b4d188.yaml @@ -0,0 +1,6 @@ +--- +upgrade: + - | + `Django REST Framework 3.8 + `_ is now + supported. diff --git a/releasenotes/notes/django-rest-framework-3-9-0afb78322dd82367.yaml b/releasenotes/notes/django-rest-framework-3-9-0afb78322dd82367.yaml new file mode 100644 index 000000000..e65531808 --- /dev/null +++ b/releasenotes/notes/django-rest-framework-3-9-0afb78322dd82367.yaml @@ -0,0 +1,6 @@ +--- +upgrade: + - | + `Django REST Framework 3.9 + `_ is now + supported. diff --git a/releasenotes/notes/faster-api-db-queries-a1b5face736fe5b8.yaml b/releasenotes/notes/faster-api-db-queries-a1b5face736fe5b8.yaml new file mode 100644 index 000000000..6c4f61140 --- /dev/null +++ b/releasenotes/notes/faster-api-db-queries-a1b5face736fe5b8.yaml @@ -0,0 +1,4 @@ +fixes: + - | + Queries to the REST API with filters are now significantly faster: slow + database queries were reworked. diff --git a/releasenotes/notes/issue-110-a5bb3184bf831280.yaml b/releasenotes/notes/issue-110-a5bb3184bf831280.yaml new file mode 100644 index 000000000..16a0fa5f0 --- /dev/null +++ b/releasenotes/notes/issue-110-a5bb3184bf831280.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + Assigning maintained projects when creating a new user in the admin page + was causing an error. This is now resolved. diff --git a/releasenotes/notes/issue-197-4f7594db1e4c9887.yaml b/releasenotes/notes/issue-197-4f7594db1e4c9887.yaml new file mode 100644 index 000000000..2777fbc2f --- /dev/null +++ b/releasenotes/notes/issue-197-4f7594db1e4c9887.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + Long headers can be wrapped using CRLF followed by WSP (whitespace). This + whitespace was not being stripped, resulting in errant whitespace being + saved for the patch subject. This is resolved though existing patches and + cover letters will need to be updated manually. diff --git a/releasenotes/notes/issue-203-ece01ae49ceac712.yaml b/releasenotes/notes/issue-203-ece01ae49ceac712.yaml new file mode 100644 index 000000000..d8609cc0e --- /dev/null +++ b/releasenotes/notes/issue-203-ece01ae49ceac712.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + An issue that resulted in checks for all patches being listed for each + patch is resolved. + (`#203 `__) diff --git a/releasenotes/notes/issue-216-d3bf9d1baa100f74.yaml b/releasenotes/notes/issue-216-d3bf9d1baa100f74.yaml new file mode 100644 index 000000000..c3756aa00 --- /dev/null +++ b/releasenotes/notes/issue-216-d3bf9d1baa100f74.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + An issue that prevented updating of delegates using the REST API is + resolved. (`#216 `__) diff --git a/releasenotes/notes/issue-217-676f3f737e46320e.yaml b/releasenotes/notes/issue-217-676f3f737e46320e.yaml new file mode 100644 index 000000000..ecf4a118f --- /dev/null +++ b/releasenotes/notes/issue-217-676f3f737e46320e.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + A project's ``list_email``, ``list_id`` and ``link_name`` fields can no + longer be updated via the REST API. This is a superuser-only operation + that, for now, should only be done via the admin interface. + (`#217 `__) diff --git a/releasenotes/notes/issue-223-0757db6ac886374f.yaml b/releasenotes/notes/issue-223-0757db6ac886374f.yaml new file mode 100644 index 000000000..e7a5c5509 --- /dev/null +++ b/releasenotes/notes/issue-223-0757db6ac886374f.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + It's now possible to assign patches to existing bundles from a user's TODO + page. + (`#213 `__) diff --git a/releasenotes/notes/issue-224-8f1c4207aa273ac6.yaml b/releasenotes/notes/issue-224-8f1c4207aa273ac6.yaml new file mode 100644 index 000000000..f6e9ccaa1 --- /dev/null +++ b/releasenotes/notes/issue-224-8f1c4207aa273ac6.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + API resources with embedded series were not showing the ``web_url`` value + for these series. This is now shown. diff --git a/releasenotes/notes/issue-225-94215600c1b23f6e.yaml b/releasenotes/notes/issue-225-94215600c1b23f6e.yaml new file mode 100644 index 000000000..035e38d83 --- /dev/null +++ b/releasenotes/notes/issue-225-94215600c1b23f6e.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + Showing comments for a non-existant patch or cover letter was returning an + empty response instead of a HTTP 404. This issue is resolved for both + resources. diff --git a/releasenotes/notes/issue-226-27ea72266d3ee9ac.yaml b/releasenotes/notes/issue-226-27ea72266d3ee9ac.yaml new file mode 100644 index 000000000..8f891e04b --- /dev/null +++ b/releasenotes/notes/issue-226-27ea72266d3ee9ac.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + Showing checks for a non-existant patch was returning an empty response + instead of a HTTP 404. Similarly, attempting to create a new check against + this patch would result in a HTTP 5xx error instead of a HTTP 404. Both + issues are now resolved. diff --git a/releasenotes/notes/issue-237-48b9442c31e74b9d.yaml b/releasenotes/notes/issue-237-48b9442c31e74b9d.yaml new file mode 100644 index 000000000..541f44a0c --- /dev/null +++ b/releasenotes/notes/issue-237-48b9442c31e74b9d.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + Fields added in API v1.1 are now consistently excluded when requesting API + v1.0, as was intended. diff --git a/releasenotes/notes/issue-273-2bb8d2bf5fa9a57e.yaml b/releasenotes/notes/issue-273-2bb8d2bf5fa9a57e.yaml new file mode 100644 index 000000000..506de0db9 --- /dev/null +++ b/releasenotes/notes/issue-273-2bb8d2bf5fa9a57e.yaml @@ -0,0 +1,8 @@ +--- +fixes: + - | + `#197`__ was the result of a issue with OzLabs instance and not Patchwork + itself, and the fix included actually ended up corrupting subjects for + everyone. It has now been reverted. + + __ https://github.com/getpatchwork/patchwork/issues/197 diff --git a/releasenotes/notes/issue-277-5bfda7ad1f72f267.yaml b/releasenotes/notes/issue-277-5bfda7ad1f72f267.yaml new file mode 100644 index 000000000..da9460d60 --- /dev/null +++ b/releasenotes/notes/issue-277-5bfda7ad1f72f267.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + The ``pwclientrc`` samples generated by Patchwork were previously not valid + INI files. This issue is resolved. (`#277 + `__) diff --git a/releasenotes/notes/issue-60-9d4fc111242f7db6.yaml b/releasenotes/notes/issue-60-9d4fc111242f7db6.yaml new file mode 100644 index 000000000..798865927 --- /dev/null +++ b/releasenotes/notes/issue-60-9d4fc111242f7db6.yaml @@ -0,0 +1,8 @@ +--- +fixes: + - | + In the past, Patchwork used to support filtering patches that weren't + delegated to anyone. This feature was removed in v1.1.0, as part of a patch + designed to support delegation to anyone. However, that feature didn't scale + and was later removed. The ability to delegate to anyone is now itself + re-introduced. diff --git a/releasenotes/notes/issue-78-accd1f9db45a2b71.yaml b/releasenotes/notes/issue-78-accd1f9db45a2b71.yaml new file mode 100644 index 000000000..ba805a338 --- /dev/null +++ b/releasenotes/notes/issue-78-accd1f9db45a2b71.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + The delegate and submitter fields will remain populated when moving + between different pages or changing filters. + (`#78 `__) diff --git a/releasenotes/notes/sql-fix-table-lists-77667621052b2f72.yaml b/releasenotes/notes/sql-fix-table-lists-77667621052b2f72.yaml new file mode 100644 index 000000000..8eaa9f48b --- /dev/null +++ b/releasenotes/notes/sql-fix-table-lists-77667621052b2f72.yaml @@ -0,0 +1,4 @@ +--- +fixes: + - | + An sql error was fixed in `lib/sql/grant-all.postgres.sql`. diff --git a/tox.ini b/tox.ini index a64d95875..ed9e77f96 100644 --- a/tox.ini +++ b/tox.ini @@ -10,9 +10,11 @@ deps = django19: django>=1.9,<1.10 django110: django>=1.10,<1.11 django111: django>=1.11,<2.0 - django{18,19,110}: djangorestframework>=3.4,<3.7 - django111: djangorestframework>=3.6,<3.7 - django{18,19,110,111}: django-filter>=1.0,<1.1 + django{18,19}: djangorestframework>=3.4,<3.7 + django110: djangorestframework>=3.4,<3.9 + django111: djangorestframework>=3.6,<3.10 + django18: django-filter>=1.0,<1.1 + django{19,110,111}: django-filter>=1.0,<1.2 setenv = DJANGO_SETTINGS_MODULE = patchwork.settings.dev PYTHONDONTWRITEBYTECODE = 1 @@ -39,7 +41,12 @@ deps = flake8 commands = flake8 {posargs} patchwork patchwork/bin/pwclient [flake8] -ignore = E129, F405 +# Some rules are ignored as their use makes the code more difficult to read: +# +# E129 visually indented line with same indent as next logical line +# F405 'name' may be undefined, or defined from star imports: 'module' +# W504 line break after binary operator +ignore = E129, F405, W504 exclude = ./patchwork/migrations [testenv:docs]