class PatchewModule(object):
    """Module base class"""

    name = None  # The name of the module, must be unique
    default_config = ""  # The default config string
    project_config_schema = None

    def get_model(self):
        # ALways read from DB to accept configuration update in-flight
        return _module_init_config(self.__class__)

    def get_config_raw(self):
        return self.get_model().config or ""

    def get_config_obj(self):
        config = configparser.ConfigParser()
        config.read_string(self.get_config_raw())
        return config

    def get_config(self, section, field, getmethod="get", default=None):
        cfg = self.get_config_obj()
        if not section in cfg.sections():
            return default
        if field in dict(cfg.items(section)):
            return getattr(cfg, getmethod)(section, field)
        else:
            return default

    def _render_template(self, request, project, tmpl, **data):
        data["project"] = project
        data["module"] = self
        return Template(tmpl).render(Context(data))

    def _build_map_scm(self, request, project, prefix, config, scm):
        schema_html = self._build_one(request, project, "", {}, scm.item)
        item = {"html": schema_html}
        config = config or {}
        items = [
            {
                "name": name,
                "html": self._build_one(
                    request, project, prefix + "." + name, value, scm.item
                ),
            }
            for name, value in config.items()
        ]
        return self._render_template(
            request,
            project,
            TMPL_MAP,
            schema=scm,
            item_schema=scm.item,
            prefix=prefix,
            items=items,
            item=item,
        )

    def _build_array_scm(self, request, project, prefix, config, scm):
        config = config or {}
        members = [
            self._build_one(
                request, project, prefix + "." + x.name, config.get(x.name), x
            )
            for x in scm.members
        ]
        return self._render_template(
            request, project, TMPL_ARRAY, schema=scm, members=members, prefix=prefix
        )

    def _build_string_scm(self, request, project, prefix, config, scm):
        return self._render_template(
            request,
            project,
            TMPL_STRING,
            schema=scm,
            name=scm.name,
            prefix=prefix,
            value=config or "",
        )

    def _build_integer_scm(self, request, project, prefix, config, scm):
        return self._render_template(
            request,
            project,
            TMPL_INTEGER,
            schema=scm,
            name=scm.name,
            prefix=prefix,
            value=config or 0,
        )

    def _build_boolean_scm(self, request, project, prefix, config, scm):
        return self._render_template(
            request,
            project,
            TMPL_BOOLEAN,
            schema=scm,
            name=scm.name,
            prefix=prefix,
            value=config or False,
        )

    def _build_enum_scm(self, request, project, prefix, config, scm):
        return self._render_template(
            request,
            project,
            TMPL_ENUM,
            schema=scm,
            name=scm.name,
            prefix=prefix,
            value=config,
        )

    def _build_one(self, request, project, prefix, config, scm):
        if type(scm) == schema.MapSchema:
            return self._build_map_scm(request, project, prefix, config, scm)
        elif type(scm) == schema.StringSchema:
            return self._build_string_scm(request, project, prefix, config, scm)
        elif type(scm) == schema.IntegerSchema:
            return self._build_integer_scm(request, project, prefix, config, scm)
        elif type(scm) == schema.BooleanSchema:
            return self._build_boolean_scm(request, project, prefix, config, scm)
        elif type(scm) == schema.EnumSchema:
            return self._build_enum_scm(request, project, prefix, config, scm)
        elif type(scm) == schema.ArraySchema:
            return self._build_array_scm(request, project, prefix, config, scm)
        assert False

    def build_config_html(self, request, project):
        assert isinstance(self.project_config_schema, schema.ArraySchema)
        scm = self.project_config_schema
        config = self.get_project_config(project)
        return self._build_one(request, project, scm.name, config, scm)

    def get_project_config(self, project):
        return project.config.get(self.project_config_schema.name, {}) def www_authenticated_op(func):
    def inner(request, *args, **kwargs):
        if not request.user.is_authenticated:
            raise PermissionDenied
        func(request, *args, **kwargs)
        return HttpResponseSeeOther(request.POST["next"])

    return require_POST(inner) class Message(models.Model):
    """Patch email message"""

    project = models.ForeignKey("Project", on_delete=models.CASCADE)
    message_id = HeaderFieldModel(db_index=True)
    in_reply_to = HeaderFieldModel(blank=True, db_index=True)
    date = models.DateTimeField(db_index=True)
    # last_reply_date and last_comment_date are subtly different.
    # last_reply_date came first and is used to sort the series by
    # reply.  It includes all messages in the thread so that newer
    # series come first, but that includes the original messages
    # making it unsuitable for implementing "has:replies".
    # last_comment_date instead is used exactly for "has:replies"
    # (it could have been a bool field "has_comment" but that would
    # be a little less flexible for future extensions).  For "has:replies"
    # we need to block messages from the original author in order to
    # not count "ping"s as replies, but that obviously makes it a
    # poor sorting order.  So here's why there are two fields.
    last_reply_date = models.DateTimeField(db_index=True, null=True)
    last_comment_date = models.DateTimeField(db_index=True, null=True)
    subject = HeaderFieldModel()
    stripped_subject = HeaderFieldModel(db_index=True)
    version = models.PositiveSmallIntegerField(default=0)
    sender = jsonfield.JSONCharField(max_length=4096, db_index=True)
    recipients = jsonfield.JSONField()
    tags = jsonfield.JSONField(default=[])
    prefixes = jsonfield.JSONField(blank=True)
    is_complete = models.BooleanField(default=False)
    is_patch = models.BooleanField()
    is_merged = models.BooleanField(default=False, blank=True)
    is_obsolete = models.BooleanField(default=False)
    is_tested = models.BooleanField(default=False)
    is_reviewed = models.BooleanField(default=False)

    # is series head if not Null
    topic = models.ForeignKey(
        "Topic", on_delete=models.CASCADE, null=True, db_index=True
    )

    # patch index number if is_patch
    patch_num = models.PositiveSmallIntegerField(null=True, blank=True)

    # number of patches we've got if series head (non-null topic)
    num_patches = models.IntegerField(null=False, default=-1, blank=True)

    queues = models.ManyToManyField(User, blank=True, through=QueuedSeries)

    objects = MessageManager()

    maintainers = jsonfield.JSONField(blank=True, default=[])
    properties = jsonfield.JSONField(default={})

    def save_mbox(self, mbox_blob):
        save_blob(mbox_blob, self.message_id)

    def get_mbox_obj(self):
        self.get_mbox()
        return self._mbox_obj

    def get_mbox(self):
        if hasattr(self, "mbox_blob"):
            return self.mbox_blob
        self.mbox_blob = load_blob(self.message_id)
        self._mbox_obj = MboxMessage(self.mbox_blob)
        return self.mbox_blob

    mbox = property(get_mbox)

    def _get_mbox_with_tags(self, series_tags=[]):
        def mbox_with_tags_iter(mbox, tags):
            regex = "^[-A-Za-z]*:"
            old_tags = set()
            lines = lines_iter(mbox)
            need_minusminusminus = False
            for line in lines:
                if line.startswith("---"):
                    need_minusminusminus = True
                    break
                yield line
                if re.match(regex, line):
                    old_tags.add(line)

            # If no --- line, tags go at the end as there's no better place
            for tag in sorted(tags):
                if (
                    tag not in old_tags
                    and not tag.startswith("Based-on")
                    and not tag.startswith("Supersedes")
                ):
                    yield tag
            if need_minusminusminus:
                yield line
            yield from lines

        mbox = self.get_mbox()
        msg = email.message_from_string(mbox)
        container = msg.get_payload(0) if msg.is_multipart() else msg
        if container.get_content_type() != "text/plain":
            return msg.as_bytes(unixfrom=True)

        payload = decode_payload(container)
        # We might be adding 8-bit trailers to a message with 7bit CTE.  For
        # patches, quoted-printable is safe and mostly human-readable.
        try:
            container.replace_header("Content-Transfer-Encoding", "quoted-printable")
        except KeyError:
            msg.add_header("Content-Transfer-Encoding", "quoted-printable")
        payload = "\n".join(
            mbox_with_tags_iter(payload, set(self.tags).union(series_tags))
        )
        payload = quopri.encodestring(payload.encode("utf-8"))
        container.set_payload(payload, charset="utf-8")
        return msg.as_bytes(unixfrom=True)

    def get_mboxes_with_tags(self):
        if not self.is_patch:
            if not self.is_complete:
                return None
            messages = self.get_patches()
            series_tags = set(self.tags)
        else:
            messages = [self]
            series_tags = set()

        mbox_list = []
        for message in messages:
            mbox_list.append(message._get_mbox_with_tags(series_tags))
        return mbox_list

    def get_mbox_with_tags(self):
        return b"\n".join(self.get_mboxes_with_tags())

    def get_num(self):
        assert self.is_patch or self.is_series_head
        cur, total = 1, 1
        for tag in self.prefixes:
            if "/" in tag:
                n, m = tag.split("/")
                try:
                    cur, total = int(n), int(m)
                    break
                except Exception:
                    pass
        return cur, total

    def get_reply(self, message_id):
        r = Message.objects.get(project=self.project, message_id=message_id)
        assert r.in_reply_to == self.message_id
        return r

    def get_replies(self):
        return Message.objects.filter(
            project=self.project, in_reply_to=self.message_id
        ).order_by("patch_num")

    def get_in_reply_to_message(self):
        if not self.in_reply_to:
            return None
        return Message.objects.filter(
            project_id=self.project_id, message_id=self.in_reply_to
        ).first()

    @property
    def is_series_head(self):
        return self.topic_id is not None

    def get_series_head(self):
        s = self
        while s:
            if s.is_series_head:
                return s
            s = s.get_in_reply_to_message()
        return None

    def get_patches(self):
        if not self.is_series_head:
            raise Exception("Can not get patches for a non-series message")
        c, n = self.get_num()
        if c == n and self.is_patch:
            return [self]
        return (
            Message.objects.patches()
            .filter(project=self.project, in_reply_to=self.message_id)
            .order_by("patch_num")
        )

    def refresh_num_patches(self):
        c, n = self.get_num()
        if c == n and self.is_patch:
            self.num_patches = 1
        else:
            self.num_patches = (
                Message.objects.patches()
                .filter(project=self.project, in_reply_to=self.message_id)
                .count()
            )
        self.save()

    def get_total_patches(self):
        num = self.get_num() or (1, 1)
        return num[1] or 1

    def get_num_patches(self):
        if not self.is_series_head:
            raise Exception("Can not get patches for a non-series message")
        if self.num_patches == -1:
            self.refresh_num_patches()
        return self.num_patches

    def get_property(self, prop, default=None):
        x = self.properties
        *path, last = prop.split(".")
        for item in path:
            if not item in x:
                return default
            x = x[item]
        return x.get(last, default)

    def delete_property(self, prop):
        x = self.properties
        *path, last = prop.split(".")
        for item in path:
            if not item in x:
                return
            x = x[item]
        if not last in x:
            return
        old_val = x[last]
        del x[last]
        self.save()
        emit_event("SetProperty", obj=self, name=prop, value=None, old_value=old_val)

    def set_property(self, prop, value):
        if value is None:
            self.delete_property(prop)
            return
        x = self.properties
        *path, last = prop.split(".")
        for item in path:
            x = x.setdefault(item, {})
        old_val = x.get(last)
        x[last] = value
        self.save()
        emit_event("SetProperty", obj=self, name=prop, value=value, old_value=old_val)

    def get_sender_addr(self):
        return self.sender[1]

    def get_sender_name(self):
        return self.sender[0]

    def get_asctime(self):
        d = self.date
        wday = d.weekday() + 1
        return "%s %s %d %d:%02d:%02d %s" % (
            "MonTueWedThuFriSatSun"[wday * 3 - 3 : wday * 3],
            "JanFebMarAprMayJunJulAugSepOctNovDec"[d.month * 3 - 3 : d.month * 3],
            d.day,
            d.hour,
            d.minute,
            d.second,
            d.year,
        )

    def get_last_reply_date(self):
        return self.last_reply_date or self.date

    def get_body(self):
        return self.get_mbox_obj().get_body()

    def get_preview(self, maxchar=1000):
        return self.get_mbox_obj().get_preview()

    def get_diff_stat(self):
        if not self.is_series_head:
            return None
        cur = []
        patterns = [
            r"\S*\s*\|\s*[0-9]*( \+*-*)?$",
            r"\S*\s*\|\s*Bin",
            r"\S* => \S*\s*|\s*[0-9]* \+*-*$",
            r"[0-9]* files changed",
            r"1 file changed",
            r"(create|delete) mode [0-7]+",
            r"mode change [0-7]+",
            r"rename .*\([0-9]+%\)$",
            r"copy .*\([0-9]+%\)$",
            r"rewrite .*\([0-9]+%\)$",
        ]
        ret = []
        for l in self.get_body().splitlines():
            line = l.strip()
            for p in patterns:
                if re.match(p, line):
                    cur.append(line)
                    ret = cur
                    break
            else:
                cur = []
                if ret and re.match(r"--- \S", line):
                    break
        return "\n".join(ret)

    def get_message_view_url(self):
        assert self.is_patch or self.is_series_head
        if self.is_series_head:
            return reverse(
                "series_detail",
                kwargs={"project": self.project.name, "message_id": self.message_id},
            )
        else:
            return reverse(
                "series_message",
                kwargs={
                    "project": self.project.name,
                    "thread_id": self.in_reply_to,
                    "message_id": self.message_id,
                },
            )

    def get_alternative_revisions(self):
        assert self.is_series_head
        return Message.objects.filter(project=self.project, topic=self.topic)

    def set_complete(self):
        if self.is_complete:
            return
        self.is_complete = True
        self.save()
        emit_event("SeriesComplete", project=self.project, series=self)

    def set_merged(self):
        if self.is_merged:
            return
        self.is_merged = True
        self.save()
        emit_event("SeriesMerged", project=self.project, series=self)

    def create_result(self, **kwargs):
        return MessageResult(message=self, **kwargs)

    def __str__(self):
        return self.project.name + "/" + self.subject

    class Meta:
        unique_together = ("project", "message_id")
        index_together = [
            ("topic", "project", "last_reply_date"),
            ("topic", "project", "date"),
            ("topic", "last_reply_date"),
            ("topic", "date"),
        ] class Project(models.Model):
    name = models.CharField(
        max_length=1024, db_index=True, unique=True, help_text="The name of the project"
    )
    mailing_list = models.CharField(
        max_length=4096,
        blank=True,
        help_text=(
            "The mailing list of the project. "
            "Will be used to verify if a message belongs "
            "to this project"
        ),
    )
    prefix_tags = models.CharField(
        max_length=1024,
        blank=True,
        help_text=(
            "Whitespace separated tags that "
            "are required to be present messages' prefix. "
            "Tags led by '/' are treated with python regex match. "
            "Tags led by '!' mean these mustn't exist."
        ),
    )
    url = models.CharField(
        max_length=4096, blank=True, help_text="The URL of the project page"
    )
    git = models.CharField(
        max_length=4096,
        blank=True,
        help_text=(
            "The git repo of the project. If a "
            "branch other than 'master' is desired, add it to the "
            "end after a whitespace"
        ),
    )
    description = models.TextField(blank=True, help_text="Description of the project")
    logo = models.ImageField(blank=True, upload_to="logo", help_text="Project logo")
    display_order = models.IntegerField(
        default=0,
        help_text=("Order number of the project " "to display, higher number first"),
    )
    parent_project = models.ForeignKey(
        "Project",
        on_delete=models.CASCADE,
        blank=True,
        null=True,
        help_text=(
            "Parent project which this "
            "project belongs to. The parent must be a "
            "top project which has "
            "parent_project=NULL"
        ),
    )
    maintainers = models.ManyToManyField(User, blank=True)
    config = jsonfield.JSONField(default={})
    properties = jsonfield.JSONField(default={})

    def __str__(self):
        return self.name

    @classmethod
    def has_project(self, project):
        return self.objects.filter(name=project).exists()

    def save(self, *args, **kwargs):
        old_project = Project.objects.filter(pk=self.pk).first()
        old_config = old_project.config if old_project else None
        super(Project, self).save(*args, **kwargs)
        if old_config != self.config:
            emit_event("SetProjectConfig", obj=self)

    def get_property(self, prop, default=None):
        x = self.properties
        *path, last = prop.split(".")
        for item in path:
            if not item in x:
                return default
            x = x[item]
        return x.get(last, default)

    def delete_property(self, prop):
        x = self.properties
        *path, last = prop.split(".")
        for item in path:
            if not item in x:
                return
            x = x[item]
        if not last in x:
            return
        old_val = x[last]
        del x[last]
        self.save()
        emit_event("SetProperty", obj=self, name=prop, value=None, old_value=old_val)

    def set_property(self, prop, value):
        if value is None:
            self.delete_property(prop)
            return
        x = self.properties
        *path, last = prop.split(".")
        for item in path:
            x = x.setdefault(item, {})
        old_val = x.get(last)
        x[last] = value
        self.save()
        emit_event("SetProperty", obj=self, name=prop, value=value, old_value=old_val)

    def total_series_count(self):
        return Message.objects.series_heads(project=self.name).count()

    def maintained_by(self, user):
        if user.is_anonymous:
            return False
        if user.is_superuser:
            return True
        if self.maintainers.filter(id=user.id).exists():
            return True
        return False

    def get_mailing_lists(self):
        r = self.mailing_list.split()
        return [x.rstrip(",;") for x in r]

    def recognizes(self, m):
        """Test if @m is considered a message in this project"""
        addr_ok = False
        for name, addr in m.get_to() + m.get_cc():
            if addr in self.get_mailing_lists():
                addr_ok = True
                break
        if addr_ok:
            for t in self.prefix_tags.split():
                found = False
                if t.startswith("!"):
                    t = t[1:]
                    inversed = True
                else:
                    inversed = False
                for p in m.get_prefixes():
                    if t.startswith("/"):
                        found = re.match(t[1:], p)
                    else:
                        found = t.lower() == p.lower()
                    if found:
                        if inversed:
                            return False
                        break
                if not found and not inversed:
                    return False
            return True
        return False

    def get_subprojects(self):
        return Project.objects.filter(parent_project=self)

    def get_project_head(self):
        return self.get_property("git.head")

    def set_project_head(self, new_head):
        self.set_property("git.head", new_head)

    project_head = property(get_project_head, set_project_head)

    def series_update(self, message_ids):
        updated_series = []
        for msgid in message_ids:
            if msgid.startswith("<") and msgid.endswith(">"):
                msgid = msgid[1:-1]
            mo = Message.objects.filter(
                project=self, message_id=msgid, is_merged=False
            ).first()
            if not mo:
                continue
            mo.is_merged = True
            mo.save()
            s = mo.get_series_head()
            if s:
                updated_series.append(s)
        for series in updated_series:
            for p in series.get_patches():
                if not p.is_merged:
                    break
            else:
                series.set_merged()
        return len(updated_series)

    def create_result(self, **kwargs):
        return ProjectResult(project=self, **kwargs) class Result(models.Model):
    PENDING = "pending"
    SUCCESS = "success"
    FAILURE = "failure"
    RUNNING = "running"
    VALID_STATUSES = (PENDING, SUCCESS, FAILURE, RUNNING)
    VALID_STATUSES_RE = "|".join(VALID_STATUSES)

    name = models.CharField(max_length=256)
    last_update = models.DateTimeField()
    status = models.CharField(
        max_length=7,
        validators=[
            validators.RegexValidator(
                regex=VALID_STATUSES_RE,
                message="status must be one of " + ", ".join(VALID_STATUSES),
                code="invalid",
            )
        ],
    )
    log_entry = models.OneToOneField(LogEntry, on_delete=models.CASCADE, null=True)
    data = jsonfield.JSONField(default={})

    class Meta:
        index_together = [("status", "name")]

    def is_success(self):
        return self.status == self.SUCCESS

    def is_failure(self):
        return self.status == self.FAILURE

    def is_completed(self):
        return self.is_success() or self.is_failure()

    def is_pending(self):
        return self.status == self.PENDING

    def is_running(self):
        return self.status == self.RUNNING

    def save(self, *args, **kwargs):
        self.last_update = datetime.datetime.utcnow()
        old_result = Result.objects.filter(pk=self.pk).first()
        old_status = old_result.status if old_result else None
        old_entry = old_result.log_entry if old_result else None
        super(Result, self).save(*args, **kwargs)

        if self.log_entry is None and old_entry is not None:
            # Quick way to check if the field was actually saved to the database
            new_result = Result.objects.filter(pk=self.pk).first()
            if new_result.log_entry is None:
                old_entry.delete()

        emit_event("ResultUpdate", obj=self.obj, old_status=old_status, result=self)

    @staticmethod
    def renderer_from_name(name):
        found = re.match("^[^.]*", name)
        return mod.get_module(found.group(0)) if found else None

    @property
    def renderer(self):
        return Result.renderer_from_name(self.name)

    @property
    def obj(self):
        return None

    def render(self):
        if self.renderer is None:
            return None
        return self.renderer.render_result(self)

    @property
    def log(self):
        if self.log_entry is None:
            return None
        else:
            return self.log_entry.data

    @log.setter
    def log(self, value):
        if value is None:
            self.log_entry = None
            return

        entry = self.log_entry or LogEntry()
        entry.data = value
        entry.save()
        if self.log_entry is None:
            self.log_entry = entry

    def get_log_url(self, request=None, html=False):
        return None

    def __str__(self):
        return "%s (%s)" % (self.name, self.status) class PluginMethodField(SerializerMethodField):
    """
    A read-only field that get its representation from calling a method on
    the plugin class. The method called will be of the form
    "get_{field_name}", and should take a single argument, which is the
    object being serialized.

    For example:

        fields['extra_info'] = api.rest.PluginMethodField(obj=self)

        def get_extra_info(self, obj):
            return ...  # Calculate some data to return.
    """

    def __init__(self, obj=None, method_name=None, **kwargs):
        self.obj = obj
        super(PluginMethodField, self).__init__(method_name=method_name, **kwargs)

    def to_representation(self, value):
        method = getattr(self.obj, self.method_name)
        request = self.context["request"]
        format = self.context.get("format", None)
        return method(value, request, format) class SeriesSerializer(BaseMessageSerializer):
    class Meta:
        model = Message
        subclass_read_only_fields = (
            "message",
            "stripped_subject",
            "num_patches",
            "total_patches",
            "results",
            "mbox_uri",
        )
        fields = (
            BaseMessageSerializer.Meta.fields
            + subclass_read_only_fields
            + (
                "last_comment_date",
                "last_reply_date",
                "is_complete",
                "is_merged",
                "is_obsolete",
                "is_tested",
                "is_reviewed",
                "maintainers",
            )
        )
        read_only_fields = (
            BaseMessageSerializer.Meta.read_only_fields + subclass_read_only_fields
        )

    resource_uri = HyperlinkedMessageField(view_name="series-detail")
    mbox_uri = HyperlinkedMessageField(view_name="series-mbox")
    message = HyperlinkedMessageField(view_name="messages-detail")
    results = HyperlinkedMessageField(
        view_name="results-list", lookup_field="series_message_id"
    )
    total_patches = SerializerMethodField()
    maintainers = ListField(child=CharField(), required=False)

    def __init__(self, *args, **kwargs):
        self.detailed = kwargs.pop("detailed", False)
        super(SeriesSerializer, self).__init__(*args, **kwargs)

    def get_fields(self):
        fields = super(SeriesSerializer, self).get_fields()
        request = self.context["request"]
        dispatch_module_hook(
            "rest_series_fields_hook",
            request=request,
            fields=fields,
            detailed=self.detailed,
        )
        return fields

    def get_total_patches(self, obj):
        return obj.get_total_patches() def reverse_detail(obj, request):
    if isinstance(obj, Project):
        return rest_framework.reverse.reverse(
            "project-detail", request=request, kwargs={"pk": obj.id}
        )
    if isinstance(obj, Message):
        assert obj.is_series_head
        return rest_framework.reverse.reverse(
            "series-detail",
            request=request,
            kwargs={"projects_pk": obj.project.id, "message_id": obj.message_id},
        )
    if isinstance(obj, ProjectResult):
        po = obj.obj
        return rest_framework.reverse.reverse(
            "results-detail",
            request=request,
            kwargs={"projects_pk": po.id, "name": obj.name},
        )
    if isinstance(obj, MessageResult):
        m = obj.obj
        return rest_framework.reverse.reverse(
            "results-detail",
            request=request,
            kwargs={
                "projects_pk": m.project.id,
                "series_message_id": m.message_id,
                "name": obj.name,
            },
        )
    raise Exception("unhandled object type") class APILoginRequiredView(APIView):

    allowed_groups = []

    def check_permission(self, request):
        return False

    def check_request(self, request):
        if not request.user.is_authenticated:
            raise PermissionDenied()
        if request.user.is_superuser:
            return
        for grp in request.user.groups.all():
            if grp.name in self.allowed_groups or self.check_permission(request):
                return
        raise PermissionDenied() def prepare_series(request, s, fields=None):
    r = {}

    def want_field(f):
        return not fields or f in fields

    if want_field("subject"):
        r["subject"] = s.subject
    if want_field("project"):
        r["project"] = s.project.name
    if want_field("message-id"):
        r["message-id"] = s.message_id
    if want_field("patches"):
        r["patches"] = [prepare_patch(x) for x in s.get_patches()]
    if want_field("properties"):
        # For backwards compatibility with old clients
        r["properties"] = {}
    if want_field("tags"):
        r["tags"] = s.tags
    if want_field("is_complete"):
        r["is_complete"] = s.is_complete
    if fields:
        r = dict([(k, v) for k, v in r.items() if k in fields])
    return r
import os
import subprocess
import rest_framework
import api.rest
import schema
from django.conf.urls import url
from django.http import Http404, HttpResponseRedirect
from django.urls import reverse
from django.core.exceptions import PermissionDenied
from django.utils.html import format_html
from django.utils.decorators import method_decorator
from mod import PatchewModule, www_authenticated_op
from event import declare_event, register_handler
from api.models import Message, Project, Result
from api.rest import PluginMethodField, SeriesSerializer, reverse_detail
from api.views import APILoginRequiredView, prepare_series
from rest_framework import generics, serializers
from rest_framework.fields import CharField, SerializerMethodField

    def api_url_hook(self, urlpatterns):
        urlpatterns.append(
            url(
                r"^v1/series/unapplied/$",
                UnappliedSeriesView.as_view(),
                name="unapplied",
            )
        )

    def pending_series(self, target_repo):
        q = Message.objects.filter(results__name="git", results__status="pending")

        # Postgres could use JSON fields instead.  Fortunately projects are
        # few so this is cheap
        def match_target_repo(config, target_repo):
            push_to = config.get("git", {}).get("push_to")
            if push_to is None:
                return False
            if target_repo is None or target_repo == "":
                return True
            elif target_repo[-1] != "/":
                return push_to == target_repo or push_to.startswith(target_repo + "/")
            else:
                return push_to.startswith(target_repo)

        projects = Project.objects.values_list("id", "config").all()
        projects = [
            pid for pid, config in projects if match_target_repo(config, target_repo)
        ]
        q = q.filter(project__pk__in=projects)
        return q


class ApplierGetView(APILoginRequiredView):
    name = "applier-get"
    allowed_groups = ["importers"]

    def handle(self, request, target_repo=None):
        m = _instance.pending_series(target_repo).first()
        if not m:
            return None

        response = prepare_series(
            request,
            m,
            fields=["project", "message-id", "patches", "properties", "tags"],
        )

        po = m.project
        config = _instance.get_project_config(po)
        for k, v in config.items():
            response["git." + k] = v
        base = _instance.get_base(m)
        if base:
            response["git.repo"] = base.data["repo"]
            response["git.base"] = base.data["tag"]
        response["project.git"] = po.git
        response["mbox_uri"] = rest_framework.reverse.reverse(
            "series-mbox",
            kwargs={"projects_pk": m.project_id, "message_id": m.message_id},
            request=request,
        )
        response["result_uri"] = reverse_detail(m.git_result, request)
        response["git.push_options"] = m.git_result.data.get("git.push_options")
        return response


class UnappliedSeriesSerializer(SeriesSerializer):
    class Meta:
        model = Message
        fields = SeriesSerializer.Meta.fields + ("mirror", "result_uri", "push_options")

    mirror = SerializerMethodField()
    result_uri = SerializerMethodField()
    push_options = SerializerMethodField()

    def get_push_options(self, obj):
        if obj.project.config.get("git", {}).get("use_git_push_option", False):
            return obj.git_result.data.get("git.push_options")
        else:
            return None

    def get_result_uri(self, obj):
        request = self.context["request"]
        return reverse_detail(obj.git_result, request)

    def get_mirror(self, obj):
        request = self.context["request"]
        mirror = _instance.get_mirror(obj.project, request, None)
        mirror["source"] = obj.project.git
        return mirror


class UnappliedSeriesView(generics.ListAPIView):
    name = "unapplied"
    serializer_class = UnappliedSeriesSerializer

    def get_queryset(self):
        target_repo = self.request.query_params.get("target_repo")
        return _instance.pending_series(target_repo)


class ApplierReportView(APILoginRequiredView):
    name = "applier-report"
    allowed_groups = ["importers"]

    def handle(
        self,
        request,
        project,
        message_id,
        tag,
        url,
        base,
        repo,
        failed,
        log,
        maintainers=[],
    ):

        p = Project.objects.get(name=project)