<commit_before># -*- coding: utf-8 -*-

"""

JSON Graph Interchange Format
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The JSON Graph Interchange Format (JGIF) is `specified <http://jsongraphformat.info/>`_ similarly to the Node-Link
JSON. Interchange with this format provides compatibilty with other software and repositories, such as the
`Causal Biological Network Database <http://causalbionet.com/>`_.

"""

import logging
from collections import defaultdict

from ..canonicalize import decanonicalize_node, decanonicalize_edge
from ..constants import CITATION_TYPE, CITATION_REFERENCE, CITATION_NAME, UNQUALIFIED_EDGES
from ..constants import RELATION, FUNCTION, EVIDENCE, CITATION, ANNOTATIONS, METADATA_NAME
from ..parser import BelParser
from ..struct import BELGraph

__all__ = [
    'from_cbn_jgif',
    'from_jgif',
    'to_jgif',
]

log = logging.getLogger(__name__)

annotation_map = {
    'tissue': 'Tissue',
    'disease': 'Disease',
    'species_common_name': 'Species',
    'cell': 'Cell',
}

species_map = {
    'human': '9606',
    'rat': '10116',
    'mouse': '10090',
}

placeholder_evidence = "This Network edge has no supporting evidence.  Please add real evidence to this edge prior to deleting."

EXPERIMENT_CONTEXT = 'experiment_context'

def get_citation(evidence):
    citation = {
        CITATION_TYPE: evidence['citation']['type'].strip(),
        CITATION_REFERENCE: evidence['citation']['id'].strip()
    }

    if 'name' in evidence['citation']:
        citation[CITATION_NAME] = evidence['citation']['name'].strip()

    return citation

def map_cbn(d):
    """Pre-processes the JSON from the CBN

    - removes statements without evidence, or with placeholder evidence

    :param dict d: Raw JGIF from the CBN
    :return: Preprocessed JGIF
    :rtype: dict
    """
    for i, edge in enumerate(d['graph']['edges']):
        if 'metadata' not in d['graph']['edges'][i]:
            continue

        if 'evidences' not in d['graph']['edges'][i]['metadata']:
            continue

        for j, evidence in enumerate(d['graph']['edges'][i]['metadata']['evidences']):
            if EXPERIMENT_CONTEXT not in evidence:
                continue

            # ctx = {k.strip().lower(): v.strip() for k, v in evidence[EXPERIMENT_CONTEXT].items() if v.strip()}

            new_context = {}

            for key, value in evidence[EXPERIMENT_CONTEXT].items():
                value = value.strip()

                if not value:
                    continue

                key = key.strip().lower()

                if key == 'species_common_name':
                    new_context['Species'] = species_map[value.lower()]
                elif key in annotation_map:
                    new_context[annotation_map[key]] = value
                else:
                    new_context[key] = value

            '''
            for k, v in annotation_map.items():
                if k not in ctx:
                    continue

                d['graph']['edges'][i]['metadata']['evidences'][j][EXPERIMENT_CONTEXT][v] = ctx[k]
                del d['graph']['edges'][i]['metadata']['evidences'][j][EXPERIMENT_CONTEXT][k]

            if 'species_common_name' in ctx:
                species_name = ctx['species_common_name'].strip().lower()
                d['graph']['edges'][i]['metadata']['evidences'][j][EXPERIMENT_CONTEXT]['Species'] = species_map[
                    species_name]
                del d['graph']['edges'][i]['metadata']['evidences'][j][EXPERIMENT_CONTEXT][
                    'species_common_name']
            '''

            d['graph']['edges'][i]['metadata']['evidences'][j][EXPERIMENT_CONTEXT] = new_context
    return d

def from_cbn_jgif(graph_jgif_dict):
    """Maps the JGIF used by the Causal Biological Network Database to standard namespace and annotations, then
    builds a BEL graph using :func:`pybel.from_jgif`.

    :param dict graph_jgif_dict: The JSON object representing the graph in JGIF format
    :return: A BEL graph
    :rtype: BELGraph

    Example:

    >>> import requests
    >>> from pybel import from_cbn_jgif
    >>> apoptosis_url = 'http://causalbionet.com/Networks/GetJSONGraphFile?networkId=810385422'
    >>> graph_jgif_dict = requests.get(apoptosis_url).json()
    >>> graph = from_cbn_jgif(graph_jgif_dict)
    """
    return from_jgif(map_cbn(graph_jgif_dict))

# TODO add metadata
def from_jgif(graph_jgif_dict):
    """Builds a BEL graph from a JGIF JSON object.

    :param dict graph_jgif_dict: The JSON object representing the graph in JGIF format
    :return: A BEL graph
    :rtype: BELGraph
    """
    root = graph_jgif_dict['graph']

    metadata = {
        METADATA_NAME: root.get('label')
    }
    metadata.update(root['metadata'])

    graph = BELGraph(**metadata)
    parser = BelParser(graph)
    parser.bel_term.addParseAction(parser.handle_term)

    for node in root['nodes']:
        if 'label' not in node:
            log.warning('node missing label: %s', node)
        node_label = node['label']
        parser.bel_term.parseString(node_label)

    for i, edge in enumerate(root['edges']):

        if edge['relation'] in {'actsIn'}:
            continue  # don't need legacy BEL format

        if edge['relation'] in UNQUALIFIED_EDGES:
            pass

        if not edge['relation'] in UNQUALIFIED_EDGES and (
                        'evidences' not in edge['metadata'] or not edge['metadata']['evidences']):
            log.debug('No evidence for edge %s', edge['label'])
            continue

        for evidence in edge['metadata']['evidences']:
            if 'citation' not in evidence or not evidence['citation']:
                continue

            if 'type' not in evidence['citation'] and 'id' not in evidence['citation']:
                continue

            summary_text = evidence['summary_text'].strip()

            if not summary_text or summary_text == placeholder_evidence:
                continue

            parser.control_parser.clear()
            parser.control_parser.citation = get_citation(evidence)
            parser.control_parser.evidence = summary_text
            parser.control_parser.annotations.update(evidence[EXPERIMENT_CONTEXT])

            bel_statement = edge['label']

            try:
                parser.parseString(bel_statement, i)
            except Exception as e:
                log.warning('Error %s for %s', e, bel_statement)

    return graph

def to_jgif(graph):
    """Builds a JGIF dictionary from a BEL graph.

    :param BELGraph graph: A BEL graph
    :return: A JGIF dictionary
    :rtype: dict

    .. warning::

        Untested! This format is not general purpose and is therefore time is not heavily invested. If you want to
        use Cytoscape.js, we suggest using :func:`pybel.to_cx` instead.

    Example:

    >>> import pybel, os, json
    >>> graph_url = 'https://arty.scai.fraunhofer.de/artifactory/bel/knowledge/selventa-small-corpus/selventa-small-corpus-20150611.bel'
    >>> graph = pybel.from_url(graph_url)
    >>> graph_jgif_json = pybel.to_jgif(graph)
    >>> with open(os.path.expanduser('~/Desktop/small_corpus.json'), 'w') as f:
    ...     json.dump(graph_jgif_json, f)
    """
    node_to_bel = {}
    u_v_r_bel = {}

    nodes_entry = []
    edges_entry = []

    for i, node in enumerate(graph.nodes_iter()):
        bel = decanonicalize_node(graph, node)
        node_to_bel[node] = bel

        nodes_entry.append({
            'id': bel,
            'label': bel,
            'nodeId': i,
            'bel_function_type': graph.node[node][FUNCTION],
            'metadata': {}
        })

    for u, v in graph.edges_iter():
        relation_evidences = defaultdict(list)

        for k, d in graph.edge[u][v].items():

            if (u, v, d[RELATION]) not in u_v_r_bel:
                u_v_r_bel[u, v, d[RELATION]] = decanonicalize_edge(graph, u, v, k)

            bel = u_v_r_bel[u, v, d[RELATION]]

            evidence_dict = {
                'bel_statement': bel,
                'experiment_context': d[ANNOTATIONS],
            }

            if EVIDENCE in d:
                evidence_dict['summary_text'] = d[EVIDENCE]

            if CITATION in d:
                evidence_dict['citation'] = d[CITATION]

            relation_evidences[d[RELATION]].append(evidence_dict)

        for relation, evidences in relation_evidences.items():
            edges_entry.append({
                'source': node_to_bel[u],
                'target': node_to_bel[v],
                'relation': relation,
                'label': u_v_r_bel[u, v, relation],
                'metadata': {
                    'evidences': evidences
                }
            })

    return {
        'graph': {
            'metadata': graph.document,
            'nodes': nodes_entry,
            'edges': edges_entry
        }
    }
<commit_msg>Update JGIF export<commit_after># -*- coding: utf-8 -*-

"""

JSON Graph Interchange Format
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The JSON Graph Interchange Format (JGIF) is `specified <http://jsongraphformat.info/>`_ similarly to the Node-Link
JSON. Interchange with this format provides compatibilty with other software and repositories, such as the
`Causal Biological Network Database <http://causalbionet.com/>`_.

"""

import logging
from collections import defaultdict

from ..canonicalize import decanonicalize_node, decanonicalize_edge
from ..constants import *
from ..parser import BelParser
from ..struct import BELGraph

__all__ = [
    'from_cbn_jgif',
    'from_jgif',
    'to_jgif',
]

log = logging.getLogger(__name__)

annotation_map = {
    'tissue': 'Tissue',
    'disease': 'Disease',
    'species_common_name': 'Species',
    'cell': 'Cell',
}

species_map = {
    'human': '9606',
    'rat': '10116',
    'mouse': '10090',
}

placeholder_evidence = "This Network edge has no supporting evidence.  Please add real evidence to this edge prior to deleting."

EXPERIMENT_CONTEXT = 'experiment_context'

def get_citation(evidence):
    citation = {
        CITATION_TYPE: evidence['citation']['type'].strip(),
        CITATION_REFERENCE: evidence['citation']['id'].strip()
    }

    if 'name' in evidence['citation']:
        citation[CITATION_NAME] = evidence['citation']['name'].strip()

    return citation

def map_cbn(d):
    """Pre-processes the JSON from the CBN

    - removes statements without evidence, or with placeholder evidence

    :param dict d: Raw JGIF from the CBN
    :return: Preprocessed JGIF
    :rtype: dict
    """
    for i, edge in enumerate(d['graph']['edges']):
        if 'metadata' not in d['graph']['edges'][i]:
            continue

        if 'evidences' not in d['graph']['edges'][i]['metadata']:
            continue

        for j, evidence in enumerate(d['graph']['edges'][i]['metadata']['evidences']):
            if EXPERIMENT_CONTEXT not in evidence:
                continue

            # ctx = {k.strip().lower(): v.strip() for k, v in evidence[EXPERIMENT_CONTEXT].items() if v.strip()}

            new_context = {}

            for key, value in evidence[EXPERIMENT_CONTEXT].items():
                value = value.strip()

                if not value:
                    continue

                key = key.strip().lower()

                if key == 'species_common_name':
                    new_context['Species'] = species_map[value.lower()]
                elif key in annotation_map:
                    new_context[annotation_map[key]] = value
                else:
                    new_context[key] = value

            '''
            for k, v in annotation_map.items():
                if k not in ctx:
                    continue

                d['graph']['edges'][i]['metadata']['evidences'][j][EXPERIMENT_CONTEXT][v] = ctx[k]
                del d['graph']['edges'][i]['metadata']['evidences'][j][EXPERIMENT_CONTEXT][k]

            if 'species_common_name' in ctx:
                species_name = ctx['species_common_name'].strip().lower()
                d['graph']['edges'][i]['metadata']['evidences'][j][EXPERIMENT_CONTEXT]['Species'] = species_map[
                    species_name]
                del d['graph']['edges'][i]['metadata']['evidences'][j][EXPERIMENT_CONTEXT][
                    'species_common_name']
            '''

            d['graph']['edges'][i]['metadata']['evidences'][j][EXPERIMENT_CONTEXT] = new_context
    return d

def from_cbn_jgif(graph_jgif_dict):
    """Maps the JGIF used by the Causal Biological Network Database to standard namespace and annotations, then
    builds a BEL graph using :func:`pybel.from_jgif`.

    :param dict graph_jgif_dict: The JSON object representing the graph in JGIF format
    :return: A BEL graph
    :rtype: BELGraph

    Example:

    >>> import requests
    >>> from pybel import from_cbn_jgif
    >>> apoptosis_url = 'http://causalbionet.com/Networks/GetJSONGraphFile?networkId=810385422'
    >>> graph_jgif_dict = requests.get(apoptosis_url).json()
    >>> graph = from_cbn_jgif(graph_jgif_dict)
    """
    graph_jgif_dict = map_cbn(graph_jgif_dict)

    graph_jgif_dict['graph']['metadata'].update({
        METADATA_AUTHORS: 'Causal Biological Networks Database',
        METADATA_LICENSES: """
        Please cite:

        - www.causalbionet.com
        - https://bionet.sbvimprover.com

        as well as any relevant publications.

        The sbv IMPROVER project, the website and the Symposia are part of a collaborative project
        designed to enable scientists to learn about and contribute to the development of a new crowd
        sourcing method for verification of scientific data and results. The current challenges, website
        and biological network models were developed and are maintained as part of a collaboration among
        Selventa, OrangeBus and ADS. The project is led and funded by Philip Morris International. For more
        information on the focus of Philip Morris International’s research, please visit www.pmi.com.
        """.replace('\n', '\t')
    })

    graph = from_jgif(graph_jgif_dict)

    graph.namespace_url.update({
        'HGNC': 'https://arty.scai.fraunhofer.de/artifactory/bel/namespace/hgnc-human-genes/hgnc-human-genes-20150601.belns',
        'GOBP': 'https://arty.scai.fraunhofer.de/artifactory/bel/namespace/go-biological-process/go-biological-process-20150601.belns',
        'SFAM': 'https://arty.scai.fraunhofer.de/artifactory/bel/namespace/selventa-protein-families/selventa-protein-families-20150601.belns',
    })

    graph.annotation_url.update({
        'Cell': 'https://arty.scai.fraunhofer.de/artifactory/bel/annotation/cell/cell-line-20150601.belanno',
        'Disease': 'https://arty.scai.fraunhofer.de/artifactory/bel/annotation/disease/disease-20150601.belanno',
        'Species': 'https://arty.scai.fraunhofer.de/artifactory/bel/annotation/species-taxonomy-id/species-taxonomy-id-20170511.belanno',
        'Tissue': 'https://arty.scai.fraunhofer.de/artifactory/bel/annotation/mesh-anatomy/mesh-anatomy-20150601.belanno',
    })

    return graph

def from_jgif(graph_jgif_dict):
    """Builds a BEL graph from a JGIF JSON object.

    :param dict graph_jgif_dict: The JSON object representing the graph in JGIF format
    :return: A BEL graph
    :rtype: BELGraph
    """
    graph = BELGraph()

    root = graph_jgif_dict['graph']

    if 'label' in root:
        graph.name = root['label']

    if 'metadata' in root:
        metadata = root['metadata']

        for key in METADATA_INSERT_KEYS:
            if key in metadata:
                graph.document[key] = metadata[key]

    parser = BelParser(graph)
    parser.bel_term.addParseAction(parser.handle_term)

    for node in root['nodes']:
        if 'label' not in node:
            log.warning('node missing label: %s', node)
        node_label = node['label']
        parser.bel_term.parseString(node_label)

    for i, edge in enumerate(root['edges']):

        if edge['relation'] in {'actsIn'}:
            continue  # don't need legacy BEL format

        if edge['relation'] in UNQUALIFIED_EDGES:
            pass

        if not edge['relation'] in UNQUALIFIED_EDGES and (
                        'evidences' not in edge['metadata'] or not edge['metadata']['evidences']):
            log.debug('No evidence for edge %s', edge['label'])
            continue

        for evidence in edge['metadata']['evidences']:
            if 'citation' not in evidence or not evidence['citation']:
                continue

            if 'type' not in evidence['citation'] and 'id' not in evidence['citation']:
                continue

            summary_text = evidence['summary_text'].strip().strip('"')

            if not summary_text or summary_text == placeholder_evidence:
                continue

            parser.control_parser.clear()
            parser.control_parser.citation = get_citation(evidence)
            parser.control_parser.evidence = summary_text
            parser.control_parser.annotations.update(evidence[EXPERIMENT_CONTEXT])

            bel_statement = edge['label']

            try:
                parser.parseString(bel_statement, i)
            except Exception as e:
                log.warning('Error %s for %s', e, bel_statement)

    return graph

def to_jgif(graph):
    """Builds a JGIF dictionary from a BEL graph.

    :param BELGraph graph: A BEL graph
    :return: A JGIF dictionary
    :rtype: dict

    .. warning::

        Untested! This format is not general purpose and is therefore time is not heavily invested. If you want to
        use Cytoscape.js, we suggest using :func:`pybel.to_cx` instead.

    Example:

    >>> import pybel, os, json
    >>> graph_url = 'https://arty.scai.fraunhofer.de/artifactory/bel/knowledge/selventa-small-corpus/selventa-small-corpus-20150611.bel'
    >>> graph = pybel.from_url(graph_url)
    >>> graph_jgif_json = pybel.to_jgif(graph)
    >>> with open(os.path.expanduser('~/Desktop/small_corpus.json'), 'w') as f:
    ...     json.dump(graph_jgif_json, f)
    """
    node_to_bel = {}
    u_v_r_bel = {}

    nodes_entry = []
    edges_entry = []

    for i, node in enumerate(graph.nodes_iter()):
        bel = decanonicalize_node(graph, node)
        node_to_bel[node] = bel

        nodes_entry.append({
            'id': bel,
            'label': bel,
            'nodeId': i,
            'bel_function_type': graph.node[node][FUNCTION],
            'metadata': {}
        })

    for u, v in graph.edges_iter():
        relation_evidences = defaultdict(list)

        for k, d in graph.edge[u][v].items():

            if (u, v, d[RELATION]) not in u_v_r_bel:
                u_v_r_bel[u, v, d[RELATION]] = decanonicalize_edge(graph, u, v, k)

            bel = u_v_r_bel[u, v, d[RELATION]]

            evidence_dict = {
                'bel_statement': bel,
                'experiment_context': d[ANNOTATIONS],
            }

            if EVIDENCE in d:
                evidence_dict['summary_text'] = d[EVIDENCE]

            if CITATION in d:
                evidence_dict['citation'] = d[CITATION]

            relation_evidences[d[RELATION]].append(evidence_dict)

        for relation, evidences in relation_evidences.items():
            edges_entry.append({
                'source': node_to_bel[u],
                'target': node_to_bel[v],
                'relation': relation,
                'label': u_v_r_bel[u, v, relation],
                'metadata': {
                    'evidences': evidences
                }
            })

    return {
        'graph': {
            'metadata': graph.document,
            'nodes': nodes_entry,
            'edges': edges_entry
        }
    }
<|endoftext|>


<commit_before>from plex.objects.core.base import Property
from plex.objects.directory import Directory
from plex.objects.library.metadata.album import Album
from plex.objects.library.metadata.artist import Artist
from plex.objects.library.metadata.base import Metadata
from plex.objects.mixins.scrobble import ScrobbleMixin
from plex.objects.mixins.session import SessionMixin

class Track(Directory, Metadata, SessionMixin, ScrobbleMixin):
    artist = Property(resolver=lambda: Track.construct_artist)
    album = Property(resolver=lambda: Track.construct_album)

    index = Property(type=int)

    duration = Property(type=int)
    view_offset = Property('viewOffset', type=int)

    @staticmethod
    def construct_artist(client, node):
        attribute_map = {
            'key':          'grandparentKey',
            'ratingKey':    'grandparentRatingKey',

            'title':        'grandparentTitle',

            'thumb':        'grandparentThumb'
        }

        return Artist.construct(client, node, attribute_map, child=True)

    @staticmethod
    def construct_album(client, node):
        attribute_map = {
            'index':        'parentIndex',
            'key':          'parentKey',
            'ratingKey':    'parentRatingKey',

            'title':        'parentTitle',
            'year':         'parentYear',

            'thumb':        'parentThumb'
        }

        return Album.construct(client, node, attribute_map, child=True)
<commit_msg>Added missing "view_count" property to the `Track` object<commit_after>from plex.objects.core.base import Property
from plex.objects.directory import Directory
from plex.objects.library.metadata.album import Album
from plex.objects.library.metadata.artist import Artist
from plex.objects.library.metadata.base import Metadata
from plex.objects.mixins.scrobble import ScrobbleMixin
from plex.objects.mixins.session import SessionMixin

class Track(Directory, Metadata, SessionMixin, ScrobbleMixin):
    artist = Property(resolver=lambda: Track.construct_artist)
    album = Property(resolver=lambda: Track.construct_album)

    index = Property(type=int)

    view_count = Property('viewCount', type=int)
    view_offset = Property('viewOffset', type=int)

    duration = Property(type=int)

    @staticmethod
    def construct_artist(client, node):
        attribute_map = {
            'key':          'grandparentKey',
            'ratingKey':    'grandparentRatingKey',

            'title':        'grandparentTitle',

            'thumb':        'grandparentThumb'
        }

        return Artist.construct(client, node, attribute_map, child=True)

    @staticmethod
    def construct_album(client, node):
        attribute_map = {
            'index':        'parentIndex',
            'key':          'parentKey',
            'ratingKey':    'parentRatingKey',

            'title':        'parentTitle',
            'year':         'parentYear',

            'thumb':        'parentThumb'
        }

        return Album.construct(client, node, attribute_map, child=True)
<|endoftext|>


<commit_before># -*- coding: utf-8 -*-
from attr.exceptions import FrozenInstanceError
from collections import (MutableSequence, MutableMapping, OrderedDict,
                         MutableSet)

DEFAULT_DATE_FORMAT = "%Y-%m-%d"
DEFAULT_DATETIME_FORMAT = "ISO_FORMAT"
DEFAULT_TIME_FORMAT = "%H:%M:%S"

class ImmutableDict(dict):

    def __setitem__(self, key, value):
        raise FrozenInstanceError()

    def __delitem__(self, key):
        raise FrozenInstanceError()

    def __setattr__(self, name, value):
        raise FrozenInstanceError()

    def __delattr__(self, name):
        raise FrozenInstanceError()

    def pop(self, key, **kwargs):
        raise FrozenInstanceError()

    def clear(self):
        raise FrozenInstanceError()

class TypedSequence(MutableSequence):
    """
    Custom list type that checks the instance type of new values.

    reference:
    http://stackoverflow.com/a/3488283
    """

    def __init__(self, cls, args, allow_none=True):
        self.cls = cls
        self.allowed_types = (cls, type(None)) if allow_none else cls
        self.list = []
        self.extend(args)

    def __str__(self):
        return str(self.list)

    def __repr__(self):
        return repr(self.list)

    def __len__(self):
        return len(self.list)

    def __eq__(self, other):
        if isinstance(other, TypedSequence):
            return self.list == other.list and self.cls == other.cls
        else:
            return self.list == other

    def __getitem__(self, i):
        return self.list[i]

    def __delitem__(self, i):
        del self.list[i]

    def __setitem__(self, i, v):
        self._check(v)
        self.list[i] = v

    def insert(self, i, v):
        self._check(v)
        self.list.insert(i, v)

    def _check(self, v):
        if not isinstance(v, self.allowed_types):
            raise TypeError("Invalid value %s (%s != %s)" %
                            (v, type(v), self.cls))

class TypedMapping(MutableMapping):
    """
    Custom dict type that checks the instance type of new values.

    reference:
    http://stackoverflow.com/a/3488283
    """

    def __init__(self, cls, kwargs, key=None, allow_none=True):
        self.cls = cls
        self.allowed_types = (cls, type(None)) if allow_none else cls
        self.key = key
        self.dict = OrderedDict()
        self.update(kwargs)

    def __str__(self):
        return str(self.dict)

    def __repr__(self):
        return repr(self.dict)

    def __len__(self):
        return len(self.dict)

    def __iter__(self):
        return iter(self.dict)

    def __eq__(self, other):
        if isinstance(other, TypedMapping):
            return self.dict == other.dict and self.cls == other.cls
        else:
            return self.dict == other

    def __getitem__(self, i):
        return self.dict[i]

    def __delitem__(self, i):
        del self.dict[i]

    def __setitem__(self, i, v):
        self._check(v)
        self.dict[i] = v

    def add(self, v, key=None):
        if key is not None:
            self[key] = v

        elif self.key and hasattr(v, self.key):
            self[getattr(v, self.key)] = v  # pragma: no cover

        else:
            raise TypeError("No key set in instance or provided in call.")

    def _check(self, v):
        if not isinstance(v, self.allowed_types):
            raise TypeError("%s is not an instance of %s" % (v, self.cls))

class TypedSet(MutableSet):
    """
    Custom set type that checks the instance type of new values.

    reference:
    http://stackoverflow.com/a/3488283
    """

    def __init__(self, cls, args, allow_none=True):
        self.cls = cls
        self.allowed_types = (cls, type(None)) if allow_none else cls
        self.set = set()
        for arg in args or []:
            self.add(arg)

    def __str__(self):
        return str(self.set)

    def __repr__(self):
        return repr(self.set)

    def __len__(self):
        return len(self.set)

    def __eq__(self, other):
        if isinstance(other, TypedSet):
            return self.set == other.set and self.cls == other.cls
        else:
            return self.set == other

    def __iter__(self):
        return iter(self.set)

    def __contains__(self, item):
        return item in self.set

    def add(self, v):
        self._check(v)
        self.set.add(v)

    def discard(self, value):
        self.set.discard(value)

    def _check(self, v):
        if not isinstance(v, self.allowed_types):
            raise TypeError("Invalid value %s (%s != %s)" %
                            (v, type(v), self.cls))
<commit_msg>Issue #27<commit_after># -*- coding: utf-8 -*-
from attr.exceptions import FrozenInstanceError

try:
    from collections.abc import (MutableSequence, MutableMapping, OrderedDict,
                                 MutableSet)
except ImportError:
    from collections import (MutableSequence, MutableMapping, OrderedDict,
                             MutableSet)

DEFAULT_DATE_FORMAT = "%Y-%m-%d"
DEFAULT_DATETIME_FORMAT = "ISO_FORMAT"
DEFAULT_TIME_FORMAT = "%H:%M:%S"

class ImmutableDict(dict):

    def __setitem__(self, key, value):
        raise FrozenInstanceError()

    def __delitem__(self, key):
        raise FrozenInstanceError()

    def __setattr__(self, name, value):
        raise FrozenInstanceError()

    def __delattr__(self, name):
        raise FrozenInstanceError()

    def pop(self, key, **kwargs):
        raise FrozenInstanceError()

    def clear(self):
        raise FrozenInstanceError()

class TypedSequence(MutableSequence):
    """
    Custom list type that checks the instance type of new values.

    reference:
    http://stackoverflow.com/a/3488283
    """

    def __init__(self, cls, args, allow_none=True):
        self.cls = cls
        self.allowed_types = (cls, type(None)) if allow_none else cls
        self.list = []
        self.extend(args)

    def __str__(self):
        return str(self.list)

    def __repr__(self):
        return repr(self.list)

    def __len__(self):
        return len(self.list)

    def __eq__(self, other):
        if isinstance(other, TypedSequence):
            return self.list == other.list and self.cls == other.cls
        else:
            return self.list == other

    def __getitem__(self, i):
        return self.list[i]

    def __delitem__(self, i):
        del self.list[i]

    def __setitem__(self, i, v):
        self._check(v)
        self.list[i] = v

    def insert(self, i, v):
        self._check(v)
        self.list.insert(i, v)

    def _check(self, v):
        if not isinstance(v, self.allowed_types):
            raise TypeError("Invalid value %s (%s != %s)" %
                            (v, type(v), self.cls))

class TypedMapping(MutableMapping):
    """
    Custom dict type that checks the instance type of new values.

    reference:
    http://stackoverflow.com/a/3488283
    """

    def __init__(self, cls, kwargs, key=None, allow_none=True):
        self.cls = cls
        self.allowed_types = (cls, type(None)) if allow_none else cls
        self.key = key
        self.dict = OrderedDict()
        self.update(kwargs)

    def __str__(self):
        return str(self.dict)

    def __repr__(self):
        return repr(self.dict)

    def __len__(self):
        return len(self.dict)

    def __iter__(self):
        return iter(self.dict)

    def __eq__(self, other):
        if isinstance(other, TypedMapping):
            return self.dict == other.dict and self.cls == other.cls
        else:
            return self.dict == other

    def __getitem__(self, i):
        return self.dict[i]

    def __delitem__(self, i):
        del self.dict[i]

    def __setitem__(self, i, v):
        self._check(v)
        self.dict[i] = v

    def add(self, v, key=None):
        if key is not None:
            self[key] = v

        elif self.key and hasattr(v, self.key):
            self[getattr(v, self.key)] = v  # pragma: no cover

        else:
            raise TypeError("No key set in instance or provided in call.")

    def _check(self, v):
        if not isinstance(v, self.allowed_types):
            raise TypeError("%s is not an instance of %s" % (v, self.cls))

class TypedSet(MutableSet):
    """
    Custom set type that checks the instance type of new values.

    reference:
    http://stackoverflow.com/a/3488283
    """

    def __init__(self, cls, args, allow_none=True):
        self.cls = cls
        self.allowed_types = (cls, type(None)) if allow_none else cls
        self.set = set()
        for arg in args or []:
            self.add(arg)

    def __str__(self):
        return str(self.set)

    def __repr__(self):
        return repr(self.set)

    def __len__(self):
        return len(self.set)

    def __eq__(self, other):
        if isinstance(other, TypedSet):
            return self.set == other.set and self.cls == other.cls
        else:
            return self.set == other

    def __iter__(self):
        return iter(self.set)

    def __contains__(self, item):
        return item in self.set

    def add(self, v):
        self._check(v)
        self.set.add(v)

    def discard(self, value):
        self.set.discard(value)

    def _check(self, v):
        if not isinstance(v, self.allowed_types):
            raise TypeError("Invalid value %s (%s != %s)" %
                            (v, type(v), self.cls))
<|endoftext|>


<commit_before>__copyright__ = "Copyright 2017 Birkbeck, University of London"
__author__ = "Martin Paul Eve & Andy Byers"
__license__ = "AGPL v3"
__maintainer__ = "Birkbeck Centre for Technology and Publishing"

from django.db import models
from django.utils import timezone
from django.db.models import Max, Q, Value
from django.core.validators import MinValueValidator, MaxValueValidator
from django.contrib.contenttypes.models import ContentType

from utils import shared

assignment_choices = (
    ('editor', 'Editor'),
    ('section-editor', 'Section Editor'),
)

class EditorAssignment(models.Model):
    article = models.ForeignKey('submission.Article')
    editor = models.ForeignKey('core.Account')
    editor_type = models.CharField(max_length=20, choices=assignment_choices)
    assigned = models.DateTimeField(default=timezone.now)
    notified = models.BooleanField(default=False)

    class Meta:
        unique_together = ('article', 'editor')

def review_decision():
    return (
        ('accept', 'Accept Without Revisions'),
        ('minor_revisions', 'Minor Revisions Required'),
        ('major_revisions', 'Major Revisions Required'),
        ('reject', 'Reject'),
    )

def review_type():
    return (
        ('traditional', 'Traditional'),
        #('annotation', 'Annotation'),
    )

def review_visibilty():
    return (
        ('open', 'Open'),
        ('blind', 'Single Blind'),
        ('double-blind', 'Double Blind')
    )

class ReviewRound(models.Model):
    article = models.ForeignKey('submission.Article')
    round_number = models.IntegerField()
    review_files = models.ManyToManyField('core.File')
    date_started = models.DateTimeField(auto_now_add=True)

    class Meta:
        unique_together = ('article', 'round_number')
        ordering = ('-round_number',)

    def __str__(self):
        return u'%s - %s round_number: %s' % (self.pk, self.article.title, self.round_number)

    def __repr__(self):
        return u'%s - %s round number: %s' % (self.pk, self.article.title, self.round_number)

    def active_reviews(self):
        return self.reviewassignment_set.exclude(
            Q(date_declined__isnull=False) | Q(date_accepted__isnull=True) | Q(decision='withdrawn')
        )

    def inactive_reviews(self):
        return self.reviewassignment_set.filter(
            Q(date_declined__isnull=False) | Q(date_accepted__isnull=True) | Q(decision='withdrawn')
        ).order_by(
            'decision',
        )

    @classmethod
    def latest_article_round(cls, article):
        """ Works out and returns the latest article review round
        MS: I'm still not quite sure why it works but it does
        the round with a single query:
            SELECT "review_reviewround"."*"
            "FROM "review_reviewround"
            WHERE ("review_reviewround"."article_id" = {id}
            AND "review_reviewround"."round_number" = (
                SELECT MAX(U0."round_number") AS "latest_round"
                FROM "review_reviewround" U0 WHERE U0."article_id" = {id})
            )
            ORDER BY "review_reviewround"."round_number" DESC

        """
        latest_round = cls.objects.filter( article=article,).annotate(
            # Annotate all rows with the same value to force a group by
            constant=Value(1),
        ).values("constant").annotate(
            latest_round=Max('round_number'),
        ).values("latest_round")

        return cls.objects.get(article=article, round_number=latest_round)

class ReviewAssignment(models.Model):
    # FKs
    article = models.ForeignKey('submission.Article')
    reviewer = models.ForeignKey('core.Account', related_name='reviewer', help_text='User to undertake the review',
                                 null=True, on_delete=models.SET_NULL)
    editor = models.ForeignKey('core.Account', related_name='editor', help_text='Editor requesting the review',
                               null=True, on_delete=models.SET_NULL)

    # Info
    review_round = models.ForeignKey(ReviewRound, blank=True, null=True)
    decision = models.CharField(
        max_length=20,
        blank=True,
        null=True,
        choices=review_decision(),
        verbose_name='Recommendation',
    )
    competing_interests = models.TextField(blank=True, null=True,
                                           help_text="If any of the authors or editors "
                                                     "have any competing interests please add them here. "
                                                     "EG. 'This study was paid for by corp xyz.'.")
    review_type = models.CharField(max_length=20, choices=review_type(), default='traditional',
                                   help_text='Currently only traditional, form based, review is available.')
    visibility = models.CharField(max_length=20, choices=review_visibilty(), default='double-blind')
    form = models.ForeignKey('ReviewForm', null=True, on_delete=models.SET_NULL)
    access_code = models.CharField(max_length=100, blank=True, null=True)

    # Dates
    date_requested = models.DateTimeField(auto_now_add=True)
    date_due = models.DateField()
    date_accepted = models.DateTimeField(blank=True, null=True)
    date_declined = models.DateTimeField(blank=True, null=True)
    date_complete = models.DateTimeField(blank=True, null=True)
    date_reminded = models.DateField(blank=True, null=True)

    is_complete = models.BooleanField(default=False)
    for_author_consumption = models.BooleanField(default=False)

    suggested_reviewers = models.TextField(blank=True, null=True)
    comments_for_editor = models.TextField(blank=True, null=True,
                                           help_text="If you have any comments for the Editor you can add them here; \
                                           these will not be shared with the Author.",
                                           verbose_name="Comments for the Editor")
    review_file = models.ForeignKey('core.File', blank=True, null=True)
    display_review_file = models.BooleanField(default=False)

    def review_form_answers(self):
        return ReviewAssignmentAnswer.objects.filter(assignment=self).order_by('frozen_element__order')

    def save_review_form(self, review_form, assignment):
        for k, v in review_form.cleaned_data.items():
            form_element = ReviewFormElement.objects.get(reviewform=assignment.form, pk=k)
            answer, _ = ReviewAssignmentAnswer.objects.update_or_create(
                assignment=self,
                original_element=form_element,
                defaults={
                    "author_can_see": form_element.default_visibility,
                    "answer": v,
                },
            )
            form_element.snapshot(answer)

    @property
    def review_rating(self):
        try:
            return ReviewerRating.objects.get(assignment=self)
        except ReviewerRating.DoesNotExist:
            return None

    @property
    def is_late(self):
        # test if the review itself is late
        if timezone.now().date() >= self.date_due:
            return True

        return False

    @property
    def task_is_late(self):
        if self.date_accepted is not None:
            return False
        else:
            # imports are here to avoid circular dependency
            from core import models as core_models
            from utils import workflow_tasks
            active_tasks = core_models.Task.objects.filter(
                Q(content_type=ContentType.objects.get_for_model(self.article)) & Q(object_id=self.article.pk) & Q(
                    completed__isnull=True) & Q(title=workflow_tasks.DO_REVIEW_TITLE) & Q(
                    assignees=self.reviewer))

            for task in active_tasks:
                if task.is_late:
                    return True

        return False

    @property
    def status(self):
        if self.decision == 'withdrawn':
            return {
                'code': 'withdrawn',
                'display': 'Withdrawn',
                'span_class': 'red',
                'date': '',
                'reminder': None,
            }
        elif self.date_complete:
            return {
                'code': 'complete',
                'display': 'Complete',
                'span_class': 'light-green',
                'date': shared.day_month(self.date_complete),
                'reminder': None,
            }
        elif self.date_accepted:
            return {
                'code': 'accept',
                'display': 'Yes',
                'span_class': 'green',
                'date': shared.day_month(self.date_accepted),
                'reminder': 'accepted',
            }
        elif self.date_declined:
            return {
                'code': 'declined',
                'display': 'No',
                'span_class': 'red',
                'date': shared.day_month(self.date_declined),
                'reminder': None,
            }
        else:
            return {
                'code': 'wait',
                'display': 'Wait',
                'span_class': 'amber',
                'date': '',
                'reminder': 'request',
            }

    def __str__(self):
        if self.reviewer:
            reviewer_name = self.reviewer.full_name()
        else:
            reviewer_name = "No reviewer"

        return u'{0} - Article: {1}, Reviewer: {2}'.format(
            self.id, self.article.title, reviewer_name)

class ReviewForm(models.Model):
    journal = models.ForeignKey('journal.Journal')

    name = models.CharField(max_length=200)
    slug = models.SlugField(max_length=200)

    intro = models.TextField(help_text="Message displayed at the start of the review form.")
    thanks = models.TextField(help_text="Message displayed after the reviewer is finished.")

    elements = models.ManyToManyField('ReviewFormElement')
    deleted = models.BooleanField(default=False)

    def __str__(self):
        return u'{0} - {1}'.format(self.id, self.name)

def element_kind_choices():
    return (
        ('text', 'Text Field'),
        ('textarea', 'Text Area'),
        ('check', 'Check Box'),
        ('select', 'Select'),
        ('email', 'Email'),
        ('upload', 'Upload'),
        ('date', 'Date'),
    )

def element_width_choices():
    return (
        ('large-4 columns', 'third'),
        ('large-6 columns', 'half'),
        ('large-12 columns', 'full'),
    )

class BaseReviewFormElement(models.Model):
    name = models.CharField(max_length=200)
    kind = models.CharField(max_length=50, choices=element_kind_choices())
    choices = models.CharField(max_length=1000, null=True, blank=True,
                               help_text='Seperate choices with the bar | character.')
    required = models.BooleanField(default=True)
    order = models.IntegerField()
    width = models.CharField(max_length=20, choices=element_width_choices())
    help_text = models.TextField(blank=True, null=True)

    default_visibility = models.BooleanField(default=True, help_text='If true, this setting will be available '
                                                                     'to the author automatically, if false it will'
                                                                     'be hidden to the author by default.')

    class Meta:
        ordering = ('order', 'name')
        abstract = True

    def __str__(self):
        return "Element: {0} ({1})".format(self.name, self.kind)

    def choices_list(self):
        if self.choices:
            return

class ReviewFormElement(BaseReviewFormElement):

    class Meta(BaseReviewFormElement.Meta):
        pass

    def snapshot(self, answer):
        frozen , _= FrozenReviewFormElement.objects.update_or_create(
            answer=answer,
            defaults=dict(
                form_element=self,
                name=self.name,
                kind=self.kind,
                choices=self.choices,
                required=self.required,
                order=self.order,
                width=self.width,
                help_text=self.help_text,
                default_visibility=self.default_visibility,
            )
        )
        return frozen

class ReviewAssignmentAnswer(models.Model):
    assignment = models.ForeignKey(ReviewAssignment)
    original_element = models.ForeignKey(
        ReviewFormElement, null=True, on_delete=models.SET_NULL)
    answer = models.TextField(blank=True, null=True)
    edited_answer = models.TextField(null=True, blank=True)
    author_can_see = models.BooleanField(default=True)

    def __str__(self):
        return "{0}, {1}".format(self.assignment, self.element)

    @property
    def element(self):
        return self.frozen_element

class FrozenReviewFormElement(BaseReviewFormElement):
    """ A snapshot of a review form element at the time an answer is created"""
    form_element = models.ForeignKey(
        ReviewFormElement, null=True, on_delete=models.SET_NULL)
    answer = models.OneToOneField(
        ReviewAssignmentAnswer, related_name="frozen_element")

    class Meta(BaseReviewFormElement.Meta):
        pass

class ReviewFormAnswer(models.Model):
    review_assignment = models.ForeignKey(ReviewAssignment)
    form_element = models.ForeignKey(ReviewFormElement)
    answer = models.TextField()

class ReviewerRating(models.Model):
    assignment = models.OneToOneField(ReviewAssignment)
    rating = models.IntegerField(validators=[MinValueValidator(1),
                                             MaxValueValidator(10)])
    rater = models.ForeignKey('core.Account')

    def __str__(self):
        return "Reviewer: {0}, Article: {1}, Rating: {2}".format(
            self.assignment.reviewer.full_name(), self.assignment.article.title, self.rating
        )

class RevisionAction(models.Model):
    text = models.TextField()
    logged = models.DateTimeField(default=None, null=True, blank=True)
    user = models.ForeignKey('core.Account')

    def __str__(self):
        return "Revision Action by {0}: {1}".format(self.user.full_name(), self.text)

def revision_type():
    return (
        ('minor_revisions', 'Minor Revisions'),
        ('major_revisions', 'Major Revisions'),
    )

class RevisionRequest(models.Model):
    article = models.ForeignKey('submission.Article')
    editor = models.ForeignKey('core.Account')
    editor_note = models.TextField()  # Note from Editor to Author
    author_note = models.TextField(blank=True, null=True, verbose_name="Covering Letter")  # Note from Author to Editor
    actions = models.ManyToManyField(RevisionAction)  # List of actions Author took during Revision Request
    type = models.CharField(max_length=20, choices=revision_type(), default='minor_revisions')

    date_requested = models.DateField(default=timezone.now)
    date_due = models.DateField()
    date_completed = models.DateTimeField(blank=True, null=True)

    def __str__(self):
        return "Revision of {0} requested by {1}".format(self.article.title, self.editor.full_name())

class EditorOverride(models.Model):
    article = models.ForeignKey('submission.Article')
    editor = models.ForeignKey('core.Account')
    overwritten = models.DateTimeField(default=timezone.now)

    def __str__(self):
        return "{0} overrode their access to {1}".format(self.editor.full_name(), self.article.title)

class DecisionDraft(models.Model):
    article = models.ForeignKey('submission.Article')
    editor = models.ForeignKey('core.Account', related_name='draft_editor', null=True)
    section_editor = models.ForeignKey('core.Account', related_name='draft_section_editor')
    decision = models.CharField(
        max_length=100,
        choices=review_decision(),
        verbose_name='Draft Decision',
    )
    message_to_editor = models.TextField(
        null=True,
        blank=True,
        help_text='This is the email that will be sent to the editor notifying them that you are '
                  'logging your draft decision.',
        verbose_name='Email to Editor',
    )
    email_message = models.TextField(
        null=True,
        blank=True,
        help_text='This is a draft of the email that will be sent to the author. Your editor will check this.',
        verbose_name='Draft Email to Author',
    )
    drafted = models.DateTimeField(auto_now=True)

    editor_decision = models.CharField(
        max_length=20,
        choices=(('accept', 'Accept'), ('decline', 'Decline')),
        null=True,
        blank=True,
    )
    revision_request_due_date = models.DateTimeField(
        blank=True,
        null=True,
        help_text="Stores a due date for a Drafted Revision Request.",
    )
    editor_decline_rationale = models.TextField(
        null=True,
        blank=True,
        help_text="Provide the section editor with a rationale for declining their drafted decision.",
        verbose_name="Rationale for Declining Draft Decision",
    )

    def __str__(self):
        return "{0}: {1}".format(self.article.title,
                                 self.decision)
<commit_msg>Update filters<commit_after>__copyright__ = "Copyright 2017 Birkbeck, University of London"
__author__ = "Martin Paul Eve & Andy Byers"
__license__ = "AGPL v3"
__maintainer__ = "Birkbeck Centre for Technology and Publishing"

from django.db import models
from django.utils import timezone
from django.db.models import Max, Q, Value
from django.core.validators import MinValueValidator, MaxValueValidator
from django.contrib.contenttypes.models import ContentType

from utils import shared

assignment_choices = (
    ('editor', 'Editor'),
    ('section-editor', 'Section Editor'),
)

class EditorAssignment(models.Model):
    article = models.ForeignKey('submission.Article')
    editor = models.ForeignKey('core.Account')
    editor_type = models.CharField(max_length=20, choices=assignment_choices)
    assigned = models.DateTimeField(default=timezone.now)
    notified = models.BooleanField(default=False)

    class Meta:
        unique_together = ('article', 'editor')

def review_decision():
    return (
        ('accept', 'Accept Without Revisions'),
        ('minor_revisions', 'Minor Revisions Required'),
        ('major_revisions', 'Major Revisions Required'),
        ('reject', 'Reject'),
    )

def review_type():
    return (
        ('traditional', 'Traditional'),
        #('annotation', 'Annotation'),
    )

def review_visibilty():
    return (
        ('open', 'Open'),
        ('blind', 'Single Blind'),
        ('double-blind', 'Double Blind')
    )

class ReviewRound(models.Model):
    article = models.ForeignKey('submission.Article')
    round_number = models.IntegerField()
    review_files = models.ManyToManyField('core.File')
    date_started = models.DateTimeField(auto_now_add=True)

    class Meta:
        unique_together = ('article', 'round_number')
        ordering = ('-round_number',)

    def __str__(self):
        return u'%s - %s round_number: %s' % (self.pk, self.article.title, self.round_number)

    def __repr__(self):
        return u'%s - %s round number: %s' % (self.pk, self.article.title, self.round_number)

    def active_reviews(self):
        return self.reviewassignment_set.exclude(
            Q(date_declined__isnull=False) | Q(decision='withdrawn')
        ).order_by(
            '-decision',
        )

    def inactive_reviews(self):
        return self.reviewassignment_set.filter(
            Q(date_declined__isnull=False) | Q(decision='withdrawn')
        ).order_by(
            'decision',
        )

    @classmethod
    def latest_article_round(cls, article):
        """ Works out and returns the latest article review round
        MS: I'm still not quite sure why it works but it does
        the round with a single query:
            SELECT "review_reviewround"."*"
            "FROM "review_reviewround"
            WHERE ("review_reviewround"."article_id" = {id}
            AND "review_reviewround"."round_number" = (
                SELECT MAX(U0."round_number") AS "latest_round"
                FROM "review_reviewround" U0 WHERE U0."article_id" = {id})
            )
            ORDER BY "review_reviewround"."round_number" DESC

        """
        latest_round = cls.objects.filter( article=article,).annotate(
            # Annotate all rows with the same value to force a group by
            constant=Value(1),
        ).values("constant").annotate(
            latest_round=Max('round_number'),
        ).values("latest_round")

        return cls.objects.get(article=article, round_number=latest_round)

class ReviewAssignment(models.Model):
    # FKs
    article = models.ForeignKey('submission.Article')
    reviewer = models.ForeignKey('core.Account', related_name='reviewer', help_text='User to undertake the review',
                                 null=True, on_delete=models.SET_NULL)
    editor = models.ForeignKey('core.Account', related_name='editor', help_text='Editor requesting the review',
                               null=True, on_delete=models.SET_NULL)

    # Info
    review_round = models.ForeignKey(ReviewRound, blank=True, null=True)
    decision = models.CharField(
        max_length=20,
        blank=True,
        null=True,
        choices=review_decision(),
        verbose_name='Recommendation',
    )
    competing_interests = models.TextField(blank=True, null=True,
                                           help_text="If any of the authors or editors "
                                                     "have any competing interests please add them here. "
                                                     "EG. 'This study was paid for by corp xyz.'.")
    review_type = models.CharField(max_length=20, choices=review_type(), default='traditional',
                                   help_text='Currently only traditional, form based, review is available.')
    visibility = models.CharField(max_length=20, choices=review_visibilty(), default='double-blind')
    form = models.ForeignKey('ReviewForm', null=True, on_delete=models.SET_NULL)
    access_code = models.CharField(max_length=100, blank=True, null=True)

    # Dates
    date_requested = models.DateTimeField(auto_now_add=True)
    date_due = models.DateField()
    date_accepted = models.DateTimeField(blank=True, null=True)
    date_declined = models.DateTimeField(blank=True, null=True)
    date_complete = models.DateTimeField(blank=True, null=True)
    date_reminded = models.DateField(blank=True, null=True)

    is_complete = models.BooleanField(default=False)
    for_author_consumption = models.BooleanField(default=False)

    suggested_reviewers = models.TextField(blank=True, null=True)
    comments_for_editor = models.TextField(blank=True, null=True,
                                           help_text="If you have any comments for the Editor you can add them here; \
                                           these will not be shared with the Author.",
                                           verbose_name="Comments for the Editor")
    review_file = models.ForeignKey('core.File', blank=True, null=True)
    display_review_file = models.BooleanField(default=False)

    def review_form_answers(self):
        return ReviewAssignmentAnswer.objects.filter(assignment=self).order_by('frozen_element__order')

    def save_review_form(self, review_form, assignment):
        for k, v in review_form.cleaned_data.items():
            form_element = ReviewFormElement.objects.get(reviewform=assignment.form, pk=k)
            answer, _ = ReviewAssignmentAnswer.objects.update_or_create(
                assignment=self,
                original_element=form_element,
                defaults={
                    "author_can_see": form_element.default_visibility,
                    "answer": v,
                },
            )
            form_element.snapshot(answer)

    @property
    def review_rating(self):
        try:
            return ReviewerRating.objects.get(assignment=self)
        except ReviewerRating.DoesNotExist:
            return None

    @property
    def is_late(self):
        # test if the review itself is late
        if timezone.now().date() >= self.date_due:
            return True

        return False

    @property
    def task_is_late(self):
        if self.date_accepted is not None:
            return False
        else:
            # imports are here to avoid circular dependency
            from core import models as core_models
            from utils import workflow_tasks
            active_tasks = core_models.Task.objects.filter(
                Q(content_type=ContentType.objects.get_for_model(self.article)) & Q(object_id=self.article.pk) & Q(
                    completed__isnull=True) & Q(title=workflow_tasks.DO_REVIEW_TITLE) & Q(
                    assignees=self.reviewer))

            for task in active_tasks:
                if task.is_late:
                    return True

        return False

    @property
    def status(self):
        if self.decision == 'withdrawn':
            return {
                'code': 'withdrawn',
                'display': 'Withdrawn',
                'span_class': 'red',
                'date': '',
                'reminder': None,
            }
        elif self.date_complete:
            return {
                'code': 'complete',
                'display': 'Complete',
                'span_class': 'light-green',
                'date': shared.day_month(self.date_complete),
                'reminder': None,
            }
        elif self.date_accepted:
            return {
                'code': 'accept',
                'display': 'Yes',
                'span_class': 'green',
                'date': shared.day_month(self.date_accepted),
                'reminder': 'accepted',
            }
        elif self.date_declined:
            return {
                'code': 'declined',
                'display': 'No',
                'span_class': 'red',
                'date': shared.day_month(self.date_declined),
                'reminder': None,
            }
        else:
            return {
                'code': 'wait',
                'display': 'Wait',
                'span_class': 'amber',
                'date': '',
                'reminder': 'request',
            }

    def __str__(self):
        if self.reviewer:
            reviewer_name = self.reviewer.full_name()
        else:
            reviewer_name = "No reviewer"

        return u'{0} - Article: {1}, Reviewer: {2}'.format(
            self.id, self.article.title, reviewer_name)

class ReviewForm(models.Model):
    journal = models.ForeignKey('journal.Journal')

    name = models.CharField(max_length=200)
    slug = models.SlugField(max_length=200)

    intro = models.TextField(help_text="Message displayed at the start of the review form.")
    thanks = models.TextField(help_text="Message displayed after the reviewer is finished.")

    elements = models.ManyToManyField('ReviewFormElement')
    deleted = models.BooleanField(default=False)

    def __str__(self):
        return u'{0} - {1}'.format(self.id, self.name)

def element_kind_choices():
    return (
        ('text', 'Text Field'),
        ('textarea', 'Text Area'),
        ('check', 'Check Box'),
        ('select', 'Select'),
        ('email', 'Email'),
        ('upload', 'Upload'),
        ('date', 'Date'),
    )

def element_width_choices():
    return (
        ('large-4 columns', 'third'),
        ('large-6 columns', 'half'),
        ('large-12 columns', 'full'),
    )

class BaseReviewFormElement(models.Model):
    name = models.CharField(max_length=200)
    kind = models.CharField(max_length=50, choices=element_kind_choices())
    choices = models.CharField(max_length=1000, null=True, blank=True,
                               help_text='Seperate choices with the bar | character.')
    required = models.BooleanField(default=True)
    order = models.IntegerField()
    width = models.CharField(max_length=20, choices=element_width_choices())
    help_text = models.TextField(blank=True, null=True)

    default_visibility = models.BooleanField(default=True, help_text='If true, this setting will be available '
                                                                     'to the author automatically, if false it will'
                                                                     'be hidden to the author by default.')

    class Meta:
        ordering = ('order', 'name')
        abstract = True

    def __str__(self):
        return "Element: {0} ({1})".format(self.name, self.kind)

    def choices_list(self):
        if self.choices:
            return

class ReviewFormElement(BaseReviewFormElement):

    class Meta(BaseReviewFormElement.Meta):
        pass

    def snapshot(self, answer):
        frozen , _= FrozenReviewFormElement.objects.update_or_create(
            answer=answer,
            defaults=dict(
                form_element=self,
                name=self.name,
                kind=self.kind,
                choices=self.choices,
                required=self.required,
                order=self.order,
                width=self.width,
                help_text=self.help_text,
                default_visibility=self.default_visibility,
            )
        )
        return frozen

class ReviewAssignmentAnswer(models.Model):
    assignment = models.ForeignKey(ReviewAssignment)
    original_element = models.ForeignKey(
        ReviewFormElement, null=True, on_delete=models.SET_NULL)
    answer = models.TextField(blank=True, null=True)
    edited_answer = models.TextField(null=True, blank=True)
    author_can_see = models.BooleanField(default=True)

    def __str__(self):
        return "{0}, {1}".format(self.assignment, self.element)

    @property
    def element(self):
        return self.frozen_element

class FrozenReviewFormElement(BaseReviewFormElement):
    """ A snapshot of a review form element at the time an answer is created"""
    form_element = models.ForeignKey(
        ReviewFormElement, null=True, on_delete=models.SET_NULL)
    answer = models.OneToOneField(
        ReviewAssignmentAnswer, related_name="frozen_element")

    class Meta(BaseReviewFormElement.Meta):
        pass

class ReviewFormAnswer(models.Model):
    review_assignment = models.ForeignKey(ReviewAssignment)
    form_element = models.ForeignKey(ReviewFormElement)
    answer = models.TextField()

class ReviewerRating(models.Model):
    assignment = models.OneToOneField(ReviewAssignment)
    rating = models.IntegerField(validators=[MinValueValidator(1),
                                             MaxValueValidator(10)])
    rater = models.ForeignKey('core.Account')

    def __str__(self):
        return "Reviewer: {0}, Article: {1}, Rating: {2}".format(
            self.assignment.reviewer.full_name(), self.assignment.article.title, self.rating
        )

class RevisionAction(models.Model):
    text = models.TextField()
    logged = models.DateTimeField(default=None, null=True, blank=True)
    user = models.ForeignKey('core.Account')

    def __str__(self):
        return "Revision Action by {0}: {1}".format(self.user.full_name(), self.text)

def revision_type():
    return (
        ('minor_revisions', 'Minor Revisions'),
        ('major_revisions', 'Major Revisions'),
    )

class RevisionRequest(models.Model):
    article = models.ForeignKey('submission.Article')
    editor = models.ForeignKey('core.Account')
    editor_note = models.TextField()  # Note from Editor to Author
    author_note = models.TextField(blank=True, null=True, verbose_name="Covering Letter")  # Note from Author to Editor
    actions = models.ManyToManyField(RevisionAction)  # List of actions Author took during Revision Request
    type = models.CharField(max_length=20, choices=revision_type(), default='minor_revisions')

    date_requested = models.DateField(default=timezone.now)
    date_due = models.DateField()
    date_completed = models.DateTimeField(blank=True, null=True)

    def __str__(self):
        return "Revision of {0} requested by {1}".format(self.article.title, self.editor.full_name())

class EditorOverride(models.Model):
    article = models.ForeignKey('submission.Article')
    editor = models.ForeignKey('core.Account')
    overwritten = models.DateTimeField(default=timezone.now)

    def __str__(self):
        return "{0} overrode their access to {1}".format(self.editor.full_name(), self.article.title)

class DecisionDraft(models.Model):
    article = models.ForeignKey('submission.Article')
    editor = models.ForeignKey('core.Account', related_name='draft_editor', null=True)
    section_editor = models.ForeignKey('core.Account', related_name='draft_section_editor')
    decision = models.CharField(
        max_length=100,
        choices=review_decision(),
        verbose_name='Draft Decision',
    )
    message_to_editor = models.TextField(
        null=True,
        blank=True,
        help_text='This is the email that will be sent to the editor notifying them that you are '
                  'logging your draft decision.',
        verbose_name='Email to Editor',
    )
    email_message = models.TextField(
        null=True,
        blank=True,
        help_text='This is a draft of the email that will be sent to the author. Your editor will check this.',
        verbose_name='Draft Email to Author',
    )
    drafted = models.DateTimeField(auto_now=True)

    editor_decision = models.CharField(
        max_length=20,
        choices=(('accept', 'Accept'), ('decline', 'Decline')),
        null=True,
        blank=True,
    )
    revision_request_due_date = models.DateTimeField(
        blank=True,
        null=True,
        help_text="Stores a due date for a Drafted Revision Request.",
    )
    editor_decline_rationale = models.TextField(
        null=True,
        blank=True,
        help_text="Provide the section editor with a rationale for declining their drafted decision.",
        verbose_name="Rationale for Declining Draft Decision",
    )

    def __str__(self):
        return "{0}: {1}".format(self.article.title,
                                 self.decision)
<|endoftext|>


<commit_before># Automatically generated by 'package.py' script.

import sys

VERSION = '2.6'
RELEASE = 'beta3'
TIMESTAMP = '20110707-221240'

def get_version(sep=' '):
    if RELEASE == 'final':
        return VERSION
    return VERSION + sep + RELEASE

def get_full_version(who=''):
    sys_version = sys.version.split()[0]
    version = '%s %s (%s %s on %s)' \
        % (who, get_version(), _get_interpreter(), sys_version, sys.platform)
    return version.strip()

def _get_interpreter():
    if sys.platform.startswith('java'):
        return 'Jython'
    if sys.platform == 'cli':
        return 'IronPython'
    return 'Python'
<commit_msg>back to trunk<commit_after># Automatically generated by 'package.py' script.

import sys

VERSION = 'trunk'
RELEASE = '20110707'
TIMESTAMP = '20110707-221632'

def get_version(sep=' '):
    if RELEASE == 'final':
        return VERSION
    return VERSION + sep + RELEASE

def get_full_version(who=''):
    sys_version = sys.version.split()[0]
    version = '%s %s (%s %s on %s)' \
        % (who, get_version(), _get_interpreter(), sys_version, sys.platform)
    return version.strip()

def _get_interpreter():
    if sys.platform.startswith('java'):
        return 'Jython'
    if sys.platform == 'cli':
        return 'IronPython'
    return 'Python'
<|endoftext|>


<commit_before># Automatically generated by 'package.py' script.

import sys

VERSION = 'trunk'
RELEASE = '20110623'
TIMESTAMP = '20110623-223004'

def get_version(sep=' '):
    if RELEASE == 'final':
        return VERSION
    return VERSION + sep + RELEASE

def get_full_version(who=''):
    sys_version = sys.version.split()[0]
    version = '%s %s (%s %s on %s)' \
        % (who, get_version(), _get_interpreter(), sys_version, sys.platform)
    return version.strip()

def _get_interpreter():
    if sys.platform.startswith('java'):
        return 'Jython'
    if sys.platform == 'cli':
        return 'IronPython'
    return 'Python'
<commit_msg>2.6beta2<commit_after># Automatically generated by 'package.py' script.

import sys

VERSION = '2.6'
RELEASE = 'beta2'
TIMESTAMP = '20110629-154450'

def get_version(sep=' '):
    if RELEASE == 'final':
        return VERSION
    return VERSION + sep + RELEASE

def get_full_version(who=''):
    sys_version = sys.version.split()[0]
    version = '%s %s (%s %s on %s)' \
        % (who, get_version(), _get_interpreter(), sys_version, sys.platform)
    return version.strip()

def _get_interpreter():
    if sys.platform.startswith('java'):
        return 'Jython'
    if sys.platform == 'cli':
        return 'IronPython'
    return 'Python'
<|endoftext|>


<commit_before># Copyright (c) 2009, Karoly Lorentey  <karoly@lorentey.hu>

import stagger

def verb(verbose, *args, **kwargs):
    if verbose:
        print(*args, **kwargs)

def check_tag_data(data):
    "Raise a ValueError if DATA doesn't seem to be a well-formed ID3 tag."
    if len(data) < 10:
        raise ValueError("Tag too short")
    if data[0:3] != b"ID3":
        raise ValueError("Missing ID3 identifier")
    if data[3] >= 5 or data[4] != 0:
        raise ValueError("Unknown ID3 version")
    length = stagger.conversions.Syncsafe.decode(data[6:10]) + 10
    if len(data) != length:
        raise ValueError("Tag size mismatch")

def get_raw_tag_data(filename):
    "Return the ID3 tag in FILENAME as a raw byte string."
    with open(filename, "rb") as file:
        try:
            (cls, offset, length) = stagger.tags.detect_tag(file)
        except stagger.NoTagError:
            return bytes()
        file.seek(offset)
        return file.read(length)

def set_raw_tag_data(filename, data, act=True, verbose=False):
    "Replace the ID3 tag in FILENAME with DATA."
    check_tag_data(data)
    with open(filename, "rb+") as file:
        try:
            (cls, offset, length) = stagger.tags.detect_tag(file)
        except stagger.NoTagError:
            (offset, length) = (0, 0)
        if length > 0:
            verb(verbose, "{0}: replaced tag with {1} bytes of data"
                 .format(filename, len(data)))
        else:
            verb(verbose, "{0}: created tag with {1} bytes of data"
                 .format(filename, len(data)))
        if act:
            stagger.fileutil.replace_chunk(file, offset, length, data)

def set_frames(filename, valuedict, act=True, verbose=False):
    try:
        tag = stagger.read_tag(filename)
    except stagger.NoTagError:
        verb(verbose, "{0}: new ID3v2.{1} tag"
             .format(filename, stagger.default_tag.version))
        tag = stagger.default_tag()
    for (key, value) in valuedict.items():
        if key.lower() in tag._friendly_names:
            # Use friendly name API
            setattr(tag, key.lower(), value)
            newval = repr(getattr(tag, key.lower()))
        else:
            # Use frameid API
            tag[key] = value
            newval = tag[key]
        verb(verbose, "{0}: {1}: set to {2}".format(filename, key, newval))
    if act:
        tag.write(filename)

def remove_frames(filename, frameids, act=True, verbose=False):
    try:
        tag = stagger.read_tag(filename)
    except stagger.NoTagError:
        verb(verbose, "{0}: no ID3 tag".format(filename))
        return

    for frameid in frameids:
        try:
            del tag[frameid]
            verb(verbose, "{0}: {1}: deleted".format(filename, frameid))
        except KeyError:
            verb(verbose, "{0}: {1}: not in file".format(filename, frameid))
    if act:
        tag.write(filename)
<commit_msg>stagger/util.py (set_frames): Fix friendly names with dashes.<commit_after># Copyright (c) 2009, Karoly Lorentey  <karoly@lorentey.hu>

import stagger

def verb(verbose, *args, **kwargs):
    if verbose:
        print(*args, **kwargs)

def check_tag_data(data):
    "Raise a ValueError if DATA doesn't seem to be a well-formed ID3 tag."
    if len(data) < 10:
        raise ValueError("Tag too short")
    if data[0:3] != b"ID3":
        raise ValueError("Missing ID3 identifier")
    if data[3] >= 5 or data[4] != 0:
        raise ValueError("Unknown ID3 version")
    length = stagger.conversions.Syncsafe.decode(data[6:10]) + 10
    if len(data) != length:
        raise ValueError("Tag size mismatch")

def get_raw_tag_data(filename):
    "Return the ID3 tag in FILENAME as a raw byte string."
    with open(filename, "rb") as file:
        try:
            (cls, offset, length) = stagger.tags.detect_tag(file)
        except stagger.NoTagError:
            return bytes()
        file.seek(offset)
        return file.read(length)

def set_raw_tag_data(filename, data, act=True, verbose=False):
    "Replace the ID3 tag in FILENAME with DATA."
    check_tag_data(data)
    with open(filename, "rb+") as file:
        try:
            (cls, offset, length) = stagger.tags.detect_tag(file)
        except stagger.NoTagError:
            (offset, length) = (0, 0)
        if length > 0:
            verb(verbose, "{0}: replaced tag with {1} bytes of data"
                 .format(filename, len(data)))
        else:
            verb(verbose, "{0}: created tag with {1} bytes of data"
                 .format(filename, len(data)))
        if act:
            stagger.fileutil.replace_chunk(file, offset, length, data)

def set_frames(filename, valuedict, act=True, verbose=False):
    try:
        tag = stagger.read_tag(filename)
    except stagger.NoTagError:
        verb(verbose, "{0}: new ID3v2.{1} tag"
             .format(filename, stagger.default_tag.version))
        tag = stagger.default_tag()
    for (key, value) in valuedict.items():
        if key.lower() in tag._friendly_names:
            # Use friendly name API
            key = key.lower().replace("-", "_")
            setattr(tag, key, value)
            newval = repr(getattr(tag, key))
        else:
            # Use frameid API
            tag[key] = value
            newval = tag[key]
        verb(verbose, "{0}: {1}: set to {2}".format(filename, key, newval))
    if act:
        tag.write(filename)

def remove_frames(filename, frameids, act=True, verbose=False):
    try:
        tag = stagger.read_tag(filename)
    except stagger.NoTagError:
        verb(verbose, "{0}: no ID3 tag".format(filename))
        return

    for frameid in frameids:
        try:
            del tag[frameid]
            verb(verbose, "{0}: {1}: deleted".format(filename, frameid))
        except KeyError:
            verb(verbose, "{0}: {1}: not in file".format(filename, frameid))
    if act:
        tag.write(filename)
<|endoftext|>


<commit_before>from flask.ext.appbuilder.models.datamodel import SQLAModel
from flask import g, request, url_for
from flask.ext.login import current_user

def app_template_filter(filter_name=''):
    def wrap(f):
        if not hasattr(f, '_filter'):
            f._filter = filter_name
        return f
    return wrap

class TemplateFilters(object):

    security_manager = None

    def __init__(self, app, security_manager):
        self.security_manager = security_manager
        for attr_name in dir(self):
            if hasattr(getattr(self, attr_name),'_filter'):
                attr = getattr(self, attr_name)
                app.jinja_env.filters[attr._filter] = attr

    @app_template_filter('link_order')
    def link_order_filter(self, column, generalview_name):
        """
        Arguments are passed like: _oc_<VIEW_NAME>=<COL_NAME>&_od_<VIEW_NAME>='asc'|'desc'
        """
        new_args = request.view_args.copy()
        args = request.args.copy()
        if ('_oc_' + generalview_name) in args:
            args['_oc_' + generalview_name] = column
            if args.get('_od_' + generalview_name) == 'asc':
                args['_od_' + generalview_name] = 'desc'
            else:
                args['_od_' + generalview_name] = 'asc'
        else:
            args['_oc_' + generalview_name] = column
            args['_od_' + generalview_name] = 'asc'
        return url_for(request.endpoint,**dict(new_args.items() + args.to_dict().items()))

    @app_template_filter('link_page')
    def link_page_filter(self, page, generalview_name):
        """
        Arguments are passed like: page_<VIEW_NAME>=<PAGE_NUMBER>
        """
        new_args = request.view_args.copy()
        args = request.args.copy()
        args['page_' + generalview_name] = page
        return url_for(request.endpoint,**dict(new_args.items() + args.to_dict().items()))

    @app_template_filter('get_link_next')
    def get_link_next_filter(self, s):
        return request.args.get('next')

    # to improve
    @app_template_filter('set_link_filters')
    def set_link_filters_filter(self, path, filters, pk):
        lnkstr = path

        for _filter in filters:
            lnkstr = lnkstr + '&_flt_' + _filter + '=' + str(pk)
        return lnkstr

    @app_template_filter('get_link_order')
    def get_link_order_filter(self, column, generalview_name):
        if request.args.get('_oc_' + generalview_name) == column:
            if (request.args.get('_od_' + generalview_name) == 'asc'):
                return 2
            else:
                return 1
        else:
            return 0

    @app_template_filter('get_attr')
    def get_attr_filter(self, obj, item):
        return getattr(obj, item)

    @app_template_filter('is_menu_visible')
    def is_menu_visible(self, item):
        if current_user.is_authenticated():
            if self.security_manager.is_menu_public(item) or self.security_manager.has_menu_access(g.user, item.name):
                return True
            else:
                return False
        else:
            if self.security_manager.is_menu_public(item.name):
                return True
            else:
                return False

    @app_template_filter('is_item_visible')
    def is_item_visible(self, permission, item):
        if current_user.is_authenticated():
            if self.security_manager.has_permission_on_view(g.user, permission, item):
                return True
            else:
                return False
        else:
            if self.security_manager.is_item_public(permission, item):
                return True
            else:
                return False
<commit_msg>Update filters.py<commit_after>from flask.ext.appbuilder.models.datamodel import SQLAModel
from flask import g, request, url_for
from flask.ext.login import current_user

def app_template_filter(filter_name=''):
    def wrap(f):
        if not hasattr(f, '_filter'):
            f._filter = filter_name
        return f
    return wrap

class TemplateFilters(object):

    security_manager = None

    def __init__(self, app, security_manager):
        self.security_manager = security_manager
        for attr_name in dir(self):
            if hasattr(getattr(self, attr_name),'_filter'):
                attr = getattr(self, attr_name)
                app.jinja_env.filters[attr._filter] = attr

    @app_template_filter('link_order')
    def link_order_filter(self, column, generalview_name):
        """
        Arguments are passed like: _oc_<VIEW_NAME>=<COL_NAME>&_od_<VIEW_NAME>='asc'|'desc'
        """
        new_args = request.view_args.copy()
        args = request.args.copy()
        if ('_oc_' + generalview_name) in args:
            args['_oc_' + generalview_name] = column
            if args.get('_od_' + generalview_name) == 'asc':
                args['_od_' + generalview_name] = 'desc'
            else:
                args['_od_' + generalview_name] = 'asc'
        else:
            args['_oc_' + generalview_name] = column
            args['_od_' + generalview_name] = 'asc'
        return url_for(request.endpoint,**dict(new_args.items() + args.to_dict().items()))

    @app_template_filter('link_page')
    def link_page_filter(self, page, generalview_name):
        """
        Arguments are passed like: page_<VIEW_NAME>=<PAGE_NUMBER>
        """
        new_args = request.view_args.copy()
        args = request.args.copy()
        args['page_' + generalview_name] = page
        return url_for(request.endpoint,**dict(new_args.items() + args.to_dict().items()))

    @app_template_filter('get_link_next')
    def get_link_next_filter(self, s):
        return request.args.get('next')

    # to improve
    @app_template_filter('set_link_filters')
    def set_link_filters_filter(self, path, filters, pk):
        lnkstr = path
        """
        for _filter in filters:
            lnkstr = lnkstr + '&_flt_' + _filter + '=' + str(pk)
        """
        return lnkstr

    @app_template_filter('get_link_order')
    def get_link_order_filter(self, column, generalview_name):
        if request.args.get('_oc_' + generalview_name) == column:
            if (request.args.get('_od_' + generalview_name) == 'asc'):
                return 2
            else:
                return 1
        else:
            return 0

    @app_template_filter('get_attr')
    def get_attr_filter(self, obj, item):
        return getattr(obj, item)

    @app_template_filter('is_menu_visible')
    def is_menu_visible(self, item):
        if current_user.is_authenticated():
            if self.security_manager.is_menu_public(item) or self.security_manager.has_menu_access(g.user, item.name):
                return True
            else:
                return False
        else:
            if self.security_manager.is_menu_public(item.name):
                return True
            else:
                return False

    @app_template_filter('is_item_visible')
    def is_item_visible(self, permission, item):
        if current_user.is_authenticated():
            if self.security_manager.has_permission_on_view(g.user, permission, item):
                return True
            else:
                return False
        else:
            if self.security_manager.is_item_public(permission, item):
                return True
            else:
                return False
<|endoftext|>


<commit_before>import argparse
import fnmatch
import json
import os
import sh
import subprocess

arc_diff = sh.Command._create('arc').bake("diff",
                                          allow_untracked=True,
                                          browse=True)
git_diff = sh.Command._create('git').bake("diff-tree",
                                          "--no-commit-id",
                                          "--name-only",
                                          "-r")
git_merge_base = sh.Command._create('git').bake("merge-base")

def list_changed_files(start, end=None):
    if end:
        commit_range = git_merge_base(start, end).split()[0]
        commit_range += "..%s" % (end,)
    else:
        commit_range = start
    return git_diff(commit_range).split()

def match_owners(owners_filepath, filename):
    if os.path.exists(owners_filepath):
        with open(owners_filepath, 'r') as f:
            raw_data = json.load(f)
            # handle simple file format
            if type(raw_data) == dict:
                return {
                    "reviewers": set(raw_data["reviewers"]),
                    "subscribers": set(raw_data["subscribers"])
                }
            else:
                assert type(raw_data) == list
                owners = {
                    "reviewers": set(),
                    "subscribers": set()
                }
                for data in raw_data:
                    patterns = data["includes"]
                    if any(fnmatch.fnmatch(filename, pat)
                           for pat in patterns):
                        cur_owners = {
                            "reviewers": set(data["reviewers"]),
                            "subscribers": set(data["subscribers"])
                        }
                        owners = merge_owners(owners, cur_owners)
                return owners
    else:
        return None

def merge_owners(owners1, owners2):
    return {
        "reviewers": owners1["reviewers"].union(owners2["reviewers"]),
        "subscribers": owners1["subscribers"].union(owners2["subscribers"])
    }

def get_owners(filename):
    cur_path = filename
    owners = {
        "reviewers": set(),
        "subscribers": set()
    }
    while cur_path:
        cur_dir = os.path.dirname(cur_path)
        owners_filepath = os.path.join(cur_dir, "OWNERS.json")
        cur_owners = match_owners(owners_filepath, filename)
        if cur_owners:
            owners = merge_owners(owners, cur_owners)
        # continue one level up
        cur_path = cur_dir
    return owners

def get_commit_owners(start, end=None):
    owners = {
        "reviewers": set(),
        "subscribers": set()
    }
    for f in list_changed_files(start, end):
        cur_owners = get_owners(f)
        if cur_owners:
            owners = merge_owners(owners, cur_owners)
    return owners

def create_review(start, end=None):
    owners = get_commit_owners(start, end)
    _arc_diff = arc_diff
    # take this off once done testing
    # only=True)
    if end != "HEAD":
        _arc_diff = _arc_diff.bake(head=end)
    reviewers = ','.join(owners["reviewers"])
    subs = ','.join(owners["subscribers"])
    if subs:
        _arc_diff = _arc_diff.bake(cc=subs)
    subprocess.call(str(_arc_diff.bake(start, reviewers=reviewers)),
                    shell=True)

if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument("-s", "--single",
                        help="Single-commit mode. Base now means the only \
                        commit to be reviewed",
                        action="store_true")
    parser.add_argument("base",
                        help="The last commit/branch before the work to be \
                        reviewed")
    parser.add_argument("--to",
                        help="The last commit in the work to be reviewed")
    args = parser.parse_args()
    if args.single:
        last_commit = None
    elif args.to:
        last_commit = args.to
    else:
        last_commit = "HEAD"
    create_review(args.base,
                  last_commit)
<commit_msg>Make request_review.py executable<commit_after>import argparse
import fnmatch
import json
import os
import sh
import subprocess

class Command(object):
    def __init__(self, cmd, *args):
        self._args = [cmd] + list(args)

    def bake(self, arg, *args):
        self._args += [arg] + list(args)

def create_command(cmd, *args):
    _args = [cmd] + list(args)

    def runner(*args):
        return subprocess.check_output(_args + list(args))

    return runner

arc_diff = sh.Command._create('arc').bake("diff",
                                          allow_untracked=True,
                                          browse=True)
git_diff = create_command('git',
                          'diff-tree',
                          '--no-commit-id',
                          '--name-only',
                          '-r')
git_merge_base = create_command('git', 'merge-base')

def list_changed_files(start, end=None):
    if end:
        commit_range = git_merge_base(start, end).split()[0]
        commit_range += "..%s" % (end,)
    else:
        commit_range = start
    return git_diff(commit_range).split()

def match_owners(owners_filepath, filename):
    if os.path.exists(owners_filepath):
        with open(owners_filepath, 'r') as f:
            raw_data = json.load(f)
            # handle simple file format
            if type(raw_data) == dict:
                return {
                    "reviewers": set(raw_data["reviewers"]),
                    "subscribers": set(raw_data["subscribers"])
                }
            else:
                assert type(raw_data) == list
                owners = {
                    "reviewers": set(),
                    "subscribers": set()
                }
                for data in raw_data:
                    patterns = data["includes"]
                    if any(fnmatch.fnmatch(filename, pat)
                           for pat in patterns):
                        cur_owners = {
                            "reviewers": set(data["reviewers"]),
                            "subscribers": set(data["subscribers"])
                        }
                        owners = merge_owners(owners, cur_owners)
                return owners
    else:
        return None

def merge_owners(owners1, owners2):
    return {
        "reviewers": owners1["reviewers"].union(owners2["reviewers"]),
        "subscribers": owners1["subscribers"].union(owners2["subscribers"])
    }

def get_owners(filename):
    cur_path = filename
    owners = {
        "reviewers": set(),
        "subscribers": set()
    }
    while cur_path:
        cur_dir = os.path.dirname(cur_path)
        owners_filepath = os.path.join(cur_dir, "OWNERS.json")
        cur_owners = match_owners(owners_filepath, filename)
        if cur_owners:
            owners = merge_owners(owners, cur_owners)
        # continue one level up
        cur_path = cur_dir
    return owners

def get_commit_owners(start, end=None):
    owners = {
        "reviewers": set(),
        "subscribers": set()
    }
    for f in list_changed_files(start, end):
        cur_owners = get_owners(f)
        if cur_owners:
            owners = merge_owners(owners, cur_owners)
    return owners

def create_review(start, end=None):
    owners = get_commit_owners(start, end)
    _arc_diff = arc_diff
    # take this off once done testing
    # only=True)
    if end != "HEAD":
        _arc_diff = _arc_diff.bake(head=end)
    reviewers = ','.join(owners["reviewers"])
    subs = ','.join(owners["subscribers"])
    if subs:
        _arc_diff = _arc_diff.bake(cc=subs)
    subprocess.call(str(_arc_diff.bake(start, reviewers=reviewers)),
                    shell=True)

if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument("-s", "--single",
                        help="Single-commit mode. Base now means the only \
                        commit to be reviewed",
                        action="store_true")
    parser.add_argument("base",
                        help="The last commit/branch before the work to be \
                        reviewed")
    parser.add_argument("--to",
                        help="The last commit in the work to be reviewed")
    args = parser.parse_args()
    if args.single:
        last_commit = None
    elif args.to:
        last_commit = args.to
    else:
        last_commit = "HEAD"
    create_review(args.base,
                  last_commit)
<|endoftext|>