<commit_before>""" Meager code path measurement tool.
    Ned Batchelder
    http://nedbatchelder.com/blog/200803/python_code_complexity_microtool.html
    MIT License.
"""
from __future__ import with_statement

import optparse
import sys
import tokenize

from collections import defaultdict
try:
    import ast
    from ast import iter_child_nodes
except ImportError:   # Python 2.5
    from flake8.util import ast, iter_child_nodes

__version__ = '0.5.0'

class ASTVisitor(object):
    """Performs a depth-first walk of the AST."""

    def __init__(self):
        self.node = None
        self._cache = {}

    def default(self, node, *args):
        for child in iter_child_nodes(node):
            self.dispatch(child, *args)

    def dispatch(self, node, *args):
        self.node = node
        klass = node.__class__
        meth = self._cache.get(klass)
        if meth is None:
            className = klass.__name__
            meth = getattr(self.visitor, 'visit' + className, self.default)
            self._cache[klass] = meth
        return meth(node, *args)

    def preorder(self, tree, visitor, *args):
        """Do preorder walk of tree using visitor"""
        self.visitor = visitor
        visitor.visit = self.dispatch
        self.dispatch(tree, *args)  # XXX *args make sense?

class PathNode(object):
    def __init__(self, name, look="circle"):
        self.name = name
        self.look = look

    def to_dot(self):
        print('node [shape=%s,label="%s"] %d;' % (
            self.look, self.name, self.dot_id()))

    def dot_id(self):
        return id(self)

class PathGraph(object):
    def __init__(self, name, entity, lineno):
        self.name = name
        self.entity = entity
        self.lineno = lineno
        self.nodes = defaultdict(list)

    def connect(self, n1, n2):
        self.nodes[n1].append(n2)
        # Ensure that the destination node is always counted.
        self.nodes[n2] = []

    def to_dot(self):
        print('subgraph {')
        for node in self.nodes:
            node.to_dot()
        for node, nexts in self.nodes.items():
            for next in nexts:
                print('%s -- %s;' % (node.dot_id(), next.dot_id()))
        print('}')

    def complexity(self):
        """ Return the McCabe complexity for the graph.
            V-E+2
        """
        num_edges = sum([len(n) for n in self.nodes.values()])
        num_nodes = len(self.nodes)
        return num_edges - num_nodes + 2

class PathGraphingAstVisitor(ASTVisitor):
    """ A visitor for a parsed Abstract Syntax Tree which finds executable
        statements.
    """

    def __init__(self):
        super(PathGraphingAstVisitor, self).__init__()
        self.classname = ""
        self.graphs = {}
        self.reset()

    def reset(self):
        self.graph = None
        self.tail = None

    def dispatch_list(self, node_list):
        for node in node_list:
            self.dispatch(node)

    def visitFunctionDef(self, node):

        if self.classname:
            entity = '%s%s' % (self.classname, node.name)
        else:
            entity = node.name

        name = '%d:1: %r' % (node.lineno, entity)

        if self.graph is not None:
            # closure
            pathnode = self.appendPathNode(name)
            self.tail = pathnode
            self.dispatch_list(node.body)
            bottom = PathNode("", look='point')
            self.graph.connect(self.tail, bottom)
            self.graph.connect(pathnode, bottom)
            self.tail = bottom
        else:
            self.graph = PathGraph(name, entity, node.lineno)
            pathnode = PathNode(name)
            self.tail = pathnode
            self.dispatch_list(node.body)
            self.graphs["%s%s" % (self.classname, node.name)] = self.graph
            self.reset()

    visitAsyncFunctionDef = visitFunctionDef

    def visitClassDef(self, node):
        old_classname = self.classname
        self.classname += node.name + "."
        self.dispatch_list(node.body)
        self.classname = old_classname

    def appendPathNode(self, name):
        if not self.tail:
            return
        pathnode = PathNode(name)
        self.graph.connect(self.tail, pathnode)
        self.tail = pathnode
        return pathnode

    def visitSimpleStatement(self, node):
        if node.lineno is None:
            lineno = 0
        else:
            lineno = node.lineno
        name = "Stmt %d" % lineno
        self.appendPathNode(name)

    visitAssert = visitAssign = visitAugAssign = visitDelete = visitPrint = \
        visitRaise = visitYield = visitImport = visitCall = visitSubscript = \
        visitPass = visitContinue = visitBreak = visitGlobal = visitReturn = \
        visitAwait = visitSimpleStatement

    def visitLoop(self, node):
        name = "Loop %d" % node.lineno
        self._subgraph(node, name)

    visitAsyncFor = visitFor = visitWhile = visitLoop

    def visitIf(self, node):
        name = "If %d" % node.lineno
        self._subgraph(node, name)

    def _subgraph(self, node, name, extra_blocks=()):
        """create the subgraphs representing any `if` and `for` statements"""
        if self.graph is None:
            # global loop
            self.graph = PathGraph(name, name, node.lineno)
            pathnode = PathNode(name)
            self._subgraph_parse(node, pathnode, extra_blocks)
            self.graphs["%s%s" % (self.classname, name)] = self.graph
            self.reset()
        else:
            pathnode = self.appendPathNode(name)
            self._subgraph_parse(node, pathnode, extra_blocks)

    def _subgraph_parse(self, node, pathnode, extra_blocks):
        """parse the body and any `else` block of `if` and `for` statements"""
        loose_ends = []
        self.tail = pathnode
        self.dispatch_list(node.body)
        loose_ends.append(self.tail)
        for extra in extra_blocks:
            self.tail = pathnode
            self.dispatch_list(extra.body)
            loose_ends.append(self.tail)
        if node.orelse:
            self.tail = pathnode
            self.dispatch_list(node.orelse)
            loose_ends.append(self.tail)
        else:
            loose_ends.append(pathnode)
        if pathnode:
            bottom = PathNode("", look='point')
            for le in loose_ends:
                self.graph.connect(le, bottom)
            self.tail = bottom

    def visitTryExcept(self, node):
        name = "TryExcept %d" % node.lineno
        self._subgraph(node, name, extra_blocks=node.handlers)

    visitTry = visitTryExcept

    def visitWith(self, node):
        name = "With %d" % node.lineno
        self.appendPathNode(name)
        self.dispatch_list(node.body)

    visitAsyncWith = visitWith

class McCabeChecker(object):
    """McCabe cyclomatic complexity checker."""
    name = 'mccabe'
    version = __version__
    _code = 'C901'
    _error_tmpl = "C901 %r is too complex (%d)"
    max_complexity = 0

    def __init__(self, tree, filename):
        self.tree = tree

    @classmethod
    def add_options(cls, parser):
        flag = '--max-complexity'
        kwargs = {
            'default': -1,
            'action': 'store',
            'type': 'int',
            'help': 'McCabe complexity threshold',
            'parse_from_config': 'True',
        }
        config_opts = getattr(parser, 'config_options', None)
        if isinstance(config_opts, list):
            # Flake8 2.x
            kwargs.pop('parse_from_config')
            parser.add_option(flag, **kwargs)
            parser.config_options.append('max-complexity')
        else:
            parser.add_option(flag, **kwargs)

    @classmethod
    def parse_options(cls, options):
        cls.max_complexity = int(options.max_complexity)

    def run(self):
        if self.max_complexity < 0:
            return
        visitor = PathGraphingAstVisitor()
        visitor.preorder(self.tree, visitor)
        for graph in visitor.graphs.values():
            if graph.complexity() > self.max_complexity:
                text = self._error_tmpl % (graph.entity, graph.complexity())
                yield graph.lineno, 0, text, type(self)

def get_code_complexity(code, threshold=7, filename='stdin'):
    try:
        tree = compile(code, filename, "exec", ast.PyCF_ONLY_AST)
    except SyntaxError:
        e = sys.exc_info()[1]
        sys.stderr.write("Unable to parse %s: %s\n" % (filename, e))
        return 0

    complx = []
    McCabeChecker.max_complexity = threshold
    for lineno, offset, text, check in McCabeChecker(tree, filename).run():
        complx.append('%s:%d:1: %s' % (filename, lineno, text))

    if len(complx) == 0:
        return 0
    print('\n'.join(complx))
    return len(complx)

def get_module_complexity(module_path, threshold=7):
    """Returns the complexity of a module"""
    with open(module_path, "rU") as mod:
        code = mod.read()
    return get_code_complexity(code, threshold, filename=module_path)

def _read(filename):
    if (2, 5) < sys.version_info < (3, 0):
        with open(filename, 'rU') as f:
            return f.read()
    elif (3, 0) <= sys.version_info < (4, 0):
        """Read the source code."""
        try:
            with open(filename, 'rb') as f:
                (encoding, _) = tokenize.detect_encoding(f.readline)
        except (LookupError, SyntaxError, UnicodeError):
            # Fall back if file encoding is improperly declared
            with open(filename, encoding='latin-1') as f:
                return f.read()
        with open(filename, 'r', encoding=encoding):
            return f.read()

def main(argv=None):
    if argv is None:
        argv = sys.argv[1:]
    opar = optparse.OptionParser()
    opar.add_option("-d", "--dot", dest="dot",
                    help="output a graphviz dot file", action="store_true")
    opar.add_option("-m", "--min", dest="threshold",
                    help="minimum complexity for output", type="int",
                    default=1)

    options, args = opar.parse_args(argv)

    code = _read(args[0])
    tree = compile(code, args[0], "exec", ast.PyCF_ONLY_AST)
    visitor = PathGraphingAstVisitor()
    visitor.preorder(tree, visitor)

    if options.dot:
        print('graph {')
        for graph in visitor.graphs.values():
            if (not options.threshold or
                    graph.complexity() >= options.threshold):
                graph.to_dot()
        print('}')
    else:
        for graph in visitor.graphs.values():
            if graph.complexity() >= options.threshold:
                print(graph.name, graph.complexity())

if __name__ == '__main__':
    main(sys.argv[1:])
<commit_msg>Fix bug with Flake8 plugin.<commit_after>""" Meager code path measurement tool.
    Ned Batchelder
    http://nedbatchelder.com/blog/200803/python_code_complexity_microtool.html
    MIT License.
"""
from __future__ import with_statement

import optparse
import sys
import tokenize

from collections import defaultdict
try:
    import ast
    from ast import iter_child_nodes
except ImportError:   # Python 2.5
    from flake8.util import ast, iter_child_nodes

__version__ = '0.5.1'

class ASTVisitor(object):
    """Performs a depth-first walk of the AST."""

    def __init__(self):
        self.node = None
        self._cache = {}

    def default(self, node, *args):
        for child in iter_child_nodes(node):
            self.dispatch(child, *args)

    def dispatch(self, node, *args):
        self.node = node
        klass = node.__class__
        meth = self._cache.get(klass)
        if meth is None:
            className = klass.__name__
            meth = getattr(self.visitor, 'visit' + className, self.default)
            self._cache[klass] = meth
        return meth(node, *args)

    def preorder(self, tree, visitor, *args):
        """Do preorder walk of tree using visitor"""
        self.visitor = visitor
        visitor.visit = self.dispatch
        self.dispatch(tree, *args)  # XXX *args make sense?

class PathNode(object):
    def __init__(self, name, look="circle"):
        self.name = name
        self.look = look

    def to_dot(self):
        print('node [shape=%s,label="%s"] %d;' % (
            self.look, self.name, self.dot_id()))

    def dot_id(self):
        return id(self)

class PathGraph(object):
    def __init__(self, name, entity, lineno):
        self.name = name
        self.entity = entity
        self.lineno = lineno
        self.nodes = defaultdict(list)

    def connect(self, n1, n2):
        self.nodes[n1].append(n2)
        # Ensure that the destination node is always counted.
        self.nodes[n2] = []

    def to_dot(self):
        print('subgraph {')
        for node in self.nodes:
            node.to_dot()
        for node, nexts in self.nodes.items():
            for next in nexts:
                print('%s -- %s;' % (node.dot_id(), next.dot_id()))
        print('}')

    def complexity(self):
        """ Return the McCabe complexity for the graph.
            V-E+2
        """
        num_edges = sum([len(n) for n in self.nodes.values()])
        num_nodes = len(self.nodes)
        return num_edges - num_nodes + 2

class PathGraphingAstVisitor(ASTVisitor):
    """ A visitor for a parsed Abstract Syntax Tree which finds executable
        statements.
    """

    def __init__(self):
        super(PathGraphingAstVisitor, self).__init__()
        self.classname = ""
        self.graphs = {}
        self.reset()

    def reset(self):
        self.graph = None
        self.tail = None

    def dispatch_list(self, node_list):
        for node in node_list:
            self.dispatch(node)

    def visitFunctionDef(self, node):

        if self.classname:
            entity = '%s%s' % (self.classname, node.name)
        else:
            entity = node.name

        name = '%d:1: %r' % (node.lineno, entity)

        if self.graph is not None:
            # closure
            pathnode = self.appendPathNode(name)
            self.tail = pathnode
            self.dispatch_list(node.body)
            bottom = PathNode("", look='point')
            self.graph.connect(self.tail, bottom)
            self.graph.connect(pathnode, bottom)
            self.tail = bottom
        else:
            self.graph = PathGraph(name, entity, node.lineno)
            pathnode = PathNode(name)
            self.tail = pathnode
            self.dispatch_list(node.body)
            self.graphs["%s%s" % (self.classname, node.name)] = self.graph
            self.reset()

    visitAsyncFunctionDef = visitFunctionDef

    def visitClassDef(self, node):
        old_classname = self.classname
        self.classname += node.name + "."
        self.dispatch_list(node.body)
        self.classname = old_classname

    def appendPathNode(self, name):
        if not self.tail:
            return
        pathnode = PathNode(name)
        self.graph.connect(self.tail, pathnode)
        self.tail = pathnode
        return pathnode

    def visitSimpleStatement(self, node):
        if node.lineno is None:
            lineno = 0
        else:
            lineno = node.lineno
        name = "Stmt %d" % lineno
        self.appendPathNode(name)

    visitAssert = visitAssign = visitAugAssign = visitDelete = visitPrint = \
        visitRaise = visitYield = visitImport = visitCall = visitSubscript = \
        visitPass = visitContinue = visitBreak = visitGlobal = visitReturn = \
        visitAwait = visitSimpleStatement

    def visitLoop(self, node):
        name = "Loop %d" % node.lineno
        self._subgraph(node, name)

    visitAsyncFor = visitFor = visitWhile = visitLoop

    def visitIf(self, node):
        name = "If %d" % node.lineno
        self._subgraph(node, name)

    def _subgraph(self, node, name, extra_blocks=()):
        """create the subgraphs representing any `if` and `for` statements"""
        if self.graph is None:
            # global loop
            self.graph = PathGraph(name, name, node.lineno)
            pathnode = PathNode(name)
            self._subgraph_parse(node, pathnode, extra_blocks)
            self.graphs["%s%s" % (self.classname, name)] = self.graph
            self.reset()
        else:
            pathnode = self.appendPathNode(name)
            self._subgraph_parse(node, pathnode, extra_blocks)

    def _subgraph_parse(self, node, pathnode, extra_blocks):
        """parse the body and any `else` block of `if` and `for` statements"""
        loose_ends = []
        self.tail = pathnode
        self.dispatch_list(node.body)
        loose_ends.append(self.tail)
        for extra in extra_blocks:
            self.tail = pathnode
            self.dispatch_list(extra.body)
            loose_ends.append(self.tail)
        if node.orelse:
            self.tail = pathnode
            self.dispatch_list(node.orelse)
            loose_ends.append(self.tail)
        else:
            loose_ends.append(pathnode)
        if pathnode:
            bottom = PathNode("", look='point')
            for le in loose_ends:
                self.graph.connect(le, bottom)
            self.tail = bottom

    def visitTryExcept(self, node):
        name = "TryExcept %d" % node.lineno
        self._subgraph(node, name, extra_blocks=node.handlers)

    visitTry = visitTryExcept

    def visitWith(self, node):
        name = "With %d" % node.lineno
        self.appendPathNode(name)
        self.dispatch_list(node.body)

    visitAsyncWith = visitWith

class McCabeChecker(object):
    """McCabe cyclomatic complexity checker."""
    name = 'mccabe'
    version = __version__
    _code = 'C901'
    _error_tmpl = "C901 %r is too complex (%d)"
    max_complexity = -1

    def __init__(self, tree, filename):
        self.tree = tree

    @classmethod
    def add_options(cls, parser):
        flag = '--max-complexity'
        kwargs = {
            'default': -1,
            'action': 'store',
            'type': 'int',
            'help': 'McCabe complexity threshold',
            'parse_from_config': 'True',
        }
        config_opts = getattr(parser, 'config_options', None)
        if isinstance(config_opts, list):
            # Flake8 2.x
            kwargs.pop('parse_from_config')
            parser.add_option(flag, **kwargs)
            parser.config_options.append('max-complexity')
        else:
            parser.add_option(flag, **kwargs)

    @classmethod
    def parse_options(cls, options):
        cls.max_complexity = int(options.max_complexity)

    def run(self):
        if self.max_complexity < 0:
            return
        visitor = PathGraphingAstVisitor()
        visitor.preorder(self.tree, visitor)
        for graph in visitor.graphs.values():
            if graph.complexity() > self.max_complexity:
                text = self._error_tmpl % (graph.entity, graph.complexity())
                yield graph.lineno, 0, text, type(self)

def get_code_complexity(code, threshold=7, filename='stdin'):
    try:
        tree = compile(code, filename, "exec", ast.PyCF_ONLY_AST)
    except SyntaxError:
        e = sys.exc_info()[1]
        sys.stderr.write("Unable to parse %s: %s\n" % (filename, e))
        return 0

    complx = []
    McCabeChecker.max_complexity = threshold
    for lineno, offset, text, check in McCabeChecker(tree, filename).run():
        complx.append('%s:%d:1: %s' % (filename, lineno, text))

    if len(complx) == 0:
        return 0
    print('\n'.join(complx))
    return len(complx)

def get_module_complexity(module_path, threshold=7):
    """Returns the complexity of a module"""
    with open(module_path, "rU") as mod:
        code = mod.read()
    return get_code_complexity(code, threshold, filename=module_path)

def _read(filename):
    if (2, 5) < sys.version_info < (3, 0):
        with open(filename, 'rU') as f:
            return f.read()
    elif (3, 0) <= sys.version_info < (4, 0):
        """Read the source code."""
        try:
            with open(filename, 'rb') as f:
                (encoding, _) = tokenize.detect_encoding(f.readline)
        except (LookupError, SyntaxError, UnicodeError):
            # Fall back if file encoding is improperly declared
            with open(filename, encoding='latin-1') as f:
                return f.read()
        with open(filename, 'r', encoding=encoding):
            return f.read()

def main(argv=None):
    if argv is None:
        argv = sys.argv[1:]
    opar = optparse.OptionParser()
    opar.add_option("-d", "--dot", dest="dot",
                    help="output a graphviz dot file", action="store_true")
    opar.add_option("-m", "--min", dest="threshold",
                    help="minimum complexity for output", type="int",
                    default=1)

    options, args = opar.parse_args(argv)

    code = _read(args[0])
    tree = compile(code, args[0], "exec", ast.PyCF_ONLY_AST)
    visitor = PathGraphingAstVisitor()
    visitor.preorder(tree, visitor)

    if options.dot:
        print('graph {')
        for graph in visitor.graphs.values():
            if (not options.threshold or
                    graph.complexity() >= options.threshold):
                graph.to_dot()
        print('}')
    else:
        for graph in visitor.graphs.values():
            if graph.complexity() >= options.threshold:
                print(graph.name, graph.complexity())

if __name__ == '__main__':
    main(sys.argv[1:])
<|endoftext|>


<commit_before>#!/usr/bin/python3

import os
import re
import shutil
import subprocess
import sys
import tempfile
from contextlib import contextmanager

"""
Provide a blurred screenshot for i3lock.

This script screenshots each monitor, blurs it, adds a lock icon,
and uses this as the background for i3lock to have a "blurred screen"
lock effect.
"""

# The lock file to prevent multiple lock screens from triggering
LOCK_FILE_PATH = f"/tmp/{os.geteuid()}-lockscreen.lock"

# The image of a lock to superimpose on the screenshots
LOCK_IMAGE_PATH = "/usr/local/scripts/dat/lock.png"

# Regular expression for monitor dimensions from xrandr
MONITOR_DIMENSION_REGEX = re.compile(
    r"^([\w\-]+) connected [^\d]*(\d+)x(\d+)\+(\d+)\+(\d+)",
)

# The pixelation effect. These two numbers need to multiply to 10,000.
SMALL_SCALE_PERCENT = 5
LARGE_SCALE_PERCENT = 2000
assert SMALL_SCALE_PERCENT * LARGE_SCALE_PERCENT == 10000

@contextmanager
def lock_file(path):
    file = open(path, "x")
    try:
        yield
    finally:
        file.close()
        os.remove(path)

class Monitor:
    def __init__(self, name, width, height, offset_x, offset_y):
        self.name = name
        self.width = width
        self.height = height
        self.offset_x = offset_x
        self.offset_y = offset_y

    def size_and_position(self):
        return f"{self.width}x{self.height}+{self.offset_x}+{self.offset_y}"

    def position(self):
        return f"+{self.offset_x}+{self.offset_y}"

    def filename(self, directory):
        return os.path.join(directory, f"{self.name}.png")

    def __hash__(self):
        return hash(self.name)

def get_monitors_dimensions():
    monitors = []
    xrandr_output = subprocess.check_output(["xrandr", "--query"], encoding="utf-8")
    for line in xrandr_output.splitlines():
        match = MONITOR_DIMENSION_REGEX.match(line)
        if match is None:
            # Not a connected monitor, skip
            continue

        name = match[1]
        width = int(match[2])
        height = int(match[3])
        offset_x = int(match[4])
        offset_y = int(match[5])
        monitors.append(Monitor(name, width, height, offset_x, offset_y))

    return monitors

def capture_and_blur(directory, monitor):
    image_output = monitor.filename(directory)
    proc_screenshot = subprocess.Popen(
        ["maim", "-u", f"--geometry={monitor.size_and_position()}", "/dev/stdout"],
        stdout=subprocess.PIPE,
    )
    proc_convert = subprocess.Popen(
        [
            "convert",
            "/dev/stdin",
            "-scale",
            f"{SMALL_SCALE_PERCENT}%",
            "-scale",
            f"{LARGE_SCALE_PERCENT}%",
            "/dev/stdout",
        ],
        stdin=proc_screenshot.stdout,
        stdout=subprocess.PIPE,
    )
    proc_composite = subprocess.Popen(
        [
            "composite",
            "-gravity",
            "Center",
            LOCK_IMAGE_PATH,
            "/dev/stdin",
            image_output,
        ],
        stdin=proc_convert.stdout,
        stdout=subprocess.DEVNULL,
    )
    return proc_composite

def merge_images(directory, monitors):
    full_output = os.path.join(directory, "image.png")

    # Screenshot full desktop as base image
    # This is guaranteed to have the correct dimensions we need
    subprocess.check_call(["maim", "-u", full_output])

    for monitor in monitors:
        # Impose the partial image on the incomplete one
        image_input = monitor.filename(directory)
        subprocess.check_call(
            [
                "convert",
                full_output,
                image_input,
                "-geometry",
                monitor.position(),
                "-compose",
                "over",
                "-composite",
                full_output,
            ],
        )

    return full_output

def run_i3lock(image_path):
    # subprocess.check_call(['i3lock', '-i', image_path])
    subprocess.check_call(["/usr/bin/feh", image_path])

if __name__ == "__main__":
    with lock_file(LOCK_FILE_PATH):
        with tempfile.TemporaryDirectory(prefix="lockscreen-") as directory:
            # Get monitor data
            monitors = get_monitors_dimensions()

            # Start blur jobs
            processes = []
            for monitor in monitors:
                processes.append(capture_and_blur(directory, monitor))

            # Wait for jobs
            for process in processes:
                process.communicate()

            # Merge images and display i3lock
            image_path = merge_images(directory, monitors)
            run_i3lock(image_path)

#   maim -u --geometry="${x_res}x${y_res}+0+0" /dev/stdout \
#       | convert /dev/stdin -scale "$small_scale%" -scale "$large_scale%" /dev/stdout \
#       | composite -gravity Center "$lockimage" /dev/stdin "$directory/left.png" &
#   maim -u --geometry="${x_res}x${y_res}+${x_res}+0" /dev/stdout \
#       | convert /dev/stdin -scale "$small_scale%" -scale "$large_scale%" /dev/stdout \
#       | composite -gravity Center "$lockimage" /dev/stdin "$directory/right.png" &
#   wait
#   convert +append "$left" "$right" "$imagefile"
<commit_msg>Clean up script.<commit_after>#!/usr/bin/python3

import os
import re
import shutil
import subprocess
import sys
import tempfile
from contextlib import contextmanager

"""
Provide a blurred screenshot for i3lock.

This script screenshots each monitor, blurs it, adds a lock icon,
and uses this as the background for i3lock to have a "blurred screen"
lock effect.
"""

# The lock file to prevent multiple lock screens from triggering
LOCK_FILE_PATH = f"/tmp/{os.geteuid()}-lockscreen.lock"

# The image of a lock to superimpose on the screenshots
LOCK_IMAGE_PATH = "/usr/local/scripts/dat/lock.png"

# Regular expression for monitor dimensions from xrandr
MONITOR_DIMENSION_REGEX = re.compile(
    r"^([\w\-]+) connected [^\d]*(\d+)x(\d+)\+(\d+)\+(\d+)",
)

# The pixelation effect. These two numbers need to multiply to 10,000.
SMALL_SCALE_PERCENT = 5
LARGE_SCALE_PERCENT = 2000
assert SMALL_SCALE_PERCENT * LARGE_SCALE_PERCENT == 10000

@contextmanager
def lock_file(path):
    file = open(path, "x")
    try:
        yield
    finally:
        file.close()
        os.remove(path)

class Monitor:
    def __init__(self, name, width, height, offset_x, offset_y):
        self.name = name
        self.width = width
        self.height = height
        self.offset_x = offset_x
        self.offset_y = offset_y

    def size_and_position(self):
        return f"{self.width}x{self.height}+{self.offset_x}+{self.offset_y}"

    def position(self):
        return f"+{self.offset_x}+{self.offset_y}"

    def filename(self, directory):
        return os.path.join(directory, f"{self.name}.png")

    def __hash__(self):
        return hash(self.name)

def get_monitors_dimensions():
    monitors = []
    xrandr_output = subprocess.check_output(["xrandr", "--query"], encoding="utf-8")
    for line in xrandr_output.splitlines():
        match = MONITOR_DIMENSION_REGEX.match(line)
        if match is None:
            # Not a connected monitor, skip
            continue

        name = match[1]
        width = int(match[2])
        height = int(match[3])
        offset_x = int(match[4])
        offset_y = int(match[5])
        monitors.append(Monitor(name, width, height, offset_x, offset_y))

    return monitors

def capture_and_blur(directory, monitor):
    image_output = monitor.filename(directory)
    proc_screenshot = subprocess.Popen(
        ["maim", "-u", f"--geometry={monitor.size_and_position()}", "/dev/stdout"],
        stdout=subprocess.PIPE,
    )
    proc_convert = subprocess.Popen(
        [
            "convert",
            "/dev/stdin",
            "-scale",
            f"{SMALL_SCALE_PERCENT}%",
            "-scale",
            f"{LARGE_SCALE_PERCENT}%",
            "/dev/stdout",
        ],
        stdin=proc_screenshot.stdout,
        stdout=subprocess.PIPE,
    )
    proc_composite = subprocess.Popen(
        [
            "composite",
            "-gravity",
            "Center",
            LOCK_IMAGE_PATH,
            "/dev/stdin",
            image_output,
        ],
        stdin=proc_convert.stdout,
        stdout=subprocess.DEVNULL,
    )
    return proc_composite

def merge_images(directory, monitors):
    full_output = os.path.join(directory, "image.png")

    # Screenshot full desktop as base image
    # This is guaranteed to have the correct dimensions we need
    subprocess.check_call(["maim", "-u", full_output])

    for monitor in monitors:
        # Impose the partial image on the incomplete one
        image_input = monitor.filename(directory)
        subprocess.check_call(
            [
                "convert",
                full_output,
                image_input,
                "-geometry",
                monitor.position(),
                "-compose",
                "over",
                "-composite",
                full_output,
            ],
        )

    return full_output

def run_i3lock(image_path):
    subprocess.check_call(["i3lock", "-i", image_path])

if __name__ == "__main__":
    with lock_file(LOCK_FILE_PATH):
        with tempfile.TemporaryDirectory(prefix="lockscreen-") as directory:
            # Get monitor data
            monitors = get_monitors_dimensions()

            # Start blur jobs
            processes = []
            for monitor in monitors:
                processes.append(capture_and_blur(directory, monitor))

            # Wait for jobs
            for process in processes:
                process.communicate()

            # Merge images and display i3lock
            image_path = merge_images(directory, monitors)
            run_i3lock(image_path)
<|endoftext|>


<commit_before># Copyright (C) 2020 OpenMotics BV
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
"""
Input BLL
"""
from __future__ import absolute_import

import copy
import logging
import time
from threading import Lock

from peewee import JOIN

from gateway.daemon_thread import DaemonThread, DaemonThreadWait
from gateway.dto.input import InputStatusDTO
from ioc import Injectable, Inject, INJECTED, Singleton
from gateway.dto import InputDTO
from gateway.events import GatewayEvent
from gateway.models import Input, Room
from gateway.hal.master_event import MasterEvent
from gateway.base_controller import BaseController, SyncStructure
from gateway.pubsub import PubSub
from serial_utils import CommunicationTimedOutException
from toolbox import Toolbox

if False:  # MYPY
    from typing import Dict, List, Optional, Tuple, Any
    from gateway.hal.master_controller import MasterController, CommunicationFailure

logger = logging.getLogger(__name__)

@Injectable.named('input_controller')
@Singleton
class InputController(BaseController):
    SYNC_STRUCTURES = [SyncStructure(Input, 'input', skip=lambda i: i.module_type not in ['i', 'I'])]

    @Inject
    def __init__(self, master_controller=INJECTED):
        # type: (MasterController) -> None
        super(InputController, self).__init__(master_controller)
        self._cache = InputStateCache()
        self._sync_state_thread = None  # type: Optional[DaemonThread]
        self._pubsub.subscribe_master_events(PubSub.MasterTopics.INPUT, self._handle_master_event)

    def start(self):
        # type: () -> None
        super(InputController, self).start()
        self._sync_state_thread = DaemonThread(name='inputsyncstate',
                                               target=self._sync_state,
                                               interval=600, delay=10)
        self._sync_state_thread.start()

    def stop(self):
        # type: () -> None
        super(InputController, self).stop()
        if self._sync_state_thread:
            self._sync_state_thread.stop()
            self._sync_state_thread = None

    def request_sync_state(self):
        if self._sync_state_thread:
            self._sync_state_thread.request_single_run()

    def _sync_state(self):
        try:
            for state_dto in self._master_controller.load_input_status():
                _, input_dto = self._cache.handle_change(state_dto)
                if input_dto is not None:
                    # Always send events on the background sync
                    self._publish_input_change(input_dto)
        except CommunicationTimedOutException:
            logger.error('Got communication timeout during synchronization, waiting 10 seconds.')
            raise DaemonThreadWait
        except CommunicationFailure:
            # This is an expected situation
            raise DaemonThreadWait

    def _handle_input_status(self, state_dto):
        # type: (InputStatusDTO) -> None
        changed, input_dto = self._cache.handle_change(state_dto)
        if changed and input_dto is not None:
            self._publish_input_change(input_dto)

    def _publish_input_change(self, input_dto):
        # type: (InputDTO) -> None
        event_data = {'id': input_dto.id,
                      'status': input_dto.state.status if input_dto.state is not None else None,
                      'location': {'room_id': Toolbox.denonify(input_dto.room, 255)}}
        gateway_event = GatewayEvent(event_type=GatewayEvent.Types.INPUT_CHANGE, data=event_data)
        self._pubsub.publish_gateway_event(PubSub.GatewayTopics.STATE, gateway_event)

    def _handle_master_event(self, master_event):
        # type: (MasterEvent) -> None
        super(InputController, self)._handle_master_event(master_event)
        if master_event.type == MasterEvent.Types.INPUT_CHANGE:
            self._handle_input_status(master_event.data['state'])

    def load_input(self, input_id):  # type: (int) -> InputDTO
        input_ = Input.select(Input, Room) \
                      .join_from(Input, Room, join_type=JOIN.LEFT_OUTER) \
                      .where(Input.number == input_id) \
                      .get()  # type: Input  # TODO: Load dict
        input_dto = self._master_controller.load_input(input_id=input_id)
        input_dto.room = input_.room.number if input_.room is not None else None
        input_dto.event_enabled = input_.event_enabled
        return input_dto

    def load_inputs(self):  # type: () -> List[InputDTO]
        inputs_dtos = []
        for input_ in list(Input.select(Input, Room)
                                .join_from(Input, Room, join_type=JOIN.LEFT_OUTER)):  # TODO: Load dicts
            input_dto = self._master_controller.load_input(input_id=input_.number)
            input_dto.room = input_.room.number if input_.room is not None else None
            input_dto.event_enabled = input_.event_enabled
            inputs_dtos.append(input_dto)
        self._cache.update_inputs(inputs_dtos)

        return inputs_dtos

    def save_inputs(self, inputs):  # type: (List[InputDTO]) -> None
        inputs_to_save = []
        for input_dto in inputs:
            input_ = Input.get_or_none(number=input_dto.id)  # type: Input
            if input_ is None:
                logger.info('Ignored saving non-existing Input {0}'.format(input_dto.id))
            if 'event_enabled' in input_dto.loaded_fields:
                input_.event_enabled = input_dto.event_enabled
                input_.save()
            if 'room' in input_dto.loaded_fields:
                if input_dto.room is None:
                    input_.room = None
                elif 0 <= input_dto.room <= 100:
                    # TODO: Validation should happen on API layer
                    input_.room, _ = Room.get_or_create(number=input_dto.room)
                input_.save()
            inputs_to_save.append(input_dto)
        self._master_controller.save_inputs(inputs_to_save)

    def get_input_status(self, input_id):
        # type: (int) -> Optional[InputStatusDTO]
        input_state_dto = self._cache.get_input_status().get(input_id)
        if input_state_dto is None:
            raise ValueError('Input with id {} does not exist'.format(input_id))
        return input_state_dto

    def get_input_statuses(self):
        # type: () -> List[Optional[InputStatusDTO]]
        return list(self._cache.get_input_status().values())

    def set_input_status(self, status_dto):
        # type: (InputStatusDTO) -> InputStatusDTO
        self._cache.handle_change(status_dto)
        # TODO: only update cache if input is not on master (e.g. if status_dto.id < 200)
        self._master_controller.set_input(status_dto.id, status_dto.status)
        return status_dto

    def get_last_inputs(self):  # type: () -> List[int]
        """
        Get the X last changed inputs during the last Y seconds.
        """
        return self._cache.get_recent_inputs()

    def get_input_module_type(self, input_module_id):
        """ Gets the module type for a given Input Module ID """
        return self._master_controller.get_input_module_type(input_module_id)

    @staticmethod
    def load_inputs_event_enabled():
        return {input_['number']: input_['event_enabled']
                for input_ in Input.select().dicts()}

class InputStateCache(object):
    def __init__(self):
        self._cache = {}  # type: Dict[int,InputDTO]
        self._lock = Lock()

    def get_input_status(self):
        # type: () -> Dict[int,Optional[InputStatusDTO]]
        return {x.id: x.state for x in self._cache.values()}

    def set_input_status(self, input_id, status):
        # type: (int, bool) -> bool
        changed, _ = self.handle_change(InputStatusDTO(input_id, status=status))
        return changed

    def update_inputs(self, input_dtos):
        # type: (List[InputDTO]) -> None
        with self._lock:
            new_state = {}
            for input_dto in input_dtos:
                input_dto = copy.copy(input_dto)
                if input_dto.id in self._cache:
                    input_dto.state = self._cache[input_dto.id].state
                else:
                    input_dto.state = InputStatusDTO(input_dto.id)
                new_state[input_dto.id] = input_dto
            self._cache = new_state

    def get_recent_inputs(self, threshold=10):
        # type: (int) -> List[int]
        sorted_inputs = sorted(list(self._cache.values()), key=lambda x: x.state.updated_at if x.state else 0.0)
        recent_changed_inputs = [y.id for y in sorted_inputs if y.state and y.state.updated_at > time.time() - threshold]
        return recent_changed_inputs[-5:]

    def handle_change(self, state_dto):
        # type: (InputStatusDTO) -> Tuple[bool, Optional[InputDTO]]
        """
        Cache input state and detect changes.
        The classic master will send multiple status events when an input changes,
        this deduplicates actual changes based on the cached state.
        """
        with self._lock:
            input_id = state_dto.id
            if input_id not in self._cache:
                logger.warning('Received change for unknown input {0}: {1}'.format(input_id, state_dto))
                self._cache[input_id] = InputDTO(input_id, state=state_dto)
                return False, None
            changed = False
            if 'status' in state_dto.loaded_fields and self._cache[input_id].state != state_dto:
                logger.debug('Change detected in input status')
                self._cache[input_id].state = state_dto
                changed = True
            else:
                logger.debug('No change detected in input status')
            return changed, self._cache[input_id]
<commit_msg>Handle TypeError when loading an input with messed up module configuration (possible after manual module reconfigurations), triggering an automated resync which resolves the cause.<commit_after># Copyright (C) 2020 OpenMotics BV
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
"""
Input BLL
"""
from __future__ import absolute_import

import copy
import logging
import time
from threading import Lock

from peewee import JOIN

from gateway.daemon_thread import DaemonThread, DaemonThreadWait
from gateway.dto.input import InputStatusDTO
from ioc import Injectable, Inject, INJECTED, Singleton
from gateway.dto import InputDTO
from gateway.events import GatewayEvent
from gateway.models import Input, Room
from gateway.hal.master_event import MasterEvent
from gateway.hal.master_controller import CommunicationFailure
from gateway.base_controller import BaseController, SyncStructure
from gateway.pubsub import PubSub
from serial_utils import CommunicationTimedOutException
from toolbox import Toolbox

if False:  # MYPY
    from typing import Dict, List, Optional, Tuple, Any
    from gateway.hal.master_controller import MasterController

logger = logging.getLogger(__name__)

@Injectable.named('input_controller')
@Singleton
class InputController(BaseController):
    SYNC_STRUCTURES = [SyncStructure(Input, 'input', skip=lambda i: i.module_type not in ['i', 'I'])]

    @Inject
    def __init__(self, master_controller=INJECTED):
        # type: (MasterController) -> None
        super(InputController, self).__init__(master_controller)
        self._cache = InputStateCache()
        self._sync_state_thread = None  # type: Optional[DaemonThread]
        self._pubsub.subscribe_master_events(PubSub.MasterTopics.INPUT, self._handle_master_event)

    def start(self):
        # type: () -> None
        super(InputController, self).start()
        self._sync_state_thread = DaemonThread(name='inputsyncstate',
                                               target=self._sync_state,
                                               interval=600, delay=10)
        self._sync_state_thread.start()

    def stop(self):
        # type: () -> None
        super(InputController, self).stop()
        if self._sync_state_thread:
            self._sync_state_thread.stop()
            self._sync_state_thread = None

    def request_sync_state(self):
        if self._sync_state_thread:
            self._sync_state_thread.request_single_run()

    def _sync_state(self):
        try:
            for state_dto in self._master_controller.load_input_status():
                _, input_dto = self._cache.handle_change(state_dto)
                if input_dto is not None:
                    # Always send events on the background sync
                    self._publish_input_change(input_dto)
        except CommunicationTimedOutException:
            logger.error('Got communication timeout during synchronization, waiting 10 seconds.')
            raise DaemonThreadWait
        except CommunicationFailure:
            # This is an expected situation
            raise DaemonThreadWait

    def _handle_input_status(self, state_dto):
        # type: (InputStatusDTO) -> None
        changed, input_dto = self._cache.handle_change(state_dto)
        if changed and input_dto is not None:
            self._publish_input_change(input_dto)

    def _publish_input_change(self, input_dto):
        # type: (InputDTO) -> None
        event_data = {'id': input_dto.id,
                      'status': input_dto.state.status if input_dto.state is not None else None,
                      'location': {'room_id': Toolbox.denonify(input_dto.room, 255)}}
        gateway_event = GatewayEvent(event_type=GatewayEvent.Types.INPUT_CHANGE, data=event_data)
        self._pubsub.publish_gateway_event(PubSub.GatewayTopics.STATE, gateway_event)

    def _handle_master_event(self, master_event):
        # type: (MasterEvent) -> None
        super(InputController, self)._handle_master_event(master_event)
        if master_event.type == MasterEvent.Types.INPUT_CHANGE:
            self._handle_input_status(master_event.data['state'])

    def load_input(self, input_id):  # type: (int) -> InputDTO
        input_ = Input.select(Input, Room) \
                      .join_from(Input, Room, join_type=JOIN.LEFT_OUTER) \
                      .where(Input.number == input_id) \
                      .get()  # type: Input  # TODO: Load dict
        input_dto = self._master_controller.load_input(input_id=input_id)
        input_dto.room = input_.room.number if input_.room is not None else None
        input_dto.event_enabled = input_.event_enabled
        return input_dto

    def load_inputs(self):  # type: () -> List[InputDTO]
        inputs_dtos = []
        for input_ in list(Input.select(Input, Room)
                                .join_from(Input, Room, join_type=JOIN.LEFT_OUTER)):  # TODO: Load dicts
            try:
                input_dto = self._master_controller.load_input(input_id=input_.number)
            except TypeError as ex:
                logger.error('Could not load input {0}: {1}'.format(input_.number, ex))
                self.request_sync_orm()  # This likely is caused by module reconfigurations
                continue
            input_dto.room = input_.room.number if input_.room is not None else None
            input_dto.event_enabled = input_.event_enabled
            inputs_dtos.append(input_dto)
        self._cache.update_inputs(inputs_dtos)

        return inputs_dtos

    def save_inputs(self, inputs):  # type: (List[InputDTO]) -> None
        inputs_to_save = []
        for input_dto in inputs:
            input_ = Input.get_or_none(number=input_dto.id)  # type: Input
            if input_ is None:
                logger.info('Ignored saving non-existing Input {0}'.format(input_dto.id))
            if 'event_enabled' in input_dto.loaded_fields:
                input_.event_enabled = input_dto.event_enabled
                input_.save()
            if 'room' in input_dto.loaded_fields:
                if input_dto.room is None:
                    input_.room = None
                elif 0 <= input_dto.room <= 100:
                    # TODO: Validation should happen on API layer
                    input_.room, _ = Room.get_or_create(number=input_dto.room)
                input_.save()
            inputs_to_save.append(input_dto)
        self._master_controller.save_inputs(inputs_to_save)

    def get_input_status(self, input_id):
        # type: (int) -> Optional[InputStatusDTO]
        input_state_dto = self._cache.get_input_status().get(input_id)
        if input_state_dto is None:
            raise ValueError('Input with id {} does not exist'.format(input_id))
        return input_state_dto

    def get_input_statuses(self):
        # type: () -> List[Optional[InputStatusDTO]]
        return list(self._cache.get_input_status().values())

    def set_input_status(self, status_dto):
        # type: (InputStatusDTO) -> InputStatusDTO
        self._cache.handle_change(status_dto)
        # TODO: only update cache if input is not on master (e.g. if status_dto.id < 200)
        self._master_controller.set_input(status_dto.id, status_dto.status)
        return status_dto

    def get_last_inputs(self):  # type: () -> List[int]
        """
        Get the X last changed inputs during the last Y seconds.
        """
        return self._cache.get_recent_inputs()

    def get_input_module_type(self, input_module_id):
        """ Gets the module type for a given Input Module ID """
        return self._master_controller.get_input_module_type(input_module_id)

    @staticmethod
    def load_inputs_event_enabled():
        return {input_['number']: input_['event_enabled']
                for input_ in Input.select().dicts()}

class InputStateCache(object):
    def __init__(self):
        self._cache = {}  # type: Dict[int,InputDTO]
        self._lock = Lock()

    def get_input_status(self):
        # type: () -> Dict[int,Optional[InputStatusDTO]]
        return {x.id: x.state for x in self._cache.values()}

    def set_input_status(self, input_id, status):
        # type: (int, bool) -> bool
        changed, _ = self.handle_change(InputStatusDTO(input_id, status=status))
        return changed

    def update_inputs(self, input_dtos):
        # type: (List[InputDTO]) -> None
        with self._lock:
            new_state = {}
            for input_dto in input_dtos:
                input_dto = copy.copy(input_dto)
                if input_dto.id in self._cache:
                    input_dto.state = self._cache[input_dto.id].state
                else:
                    input_dto.state = InputStatusDTO(input_dto.id)
                new_state[input_dto.id] = input_dto
            self._cache = new_state

    def get_recent_inputs(self, threshold=10):
        # type: (int) -> List[int]
        sorted_inputs = sorted(list(self._cache.values()), key=lambda x: x.state.updated_at if x.state else 0.0)
        recent_changed_inputs = [y.id for y in sorted_inputs if y.state and y.state.updated_at > time.time() - threshold]
        return recent_changed_inputs[-5:]

    def handle_change(self, state_dto):
        # type: (InputStatusDTO) -> Tuple[bool, Optional[InputDTO]]
        """
        Cache input state and detect changes.
        The classic master will send multiple status events when an input changes,
        this deduplicates actual changes based on the cached state.
        """
        with self._lock:
            input_id = state_dto.id
            if input_id not in self._cache:
                logger.warning('Received change for unknown input {0}: {1}'.format(input_id, state_dto))
                self._cache[input_id] = InputDTO(input_id, state=state_dto)
                return False, None
            changed = False
            if 'status' in state_dto.loaded_fields and self._cache[input_id].state != state_dto:
                logger.debug('Change detected in input status')
                self._cache[input_id].state = state_dto
                changed = True
            else:
                logger.debug('No change detected in input status')
            return changed, self._cache[input_id]
<|endoftext|>


<commit_before>from django import forms
from django.contrib.auth import update_session_auth_hash
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.models import Group
from django.contrib.auth.views import PasswordChangeView, PasswordResetView
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
from django.http import Http404
from django.shortcuts import get_object_or_404, redirect, render, render_to_response
from django.views.generic import FormView, View
from django.views.generic.edit import UpdateView

import login.models as models

from .forms import ChangeEmailForm, CreateUserForm, DetachForm, EditUserForm
from .models import ADMIN_PERM, GroupMembership, UserGroup
from .models import myuser as OepUser

class ProfileView(View):
    def get(self, request, user_id):
        """
        Load the user identified by user_id and is OAuth-token. If latter does not exist yet, create one.
        :param request: A HTTP-request object sent by the Django framework.
        :param user_id: An user id
        :return: Profile renderer
        """
        from rest_framework.authtoken.models import Token

        for user in OepUser.objects.all():
            Token.objects.get_or_create(user=user)
        user = get_object_or_404(OepUser, pk=user_id)
        token = None
        if request.user.is_authenticated:
            token = Token.objects.get(user=request.user)
        return render(
            request, "login/profile.html", {"profile_user": user, "token": token}
        )

class GroupManagement(View, LoginRequiredMixin):
    def get(self, request):
        """
        Load and list the available groups by groupadmin.
        :param request: A HTTP-request object sent by the Django framework.
        :param user_id: An user id
        :return: Profile renderer
        """

        membership = request.user.memberships
        return render(
            request, "login/list_memberships.html", {"membership": membership}
        )

class GroupCreate(View, LoginRequiredMixin):
    def get(self, request, group_id=None):
        """
        Load the chosen action(create or edit) for a group.
        :param request: A HTTP-request object sent by the Django framework.
        :param user_id: An user id
        :param user_id: An group id
        :return: Profile renderer
        """
        group = None
        if group_id:
            group = UserGroup.objects.get(id=group_id)
            membership = get_object_or_404(
                GroupMembership, group=group, user=request.user
            )
            if membership.level < ADMIN_PERM:
                raise PermissionDenied
        return render(request, "login/group_create.html", {"group": group})

    def post(self, request, group_id=None):
        """
        Performs selected action(save or delete) for a group. If a groupname already exists, then a error
        will be output.
        The selected users become members of this group. The groupadmin is already set.
        :param request: A HTTP-request object sent by the Django framework.
        :param user_id: An user id
        :param user_id: An group id
        :return: Profile renderer
        """

        if group_id:
            group = UserGroup.objects.get(id=group_id)
            membership = get_object_or_404(
                GroupMembership, group=group, user=request.user
            )
            if membership.level < ADMIN_PERM:
                raise PermissionDenied
            group.name = request.POST["name"]
            group.description = request.POST["description"]
            group.save()
        else:
            group = UserGroup.objects.create(name=request.POST["name"])
            group.save()
            membership = GroupMembership.objects.create(
                user=request.user, group=group, level=ADMIN_PERM
            )
            membership.save()
        return redirect("/user/groups/{id}/members".format(id=group.id))

class GroupEdit(View, LoginRequiredMixin):
    def get(self, request, group_id):
        """
        Load the chosen action(create or edit) for a group.
        :param request: A HTTP-request object sent by the Django framework.
        :param user_id: An user id
        :param user_id: An group id
        :return: Profile renderer
        """
        group = get_object_or_404(UserGroup, pk=group_id)
        is_admin = False
        membership = GroupMembership.objects.filter(
            group=group, user=request.user
        ).first()
        if membership:
            is_admin = membership.level >= ADMIN_PERM
        return render(
            request,
            "login/change_form.html",
            {"group": group, "choices": GroupMembership.choices, "is_admin": is_admin},
        )

    def post(self, request, group_id):
        """
        Performs selected action(save or delete) for a group. If a groupname already exists, then a error
        will be output.
        The selected users become members of this group. The groupadmin is already set.
        :param request: A HTTP-request object sent by the Django framework.
        :param user_id: An user id
        :param user_id: An group id
        :return: Profile renderer
        """
        mode = request.POST["mode"]
        group = get_object_or_404(UserGroup, id=group_id)
        membership = get_object_or_404(GroupMembership, group=group, user=request.user)

        errors = {}
        if mode == "add_user":
            if membership.level < models.WRITE_PERM:
                raise PermissionDenied
            try:
                user = OepUser.objects.get(name=request.POST["name"])
                membership, _ = GroupMembership.objects.get_or_create(
                    group=group, user=user
                )
                membership.save()
            except OepUser.DoesNotExist:
                errors["name"] = "User does not exist"
        elif mode == "remove_user":
            if membership.level < models.DELETE_PERM:
                raise PermissionDenied
            user = OepUser.objects.get(id=request.POST["user_id"])
            membership = GroupMembership.objects.get(group=group, user=user)
            if membership.level >= ADMIN_PERM:
                admins = GroupMembership.objects.filter(group=group).exclude(user=user)
                if not admins:
                    errors["name"] = "A group needs at least one admin"
                else:
                    membership.delete()
            else:
                membership.delete()
        elif mode == "alter_user":
            if membership.level < models.ADMIN_PERM:
                raise PermissionDenied
            user = OepUser.objects.get(id=request.POST["user_id"])
            if user == request.user:
                errors["name"] = "You can not change your own permissions"
            else:
                membership = GroupMembership.objects.get(group=group, user=user)
                membership.level = request.POST["level"]
                membership.save()
        elif mode == "delete_group":
            if membership.level < models.ADMIN_PERM:
                raise PermissionDenied
            group.delete()
            return redirect("/user/groups")
        else:
            raise PermissionDenied
        return render(
            request,
            "login/change_form.html",
            {
                "group": group,
                "choices": GroupMembership.choices,
                "errors": errors,
                "is_admin": True,
            },
        )

    def __add_user(self, request, group):
        user = OepUser.objects.filter(id=request.POST["user_id"]).first()
        g = user.groups.add(group)
        g.save()
        return self.get(request)

class ProfileUpdateView(UpdateView, LoginRequiredMixin):
    """
    Autogenerate a update form for users.
    """

    model = OepUser
    fields = ["name", "affiliation", "email"]
    template_name_suffix = "_update_form"

class EditUserView(View):
    def get(self, request, user_id):
        if not request.user.id == int(user_id):
            raise PermissionDenied
        form = EditUserForm(instance=request.user)
        return render(request, "login/oepuser_edit_form.html", {"form": form})

    def post(self, request, user_id):
        if not request.user.id == int(user_id):
            raise PermissionDenied
        form = EditUserForm(request.POST, instance=request.user)
        if form.is_valid():
            form.save()
            return redirect("/user/profile/{id}".format(id=request.user.id))
        else:
            return render(request, "login/oepuser_edit_form.html", {"form": form})

class CreateUserView(View):
    def get(self, request):
        form = CreateUserForm()
        return render(request, "login/oepuser_create_form.html", {"form": form})

    def post(self, request):
        form = CreateUserForm(request.POST)
        if form.is_valid():
            user = form.save()
            return redirect("/user/profile/{id}".format(id=user.id))
        else:
            return render(request, "login/oepuser_create_form.html", {"form": form})

class DetachView(LoginRequiredMixin, View):
    def get(self, request):
        if request.user.is_native:
            raise PermissionDenied
        form = DetachForm(request.user)
        return render(request, "login/detach.html", {"form": form})

    def post(self, request):
        if request.user.is_native:
            raise PermissionDenied
        form = DetachForm(request.user, request.POST)
        if form.is_valid():
            form.save()
            return redirect("/")
        else:
            print(form.errors)
            return render(request, "login/detach.html", {"form": form})

class OEPPasswordChangeView(PasswordChangeView):
    template_name = "login/generic_form.html"
    success_url = "/"

class ActivationNoteView(FormView):
    template_name = "login/activate.html"
    form_class = ChangeEmailForm
    success_url = "user/activate"

    def form_valid(self, form):
        if self.request.user.is_anonymous or self.request.user.is_mail_verified:
            raise PermissionError
        form.save(self.request.user)
        return super(ActivationNoteView, self).form_valid(form)

def activate(request, token):
    token_obj = models.ActivationToken.objects.filter(value=token).first()
    if not token_obj:
        form = ChangeEmailForm()
        form._errors = {
            forms.forms.NON_FIELD_ERRORS: form.error_class(
                ["Your token was invalid or expired"]
            )
        }
        return render(request, "login/activate.html", {"form": form})
    else:
        token_obj.user.is_mail_verified = True
        token_obj.user.save()
        token_obj.delete()
    return redirect("/user/profile/{id}".format(id=token_obj.user.id))
<commit_msg>Remove usage of render_to_response<commit_after>from django import forms
from django.contrib.auth import update_session_auth_hash
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.models import Group
from django.contrib.auth.views import PasswordChangeView, PasswordResetView
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
from django.http import Http404
from django.shortcuts import get_object_or_404, redirect, render
from django.views.generic import FormView, View
from django.views.generic.edit import UpdateView

import login.models as models

from .forms import ChangeEmailForm, CreateUserForm, DetachForm, EditUserForm
from .models import ADMIN_PERM, GroupMembership, UserGroup
from .models import myuser as OepUser

class ProfileView(View):
    def get(self, request, user_id):
        """
        Load the user identified by user_id and is OAuth-token. If latter does not exist yet, create one.
        :param request: A HTTP-request object sent by the Django framework.
        :param user_id: An user id
        :return: Profile renderer
        """
        from rest_framework.authtoken.models import Token

        for user in OepUser.objects.all():
            Token.objects.get_or_create(user=user)
        user = get_object_or_404(OepUser, pk=user_id)
        token = None
        if request.user.is_authenticated:
            token = Token.objects.get(user=request.user)
        return render(
            request, "login/profile.html", {"profile_user": user, "token": token}
        )

class GroupManagement(View, LoginRequiredMixin):
    def get(self, request):
        """
        Load and list the available groups by groupadmin.
        :param request: A HTTP-request object sent by the Django framework.
        :param user_id: An user id
        :return: Profile renderer
        """

        membership = request.user.memberships
        return render(
            request, "login/list_memberships.html", {"membership": membership}
        )

class GroupCreate(View, LoginRequiredMixin):
    def get(self, request, group_id=None):
        """
        Load the chosen action(create or edit) for a group.
        :param request: A HTTP-request object sent by the Django framework.
        :param user_id: An user id
        :param user_id: An group id
        :return: Profile renderer
        """
        group = None
        if group_id:
            group = UserGroup.objects.get(id=group_id)
            membership = get_object_or_404(
                GroupMembership, group=group, user=request.user
            )
            if membership.level < ADMIN_PERM:
                raise PermissionDenied
        return render(request, "login/group_create.html", {"group": group})

    def post(self, request, group_id=None):
        """
        Performs selected action(save or delete) for a group. If a groupname already exists, then a error
        will be output.
        The selected users become members of this group. The groupadmin is already set.
        :param request: A HTTP-request object sent by the Django framework.
        :param user_id: An user id
        :param user_id: An group id
        :return: Profile renderer
        """

        if group_id:
            group = UserGroup.objects.get(id=group_id)
            membership = get_object_or_404(
                GroupMembership, group=group, user=request.user
            )
            if membership.level < ADMIN_PERM:
                raise PermissionDenied
            group.name = request.POST["name"]
            group.description = request.POST["description"]
            group.save()
        else:
            group = UserGroup.objects.create(name=request.POST["name"])
            group.save()
            membership = GroupMembership.objects.create(
                user=request.user, group=group, level=ADMIN_PERM
            )
            membership.save()
        return redirect("/user/groups/{id}/members".format(id=group.id))

class GroupEdit(View, LoginRequiredMixin):
    def get(self, request, group_id):
        """
        Load the chosen action(create or edit) for a group.
        :param request: A HTTP-request object sent by the Django framework.
        :param user_id: An user id
        :param user_id: An group id
        :return: Profile renderer
        """
        group = get_object_or_404(UserGroup, pk=group_id)
        is_admin = False
        membership = GroupMembership.objects.filter(
            group=group, user=request.user
        ).first()
        if membership:
            is_admin = membership.level >= ADMIN_PERM
        return render(
            request,
            "login/change_form.html",
            {"group": group, "choices": GroupMembership.choices, "is_admin": is_admin},
        )

    def post(self, request, group_id):
        """
        Performs selected action(save or delete) for a group. If a groupname already exists, then a error
        will be output.
        The selected users become members of this group. The groupadmin is already set.
        :param request: A HTTP-request object sent by the Django framework.
        :param user_id: An user id
        :param user_id: An group id
        :return: Profile renderer
        """
        mode = request.POST["mode"]
        group = get_object_or_404(UserGroup, id=group_id)
        membership = get_object_or_404(GroupMembership, group=group, user=request.user)

        errors = {}
        if mode == "add_user":
            if membership.level < models.WRITE_PERM:
                raise PermissionDenied
            try:
                user = OepUser.objects.get(name=request.POST["name"])
                membership, _ = GroupMembership.objects.get_or_create(
                    group=group, user=user
                )
                membership.save()
            except OepUser.DoesNotExist:
                errors["name"] = "User does not exist"
        elif mode == "remove_user":
            if membership.level < models.DELETE_PERM:
                raise PermissionDenied
            user = OepUser.objects.get(id=request.POST["user_id"])
            membership = GroupMembership.objects.get(group=group, user=user)
            if membership.level >= ADMIN_PERM:
                admins = GroupMembership.objects.filter(group=group).exclude(user=user)
                if not admins:
                    errors["name"] = "A group needs at least one admin"
                else:
                    membership.delete()
            else:
                membership.delete()
        elif mode == "alter_user":
            if membership.level < models.ADMIN_PERM:
                raise PermissionDenied
            user = OepUser.objects.get(id=request.POST["user_id"])
            if user == request.user:
                errors["name"] = "You can not change your own permissions"
            else:
                membership = GroupMembership.objects.get(group=group, user=user)
                membership.level = request.POST["level"]
                membership.save()
        elif mode == "delete_group":
            if membership.level < models.ADMIN_PERM:
                raise PermissionDenied
            group.delete()
            return redirect("/user/groups")
        else:
            raise PermissionDenied
        return render(
            request,
            "login/change_form.html",
            {
                "group": group,
                "choices": GroupMembership.choices,
                "errors": errors,
                "is_admin": True,
            },
        )

    def __add_user(self, request, group):
        user = OepUser.objects.filter(id=request.POST["user_id"]).first()
        g = user.groups.add(group)
        g.save()
        return self.get(request)

class ProfileUpdateView(UpdateView, LoginRequiredMixin):
    """
    Autogenerate a update form for users.
    """

    model = OepUser
    fields = ["name", "affiliation", "email"]
    template_name_suffix = "_update_form"

class EditUserView(View):
    def get(self, request, user_id):
        if not request.user.id == int(user_id):
            raise PermissionDenied
        form = EditUserForm(instance=request.user)
        return render(request, "login/oepuser_edit_form.html", {"form": form})

    def post(self, request, user_id):
        if not request.user.id == int(user_id):
            raise PermissionDenied
        form = EditUserForm(request.POST, instance=request.user)
        if form.is_valid():
            form.save()
            return redirect("/user/profile/{id}".format(id=request.user.id))
        else:
            return render(request, "login/oepuser_edit_form.html", {"form": form})

class CreateUserView(View):
    def get(self, request):
        form = CreateUserForm()
        return render(request, "login/oepuser_create_form.html", {"form": form})

    def post(self, request):
        form = CreateUserForm(request.POST)
        if form.is_valid():
            user = form.save()
            return redirect("/user/profile/{id}".format(id=user.id))
        else:
            return render(request, "login/oepuser_create_form.html", {"form": form})

class DetachView(LoginRequiredMixin, View):
    def get(self, request):
        if request.user.is_native:
            raise PermissionDenied
        form = DetachForm(request.user)
        return render(request, "login/detach.html", {"form": form})

    def post(self, request):
        if request.user.is_native:
            raise PermissionDenied
        form = DetachForm(request.user, request.POST)
        if form.is_valid():
            form.save()
            return redirect("/")
        else:
            print(form.errors)
            return render(request, "login/detach.html", {"form": form})

class OEPPasswordChangeView(PasswordChangeView):
    template_name = "login/generic_form.html"
    success_url = "/"

class ActivationNoteView(FormView):
    template_name = "login/activate.html"
    form_class = ChangeEmailForm
    success_url = "user/activate"

    def form_valid(self, form):
        if self.request.user.is_anonymous or self.request.user.is_mail_verified:
            raise PermissionError
        form.save(self.request.user)
        return super(ActivationNoteView, self).form_valid(form)

def activate(request, token):
    token_obj = models.ActivationToken.objects.filter(value=token).first()
    if not token_obj:
        form = ChangeEmailForm()
        form._errors = {
            forms.forms.NON_FIELD_ERRORS: form.error_class(
                ["Your token was invalid or expired"]
            )
        }
        return render(request, "login/activate.html", {"form": form})
    else:
        token_obj.user.is_mail_verified = True
        token_obj.user.save()
        token_obj.delete()
    return redirect("/user/profile/{id}".format(id=token_obj.user.id))
<|endoftext|>


<commit_before>import functools

import click

from .tokenstore import token_storage_adapter

class LoginManager:
    def __init__(self):
        self._token_storage = token_storage_adapter()

    def has_login(self, resource_server: str):
        """
        Determines if the user has a refresh token for the given
        resource server
        """
        tokens = self._token_storage.get_token_data(resource_server)
        return tokens is not None and "refresh_token" in tokens

def requires_login(*args: str, pass_manager: bool = False):
    """
    Command decorator for specifying a resource server that the user must have
    tokens for in order to run the command.

    Simple usage for commands that have static resource needs: simply list all
    needed resource servers as args:

    @requries_login("auth.globus.org")

    @requires_login("auth.globus.org", "transfer.api.globus.org")

    Usage for commands which have dynamic resource servers depending
    on the arguments passed to the command (e.g. commands for the GCS API)

    @requies_login(pass_manager=True)
    def command(login_manager, endpoint_id)

        login_manager.<do the thing>(endpoint_id)

    """
    resource_servers = args

    def inner(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            manager = LoginManager()

            # determine the set of resource servers missing logins
            missing_servers = {s for s in resource_servers if not manager.has_login(s)}

            # if we are missing logins, assemble error text
            # text is slightly different for 1, 2, or 3+ missing servers
            if missing_servers:

                if len(missing_servers) == 1:
                    plural_string = ""
                    server_string = missing_servers.pop()

                elif len(missing_servers) == 2:
                    plural_string = "s"
                    server_string = "{} and {}".format(
                        missing_servers.pop(), missing_servers.pop()
                    )

                else:
                    plural_string = "s"
                    single_server = missing_servers.pop()
                    server_string = ", ".join(missing_servers) + ", and {}".format(
                        single_server
                    )

                raise click.ClickException(
                    "Missing login{} for {}, please run 'globus login'".format(
                        plural_string, server_string
                    )
                )

            # if pass_manager is True, pass it as an additional positional arg
            if pass_manager:
                return func(*args, manager, **kwargs)
            else:
                return func(*args, **kwargs)

        return wrapper

    return inner
<commit_msg>Minor refactor to clarify arg names<commit_after>import functools

import click

from .tokenstore import token_storage_adapter

class LoginManager:
    def __init__(self):
        self._token_storage = token_storage_adapter()

    def has_login(self, resource_server: str):
        """
        Determines if the user has a refresh token for the given
        resource server
        """
        tokens = self._token_storage.get_token_data(resource_server)
        return tokens is not None and "refresh_token" in tokens

def requires_login(*resource_servers: str, pass_manager: bool = False):
    """
    Command decorator for specifying a resource server that the user must have
    tokens for in order to run the command.

    Simple usage for commands that have static resource needs: simply list all
    needed resource servers as args:

    @requries_login("auth.globus.org")

    @requires_login("auth.globus.org", "transfer.api.globus.org")

    Usage for commands which have dynamic resource servers depending
    on the arguments passed to the command (e.g. commands for the GCS API)

    @requies_login(pass_manager=True)
    def command(login_manager, endpoint_id)

        login_manager.<do the thing>(endpoint_id)

    """

    def inner(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            manager = LoginManager()

            # determine the set of resource servers missing logins
            missing_servers = {s for s in resource_servers if not manager.has_login(s)}

            # if we are missing logins, assemble error text
            # text is slightly different for 1, 2, or 3+ missing servers
            if missing_servers:

                if len(missing_servers) == 1:
                    plural_string = ""
                    server_string = missing_servers.pop()

                elif len(missing_servers) == 2:
                    plural_string = "s"
                    server_string = "{} and {}".format(
                        missing_servers.pop(), missing_servers.pop()
                    )

                else:
                    plural_string = "s"
                    single_server = missing_servers.pop()
                    server_string = ", ".join(missing_servers) + ", and {}".format(
                        single_server
                    )

                raise click.ClickException(
                    "Missing login{} for {}, please run 'globus login'".format(
                        plural_string, server_string
                    )
                )

            # if pass_manager is True, pass it as an additional positional arg
            if pass_manager:
                return func(*args, manager, **kwargs)
            else:
                return func(*args, **kwargs)

        return wrapper

    return inner
<|endoftext|>


<commit_before><commit_msg>Import production settings<commit_after><|endoftext|>