backend/code_review_backend/issues/serializers.py (274 lines of code) (raw):

# This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. from collections import defaultdict from urllib.parse import urlparse from django.conf import settings from django.db import transaction from rest_framework import serializers from code_review_backend.issues.models import ( LEVEL_ERROR, Diff, Issue, IssueLink, Repository, Revision, ) class RepositorySerializer(serializers.ModelSerializer): """ Serialize a Repository """ class Meta: model = Repository fields = ("id", "slug", "url") class RepositoryGetOrCreateField(serializers.SlugRelatedField): help_text = ( "Get or create a repository. URL must match allowed hosts from settings." ) default_error_messages = { **serializers.SlugRelatedField.default_error_messages, "invalid_url": "Repository URL must match hg.mozilla.org.", } queryset = Repository.objects.all() def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs, slug_field="url") def to_internal_value(self, url): parsed = urlparse(url) if parsed.netloc not in settings.ALLOWED_REPOSITORY_HOSTS: try: return self.get_queryset().get(url=url) except Repository.DoesNotExist: self.fail("invalid_url") except (TypeError, ValueError): self.fail("invalid") return try: repo, _ = self.get_queryset().get_or_create( url=url, defaults={"slug": parsed.path.lstrip("/")} ) return repo except (TypeError, ValueError): self.fail("invalid") class RevisionSerializer(serializers.ModelSerializer): """ Serialize a Revision in a Repository """ base_repository = RepositoryGetOrCreateField() head_repository = RepositoryGetOrCreateField() diffs_url = serializers.HyperlinkedIdentityField( view_name="revision-diffs-list", lookup_url_kwarg="revision_id" ) issues_bulk_url = serializers.HyperlinkedIdentityField( view_name="revision-issues-bulk", lookup_url_kwarg="revision_id" ) phabricator_url = serializers.URLField(read_only=True) phabricator_id = serializers.IntegerField( required=False, allow_null=True, min_value=1, max_value=2147483647, ) class Meta: model = Revision fields = ( "id", "base_repository", "head_repository", "base_changeset", "head_changeset", "phabricator_id", "phabricator_phid", "title", "bugzilla_id", "diffs_url", "issues_bulk_url", "phabricator_url", ) class RevisionLightSerializer(serializers.ModelSerializer): """ Serialize a Revision in a Diff light serializer """ base_repository = RepositoryGetOrCreateField() head_repository = RepositoryGetOrCreateField() phabricator_url = serializers.URLField(read_only=True) class Meta: model = Revision fields = ( "id", "phabricator_id", "base_repository", "head_repository", "base_changeset", "head_changeset", "phabricator_id", "title", "bugzilla_id", "phabricator_url", ) class DiffSerializer(serializers.ModelSerializer): """ Serialize a Diff in a Revision Used for full management """ repository = serializers.SlugRelatedField( queryset=Repository.objects.all(), slug_field="url" ) issues_url = serializers.HyperlinkedIdentityField( view_name="issues-list", lookup_url_kwarg="diff_id" ) class Meta: model = Diff fields = ( "id", "phid", "review_task_id", "repository", "mercurial_hash", "issues_url", ) class DiffLightSerializer(serializers.ModelSerializer): """ Serialize a Diff from an Issue in a check """ repository = serializers.SlugRelatedField( queryset=Repository.objects.all(), slug_field="url" ) revision = RevisionLightSerializer() class Meta: model = Diff fields = ("id", "repository", "revision") class DiffFullSerializer(serializers.ModelSerializer): """ Serialize a Diff with revision details This is used in a read only context """ revision = RevisionSerializer(read_only=True) repository = RepositorySerializer(read_only=True) issues_url = serializers.HyperlinkedIdentityField( view_name="issues-list", lookup_url_kwarg="diff_id" ) nb_issues = serializers.IntegerField(read_only=True) nb_issues_publishable = serializers.IntegerField(read_only=True) nb_warnings = serializers.IntegerField(read_only=True) nb_errors = serializers.IntegerField(read_only=True) class Meta: model = Diff fields = ( "id", "revision", "phid", "review_task_id", "repository", "mercurial_hash", "issues_url", "nb_issues", "nb_issues_publishable", "nb_warnings", "nb_errors", "created", ) class IssueSerializer(serializers.ModelSerializer): """ Serialize an Issue in a Diff """ publishable = serializers.BooleanField(read_only=True) check = serializers.CharField(source="analyzer_check", required=False) publishable = serializers.BooleanField(read_only=True) in_patch = serializers.BooleanField( source="issue_links__in_patch", allow_null=True, required=False ) new_for_revision = serializers.BooleanField( source="issue_links__new_for_revision", allow_null=True, required=False ) line = serializers.IntegerField( source="issue_links__line", allow_null=True, required=False ) nb_lines = serializers.IntegerField( source="issue_links__nb_lines", allow_null=True, required=False ) char = serializers.IntegerField( source="issue_links__char", allow_null=True, required=False ) class Meta: model = Issue fields = ( "id", "hash", "analyzer", "path", "level", "check", "message", # Attrs coming from IssueLink "publishable", "in_patch", "new_for_revision", "line", "nb_lines", "char", ) class IssueHashSerializer(serializers.ModelSerializer): """ Serialize an Issue hash """ class Meta: model = Issue fields = ( "id", "hash", ) read_only_fields = ("id", "hash") class SingleIssueBulkSerializer(IssueSerializer): # Make hash non unique to avoid validation checks hash = serializers.CharField(max_length=32) class IssueBulkSerializer(serializers.Serializer): diff_id = serializers.PrimaryKeyRelatedField( # Initialized depending on the revision used for the creation queryset=Diff.objects.none(), style={"base_template": "input.html"}, required=False, allow_null=True, ) issues = SingleIssueBulkSerializer(many=True) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if not self.context.get("revision"): return self.fields["diff_id"].queryset = self.context["revision"].diffs.all() @transaction.atomic def create(self, validated_data): diff = validated_data.get("diff_id", None) link_attrs = defaultdict(list) # Separate attributes that are specific to the IssueLink M2M for issue in validated_data["issues"]: link_attrs[issue["hash"]].append( { "new_for_revision": issue.pop( "issue_links__new_for_revision", None ), "in_patch": issue.pop("issue_links__in_patch", None), "line": issue.pop("issue_links__line", None), "nb_lines": issue.pop("issue_links__nb_lines", None), "char": issue.pop("issue_links__path", None), } ) # Only create issues that do not exist yet Issue.objects.bulk_create( [Issue(**values) for values in validated_data["issues"]], ignore_conflicts=True, ) # Retrieve issues to get existing IDs hashes = set(link_attrs.keys()) known_issues = {i.hash: i for i in Issue.objects.filter(hash__in=hashes)} assert set(known_issues.keys()) == hashes, "Failed to create all issues" # Create all links, using DB conflicts links = IssueLink.objects.bulk_create( [ IssueLink( issue_id=known_issues[issue_hash].id, diff=diff, revision=self.context["revision"], **link, ) for issue_hash, links in link_attrs.items() for link in links ], ignore_conflicts=True, ) # Endpoint expects Issue with specific attributes for re-serialization of links # TODO in treeherder: only expose hash & publishable in output output = [] for issue_hash, links in link_attrs.items(): for link in links: existing_issue = known_issues[issue_hash] # Set attributes for re-serialization output_link = {f"issue_links__{k}": v for k, v in link.items()} output_link.update(vars(existing_issue)) output_link["publishable"] = ( link["in_patch"] and existing_issue.level == LEVEL_ERROR ) output.append(output_link) return { "diff_id": diff, "issues": output, } class IssueCheckSerializer(IssueSerializer): """ Serialize an Issue with all the diffs where it has been found. Each diff is serialized with its revision's information. """ diffs = DiffLightSerializer(many=True) class Meta: model = Issue fields = IssueSerializer.Meta.fields + ("diffs",) class IssueCheckStatsSerializer(serializers.Serializer): """ Serialize the usage statistics for each check encountered """ # The view aggregates issues depending on their reference to a repository (via IssueLink M2M) repository = serializers.SlugField( source="issue_links__revision__head_repository__slug" ) analyzer = serializers.CharField() check = serializers.CharField(source="analyzer_check") total = serializers.IntegerField() publishable = serializers.IntegerField(read_only=True, default=0) class Meta: model = Issue fields = IssueSerializer.Meta.fields + ("repositories",) class HistoryPointSerializer(serializers.Serializer): """ Serialize a data point for issue checks history graphs """ date = serializers.DateField() total = serializers.IntegerField()