<commit_before>import pandas as pd

# import PyQt5
try:
    from PyQt5.QtCore import QFile
    from PyQt5.QtCore import QIODevice
    from PyQt5.QtCore import pyqtSlot
    from PyQt5.QtWebChannel import QWebChannel
    from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEnginePage, QWebEngineScript
    from PyQt5.QtWidgets import QApplication
except ImportError:
    raise ImportError('PyQt5 is not installed. Please install PyQt5 to use '
                      'GUI related functions in py_entitymatching.')

from py_entitymatching.labeler.new_labeler.controller.FilterController import FilterController
from py_entitymatching.labeler.new_labeler.controller.LabelUpdateController import LabelUpdateController
from py_entitymatching.labeler.new_labeler.controller.TuplePairDisplayController import TuplePairDisplayController
from py_entitymatching.labeler.new_labeler.controller.StatsController import StatsController
from py_entitymatching.labeler.new_labeler.utils import ApplicationContext
from py_entitymatching.labeler.new_labeler.view import Renderer

import py_entitymatching as em
import six
from py_entitymatching.utils.validation_helper import validate_object_type

def initialize_tags_comments(df, comments_col, tags_col):
    """Creates or sets the DataFrame columns to be used for comments and tags.

    Args:
        df (DataFrame): DataFrame to which comments and tags column will be added.
        comments_col (str): Name of the comments column. This column wll be created and initialized to empty string
                        if it doesn't already exist.
        tags_col (str): Name of the tags column. This column wll be created and initialized to empty string
                        if it doesn't already exist.

    Returns:
        Data Frame (DataFrame): Complete DataFrame with comments and tags columns that will be used by the Labeler.

    Raises:
    """
    if comments_col not in df.columns:
        # initialize empty col
        df[comments_col] = ""
    if tags_col not in df.columns:
        # initialize empty col
        df[tags_col] = ""

    ApplicationContext.COMMENTS_COLUMN = comments_col
    ApplicationContext.TAGS_COLUMN = tags_col

    ApplicationContext.COMPLETE_DATA_FRAME = df
    ApplicationContext.current_data_frame = df
    return df

def suggest_tags_comments_column_name(df):
    """ Checks if the column names 'comments' and 'tags' are not used in the data frame. If not used
        suggests them as names for comments and tags column. Suggests empty strings otherwise.

    Args:
        df (DataFrame): DataFrame of tuple pairs

    Returns:
        [tags_col, comments_col] (str, str): Suggestions for Tags column name and Comments column name.

    Raises:
    """
    comments_col = ""
    tags_col = ""
    if "comments" not in df.columns:
        comments_col = "comments"
    if "tags" not in df.columns:
        tags_col = "tags"
    return [tags_col, comments_col]

def client_script():
    """ Reads qtwebchannel.js from disk and creates QWebEngineScript to inject to QT window.
        This allows for JavaScript code to call python methods marked by pyqtSlot in registered objects.

    Args:
        None.

    Returns:
        None.

    Raises:
    """
    qwebchannel_js = QFile(':/qtwebchannel/qwebchannel.js')
    if not qwebchannel_js.open(QIODevice.ReadOnly):
        raise SystemExit(
            'Failed to load qwebchannel.js with error: %s' %
            qwebchannel_js.errorString())
    qwebchannel_js = bytes(qwebchannel_js.readAll()).decode('utf-8')
    script = QWebEngineScript()
    script.setSourceCode(qwebchannel_js)
    script.setName('qWebChannelJS')
    script.setWorldId(QWebEngineScript.MainWorld)
    script.setInjectionPoint(QWebEngineScript.DocumentReady)
    script.setRunsOnSubFrames(True)
    return script

class MainPage(QWebEnginePage):
    """ Main web page of the labeler application. The HTML content of this will change in response to events."""

    def __init__(self):
        super(MainPage, self).__init__(None)

    @pyqtSlot(str, str)
    def respond(self, comments_col, tags_col):
        """ Called when user confirms name of comments and tags columns to be used. Renders the main page of the Labeler.

        Args:
            comments_col (str): Name of comments column confirmed by user.
            tags_col (str): Name of tags column confirmed by user.

        Returns:
            None.

        Raises:
            None.
        """
        initialize_tags_comments(ApplicationContext.COMPLETE_DATA_FRAME, comments_col, tags_col)
        html_str = Renderer.render_main_page(
            current_page_tuple_pairs=ApplicationContext.TUPLE_PAIR_DISPLAY_CONTROLLER.get_tuples_for_page(
                ApplicationContext.current_page_number),
            match_count=ApplicationContext.STATS_CONTROLLER.count_matched_tuple_pairs(
                ApplicationContext.current_data_frame, ApplicationContext.LABEL_COLUMN),
            not_match_count=ApplicationContext.STATS_CONTROLLER.count_non_matched_tuple_pairs(
                ApplicationContext.current_data_frame, ApplicationContext.LABEL_COLUMN),
            not_sure_count=ApplicationContext.STATS_CONTROLLER.count_not_sure_tuple_pairs(
                ApplicationContext.current_data_frame, ApplicationContext.LABEL_COLUMN),
            unlabeled_count=ApplicationContext.STATS_CONTROLLER.count_not_labeled_tuple_pairs(
                ApplicationContext.current_data_frame, ApplicationContext.LABEL_COLUMN)
        )
        self.setHtml(html_str)

def _validate_inputs(data_frame, label_column_name):
    validate_object_type(data_frame, pd.DataFrame)
    if data_frame.empty:
        raise AssertionError
    validate_object_type(label_column_name, six.string_types, error_prefix='Input attr.')

    # If the label column already exists, validate that the label column has only one of the 3 allowed values
    if label_column_name in data_frame.columns:
        label_values = data_frame[label_column_name].unique()
        if label_values.size == 0 or label_values.size > 4:
            raise AssertionError
        for label in label_values:
            if label not in ["Not-Labeled", "Not-Matched", "Not-Sure", "Yes"]:
                raise AssertionError
    return True

def new_label_table(df, label_column_name):
    """ Method to be invoked to launch the Labeler application.

    Args:
        df (Dataframe): A Dataframe containing the tuple pairs that are possible matches
        label_column_name (str): Name of column to be used to save tuple pair labels.
                                This column will be created if it doesn't already exist.

    Returns:
        The updated Dataframe with the label column, comments, and tags

    Raises:
        AssertionError: If `table` is not of type pandas DataFrame.
        AssertionError: If `label_column_name` is not of type string.
    """
    _validate_inputs(df, label_column_name)

    # Remove warning for this case
    pd.options.mode.chained_assignment = None  # default='warn'
    if label_column_name not in df.columns:
        # Add label column
        df[label_column_name] = "Not-Labeled"
    ApplicationContext.LABEL_COLUMN = label_column_name

    # Get list of left table and right table attributes
    attrs = list(df.columns.values)
    r_attrs = []
    l_attrs = []
    for att in attrs:
        if 'ltable' in att:
            l_attrs.append(att[7:])
        elif 'rtable' in att:
            r_attrs.append(att[7:])

    attributes = []
    for l_att in l_attrs:
        if l_att in r_attrs:
            attributes.append(l_att)

    ApplicationContext.current_attributes = attributes
    ApplicationContext.ALL_ATTRIBUTES = ApplicationContext.current_attributes

    ApplicationContext.COMPLETE_DATA_FRAME = df
    ApplicationContext.current_data_frame = df

    [tags_col, comments_col] = suggest_tags_comments_column_name(ApplicationContext.COMPLETE_DATA_FRAME)

    em._viewapp = QApplication.instance()
    if em._viewapp is None:
        em._viewapp = QApplication([])

    view = QWebEngineView()
    main_page = MainPage()
    # main_page.profile().clearHttpCache()
    main_page.profile().scripts().insert(client_script())  # insert QT web channel JS to allow for communication
    main_page.setHtml(Renderer.render_options_page(tags_col, comments_col))
    view.setPage(main_page)

    # create channel of communication between HTML & Py
    channel = QWebChannel(main_page)
    main_page.setWebChannel(channel)

    # add controllers to the channel
    filter_controller = FilterController(main_page)
    stats_controller = StatsController(main_page)
    tuple_pair_display_controller = TuplePairDisplayController(main_page)
    label_controller = LabelUpdateController(main_page)

    ApplicationContext.FILTER_CONTROLLER = filter_controller
    ApplicationContext.STATS_CONTROLLER = stats_controller
    ApplicationContext.TUPLE_PAIR_DISPLAY_CONTROLLER = tuple_pair_display_controller
    ApplicationContext.LABEL_CONTROLLER = label_controller

    channel.registerObject('bridge', main_page)
    channel.registerObject('filter_controller', filter_controller)
    channel.registerObject('stats_controller', stats_controller)
    channel.registerObject('tuple_pair_display_controller', tuple_pair_display_controller)
    channel.registerObject('label_controller', label_controller)
    view.show()

    app = em._viewapp
    app.exec_()

    return df
    # return ApplicationContext.COMPLETE_DATA_FRAME
<commit_msg>throw exception in new_labeler() if python version < 3.5<commit_after>import pandas as pd

# import PyQt5
import sys

try:
    from PyQt5.QtCore import QFile
    from PyQt5.QtCore import QIODevice
    from PyQt5.QtCore import pyqtSlot
    from PyQt5.QtWebChannel import QWebChannel
    from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEnginePage, QWebEngineScript
    from PyQt5.QtWidgets import QApplication
except ImportError:
    raise ImportError('PyQt5 is not installed. Please install PyQt5 to use '
                      'GUI related functions in py_entitymatching.')

from py_entitymatching.labeler.new_labeler.controller.FilterController import FilterController
from py_entitymatching.labeler.new_labeler.controller.LabelUpdateController import LabelUpdateController
from py_entitymatching.labeler.new_labeler.controller.TuplePairDisplayController import TuplePairDisplayController
from py_entitymatching.labeler.new_labeler.controller.StatsController import StatsController
from py_entitymatching.labeler.new_labeler.utils import ApplicationContext
from py_entitymatching.labeler.new_labeler.view import Renderer

import py_entitymatching as em
import six
from py_entitymatching.utils.validation_helper import validate_object_type

def initialize_tags_comments(df, comments_col, tags_col):
    """Creates or sets the DataFrame columns to be used for comments and tags.

    Args:
        df (DataFrame): DataFrame to which comments and tags column will be added.
        comments_col (str): Name of the comments column. This column wll be created and initialized to empty string
                        if it doesn't already exist.
        tags_col (str): Name of the tags column. This column wll be created and initialized to empty string
                        if it doesn't already exist.

    Returns:
        Data Frame (DataFrame): Complete DataFrame with comments and tags columns that will be used by the Labeler.

    Raises:
    """
    if comments_col not in df.columns:
        # initialize empty col
        df[comments_col] = ""
    if tags_col not in df.columns:
        # initialize empty col
        df[tags_col] = ""

    ApplicationContext.COMMENTS_COLUMN = comments_col
    ApplicationContext.TAGS_COLUMN = tags_col

    ApplicationContext.COMPLETE_DATA_FRAME = df
    ApplicationContext.current_data_frame = df
    return df

def suggest_tags_comments_column_name(df):
    """ Checks if the column names 'comments' and 'tags' are not used in the data frame. If not used
        suggests them as names for comments and tags column. Suggests empty strings otherwise.

    Args:
        df (DataFrame): DataFrame of tuple pairs

    Returns:
        [tags_col, comments_col] (str, str): Suggestions for Tags column name and Comments column name.

    Raises:
    """
    comments_col = ""
    tags_col = ""
    if "comments" not in df.columns:
        comments_col = "comments"
    if "tags" not in df.columns:
        tags_col = "tags"
    return [tags_col, comments_col]

def client_script():
    """ Reads qtwebchannel.js from disk and creates QWebEngineScript to inject to QT window.
        This allows for JavaScript code to call python methods marked by pyqtSlot in registered objects.

    Args:
        None.

    Returns:
        None.

    Raises:
    """
    qwebchannel_js = QFile(':/qtwebchannel/qwebchannel.js')
    if not qwebchannel_js.open(QIODevice.ReadOnly):
        raise SystemExit(
            'Failed to load qwebchannel.js with error: %s' %
            qwebchannel_js.errorString())
    qwebchannel_js = bytes(qwebchannel_js.readAll()).decode('utf-8')
    script = QWebEngineScript()
    script.setSourceCode(qwebchannel_js)
    script.setName('qWebChannelJS')
    script.setWorldId(QWebEngineScript.MainWorld)
    script.setInjectionPoint(QWebEngineScript.DocumentReady)
    script.setRunsOnSubFrames(True)
    return script

class MainPage(QWebEnginePage):
    """ Main web page of the labeler application. The HTML content of this will change in response to events."""

    def __init__(self):
        super(MainPage, self).__init__(None)

    @pyqtSlot(str, str)
    def respond(self, comments_col, tags_col):
        """ Called when user confirms name of comments and tags columns to be used. Renders the main page of the Labeler.

        Args:
            comments_col (str): Name of comments column confirmed by user.
            tags_col (str): Name of tags column confirmed by user.

        Returns:
            None.

        Raises:
            None.
        """
        initialize_tags_comments(ApplicationContext.COMPLETE_DATA_FRAME, comments_col, tags_col)
        html_str = Renderer.render_main_page(
            current_page_tuple_pairs=ApplicationContext.TUPLE_PAIR_DISPLAY_CONTROLLER.get_tuples_for_page(
                ApplicationContext.current_page_number),
            match_count=ApplicationContext.STATS_CONTROLLER.count_matched_tuple_pairs(
                ApplicationContext.current_data_frame, ApplicationContext.LABEL_COLUMN),
            not_match_count=ApplicationContext.STATS_CONTROLLER.count_non_matched_tuple_pairs(
                ApplicationContext.current_data_frame, ApplicationContext.LABEL_COLUMN),
            not_sure_count=ApplicationContext.STATS_CONTROLLER.count_not_sure_tuple_pairs(
                ApplicationContext.current_data_frame, ApplicationContext.LABEL_COLUMN),
            unlabeled_count=ApplicationContext.STATS_CONTROLLER.count_not_labeled_tuple_pairs(
                ApplicationContext.current_data_frame, ApplicationContext.LABEL_COLUMN)
        )
        self.setHtml(html_str)

def _validate_inputs(data_frame, label_column_name):
    validate_object_type(data_frame, pd.DataFrame)
    if data_frame.empty:
        raise AssertionError
    validate_object_type(label_column_name, six.string_types, error_prefix='Input attr.')

    # If the label column already exists, validate that the label column has only one of the 3 allowed values
    if label_column_name in data_frame.columns:
        label_values = data_frame[label_column_name].unique()
        if label_values.size == 0 or label_values.size > 4:
            raise AssertionError
        for label in label_values:
            if label not in ["Not-Labeled", "Not-Matched", "Not-Sure", "Yes"]:
                raise AssertionError
    return True

def new_label_table(df, label_column_name):
    """ Method to be invoked to launch the Labeler application.

    Args:
        df (Dataframe): A Dataframe containing the tuple pairs that are possible matches
        label_column_name (str): Name of column to be used to save tuple pair labels.
                                This column will be created if it doesn't already exist.

    Returns:
        The updated Dataframe with the label column, comments, and tags

    Raises:
        AssertionError: If `table` is not of type pandas DataFrame.
        AssertionError: If `label_column_name` is not of type string.
        ImportError: If python version is less than 3.5
    """
    if sys.version_info < (3, 5):
        raise ImportError("Python 3.3 or greater is required")
    _validate_inputs(df, label_column_name)

    # Remove warning for this case
    pd.options.mode.chained_assignment = None  # default='warn'
    if label_column_name not in df.columns:
        # Add label column
        df[label_column_name] = "Not-Labeled"
    ApplicationContext.LABEL_COLUMN = label_column_name

    # Get list of left table and right table attributes
    attrs = list(df.columns.values)
    r_attrs = []
    l_attrs = []
    for att in attrs:
        if 'ltable' in att:
            l_attrs.append(att[7:])
        elif 'rtable' in att:
            r_attrs.append(att[7:])

    attributes = []
    for l_att in l_attrs:
        if l_att in r_attrs:
            attributes.append(l_att)

    ApplicationContext.current_attributes = attributes
    ApplicationContext.ALL_ATTRIBUTES = ApplicationContext.current_attributes

    ApplicationContext.COMPLETE_DATA_FRAME = df
    ApplicationContext.current_data_frame = df

    [tags_col, comments_col] = suggest_tags_comments_column_name(ApplicationContext.COMPLETE_DATA_FRAME)

    em._viewapp = QApplication.instance()
    if em._viewapp is None:
        em._viewapp = QApplication([])

    view = QWebEngineView()
    main_page = MainPage()
    # main_page.profile().clearHttpCache()
    main_page.profile().scripts().insert(client_script())  # insert QT web channel JS to allow for communication
    main_page.setHtml(Renderer.render_options_page(tags_col, comments_col))
    view.setPage(main_page)

    # create channel of communication between HTML & Py
    channel = QWebChannel(main_page)
    main_page.setWebChannel(channel)

    # add controllers to the channel
    filter_controller = FilterController(main_page)
    stats_controller = StatsController(main_page)
    tuple_pair_display_controller = TuplePairDisplayController(main_page)
    label_controller = LabelUpdateController(main_page)

    ApplicationContext.FILTER_CONTROLLER = filter_controller
    ApplicationContext.STATS_CONTROLLER = stats_controller
    ApplicationContext.TUPLE_PAIR_DISPLAY_CONTROLLER = tuple_pair_display_controller
    ApplicationContext.LABEL_CONTROLLER = label_controller

    channel.registerObject('bridge', main_page)
    channel.registerObject('filter_controller', filter_controller)
    channel.registerObject('stats_controller', stats_controller)
    channel.registerObject('tuple_pair_display_controller', tuple_pair_display_controller)
    channel.registerObject('label_controller', label_controller)
    view.show()

    app = em._viewapp
    app.exec_()

    return df
    # return ApplicationContext.COMPLETE_DATA_FRAME
<|endoftext|>


<commit_before>#
# Copyright (C) 2011 Google Inc. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
#         * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#         * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following disclaimer
# in the documentation and/or other materials provided with the
# distribution.
#         * Neither the name of Google Inc. nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#

{
    'includes': [
        'WinPrecompile.gypi',
        '../../bindings/bindings.gypi',
        '../../core/core.gypi',
        '../../core/features.gypi',
        '../../modules/modules.gypi',
        '../../web/web.gypi',
        '../../wtf/wtf.gypi',
    ],
    'targets': [
        {
            'target_name': 'webkit',
            'type': '<(component)',
            'variables': { 'enable_wexit_time_destructors': 1, },
            'dependencies': [
                '../../core/core.gyp:webcore',
                '../../modules/modules.gyp:modules',
                '<(DEPTH)/skia/skia.gyp:skia',
                '<(DEPTH)/third_party/angle_dx11/src/build_angle.gyp:translator_glsl',
                '<(DEPTH)/third_party/icu/icu.gyp:icuuc',
                '<(DEPTH)/third_party/npapi/npapi.gyp:npapi',
                '<(DEPTH)/v8/tools/gyp/v8.gyp:v8',
                'blink_common',
            ],
            'export_dependent_settings': [
                '<(DEPTH)/skia/skia.gyp:skia',
                '<(DEPTH)/third_party/icu/icu.gyp:icuuc',
                '<(DEPTH)/third_party/npapi/npapi.gyp:npapi',
                '<(DEPTH)/v8/tools/gyp/v8.gyp:v8',
            ],
            'include_dirs': [
                '../../../public/web',
                '../../web',
                '<(DEPTH)/third_party/angle_dx11/include',
                '<(DEPTH)/third_party/skia/include/utils',
            ],
            'defines': [
                'WEBKIT_IMPLEMENTATION=1',
                'INSIDE_WEBKIT',
            ],
            'sources': [
                '<@(webcore_platform_support_files)',
                '<@(web_files)',
            ],
            'conditions': [
                ['component=="shared_library"', {
                    'dependencies': [
                        '../../core/core.gyp:webcore_derived',
                        '../../core/core.gyp:webcore_test_support',
                        '<(DEPTH)/base/base.gyp:test_support_base',
                        '<(DEPTH)/testing/gmock.gyp:gmock',
                        '<(DEPTH)/testing/gtest.gyp:gtest',
                        '<(DEPTH)/third_party/icu/icu.gyp:*',
                        '<(libjpeg_gyp_path):libjpeg',
                        '<(DEPTH)/third_party/libpng/libpng.gyp:libpng',
                        '<(DEPTH)/third_party/libwebp/libwebp.gyp:libwebp',
                        '<(DEPTH)/third_party/libxml/libxml.gyp:libxml',
                        '<(DEPTH)/third_party/libxslt/libxslt.gyp:libxslt',
                        '<(DEPTH)/third_party/modp_b64/modp_b64.gyp:modp_b64',
                        '<(DEPTH)/third_party/ots/ots.gyp:ots',
                        '<(DEPTH)/third_party/zlib/zlib.gyp:zlib',
                        '<(DEPTH)/url/url.gyp:url_lib',
                        '<(DEPTH)/v8/tools/gyp/v8.gyp:v8',
                        # We must not add webkit_support here because of cyclic dependency.
                    ],
                    'export_dependent_settings': [
                        '<(DEPTH)/url/url.gyp:url_lib',
                        '<(DEPTH)/v8/tools/gyp/v8.gyp:v8',
                    ],
                    'include_dirs': [
                        # WARNING: Do not view this particular case as a precedent for
                        # including WebCore headers in DumpRenderTree project.
                        '../../core/testing/v8', # for WebCoreTestSupport.h, needed to link in window.internals code.
                    ],
                    'sources': [
                        '<@(bindings_unittest_files)',
                        '<@(core_unittest_files)',
                        '<@(modules_unittest_files)',
                        '<@(web_unittest_files)',
                        '../../web/WebTestingSupport.cpp',
                        '../../web/tests/WebUnitTests.cpp',   # Components test runner support.
                    ],
                    'conditions': [
                        ['OS=="win" or OS=="mac"', {
                            'dependencies': [
                                '<(DEPTH)/third_party/nss/nss.gyp:*',
                            ],
                        }],
                        ['clang==1', {
                            # FIXME: It would be nice to enable this in shared builds too,
                            # but the test files have global constructors from the GTEST macro
                            # and we pull in the test files into the webkit target in the
                            # shared build.
                            'cflags!': ['-Wglobal-constructors'],
                            'xcode_settings': {
                              'WARNING_CFLAGS!': ['-Wglobal-constructors'],
                            },
                        }],
                    ],
                    'msvs_settings': {
                      'VCLinkerTool': {
                        'conditions': [
                          ['incremental_chrome_dll==1', {
                            'UseLibraryDependencyInputs': "true",
                          }],
                        ],
                      },
                    },
                }],
                ['OS == "linux"', {
                    'dependencies': [
                        '<(DEPTH)/build/linux/system.gyp:fontconfig',
                    ],
                    'include_dirs': [
                        '../../../public/web/linux',
                    ],
                }, {
                    'sources/': [
                        ['exclude', '/linux/'],
                    ],
                }],
                ['use_x11 == 1', {
                    'dependencies': [
                        '<(DEPTH)/build/linux/system.gyp:x11',
                    ],
                    'include_dirs': [
                        '../../../public/web/x11',
                    ],
                }, {
                    'sources/': [
                        ['exclude', '/x11/'],
                    ]
                }],
                ['toolkit_uses_gtk == 1', {
                    'dependencies': [
                        '<(DEPTH)/build/linux/system.gyp:gtk',
                    ],
                    'include_dirs': [
                        '../../../public/web/gtk',
                    ],
                }, { # else: toolkit_uses_gtk != 1
                    'sources/': [
                        ['exclude', '/gtk/'],
                    ],
                }],
                ['OS=="android"', {
                    'include_dirs': [
                        '../../../public/web/android',
                        '../../../public/web/linux', # We need linux/WebFontRendering.h on Android.
                    ],
                }, { # else: OS!="android"
                    'sources/': [
                        ['exclude', '/android/'],
                    ],
                }],
                ['OS=="mac"', {
                    'include_dirs': [
                        '../../../public/web/mac',
                    ],
                    'link_settings': {
                        'libraries': [
                            '$(SDKROOT)/System/Library/Frameworks/Accelerate.framework',
                            '$(SDKROOT)/System/Library/Frameworks/OpenGL.framework',
                        ],
                    },
                }, { # else: OS!="mac"
                    'sources/': [
                        ['exclude', '/mac/'],
                    ],
                }],
                ['OS=="win"', {
                    'include_dirs': [
                        '../../../public/web/win',
                    ],
                }, { # else: OS!="win"
                    'sources/': [['exclude', '/win/']],
                    'variables': {
                        # FIXME: Turn on warnings on Windows.
                        'chromium_code': 1,
                    }
                }],
                ['use_default_render_theme==1', {
                    'include_dirs': [
                        '../../../public/web/default',
                    ],
                }, { # else use_default_render_theme==0
                    'sources/': [
                        ['exclude', '../../web/default/WebRenderTheme.cpp'],
                    ],
                }],
            ],
            'direct_dependent_settings': {
                'include_dirs': [
                    '../../../',
                ],
            },
            'target_conditions': [
                ['OS=="android"', {
                    'sources/': [
                        ['include', '^../../web/linux/WebFontRendering\\.cpp$'],
                        ['include', '^../../web/linux/WebFontRenderStyle\\.cpp$'],
                    ],
                }],
            ],
        },
        {
            'target_name': 'webkit_test_support',
            'conditions': [
                ['component=="shared_library"', {
                    'type': 'none',
                }, {
                    'type': 'static_library',
                    'dependencies': [
                        '../../wtf/wtf.gyp:wtf',
                        '../../core/core.gyp:webcore_test_support',
                    ],
                    'include_dirs': [
                        '../../../public/web',
                        '../../core/testing/v8', # for WebCoreTestSupport.h, needed to link in window.internals code.
                        '../../../',
                    ],
                    'sources': [
                        '../../web/WebTestingSupport.cpp',
                    ],
                }],
            ],
        },
        {
            'target_name': 'blink_common',
            'type': '<(component)',
            'variables': { 'enable_wexit_time_destructors': 1 },
            'dependencies': [
                '../../wtf/wtf.gyp:wtf',
                '<(DEPTH)/skia/skia.gyp:skia',
            ],
            'defines': [
                'INSIDE_WEBKIT',
                'BLINK_COMMON_IMPLEMENTATION=1',
            ],
            'include_dirs': [
                '../..',
                '../../..',
            ],
            'sources': [
                '../../core/platform/chromium/support/WebCString.cpp',
                '../../core/platform/chromium/support/WebString.cpp',
                '../../web/WebCommon.cpp',
            ],
        },
    ], # targets
    'conditions': [
        ['gcc_version>=46', {
            'target_defaults': {
                # Disable warnings about c++0x compatibility, as some names (such
                # as nullptr) conflict with upcoming c++0x types.
                'cflags_cc': ['-Wno-c++0x-compat'],
            },
        }],
        ['OS=="mac"', {
            'targets': [
                {
                    'target_name': 'copy_mesa',
                    'type': 'none',
                    'dependencies': ['<(DEPTH)/third_party/mesa/mesa.gyp:osmesa'],
                    'copies': [{
                        'destination': '<(PRODUCT_DIR)/DumpRenderTree.app/Contents/MacOS/',
                        'files': ['<(PRODUCT_DIR)/osmesa.so'],
                    }],
                },
            ],
        }],
        ['clang==1', {
            'target_defaults': {
                'cflags': ['-Wglobal-constructors'],
                'xcode_settings': {
                    'WARNING_CFLAGS': ['-Wglobal-constructors'],
                },
            },
        }],
    ], # conditions
}
<commit_msg>add dependency on v8 to blink_minimal<commit_after>#
# Copyright (C) 2011 Google Inc. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
#         * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#         * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following disclaimer
# in the documentation and/or other materials provided with the
# distribution.
#         * Neither the name of Google Inc. nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#

{
    'includes': [
        'WinPrecompile.gypi',
        '../../bindings/bindings.gypi',
        '../../core/core.gypi',
        '../../core/features.gypi',
        '../../modules/modules.gypi',
        '../../web/web.gypi',
        '../../wtf/wtf.gypi',
    ],
    'targets': [
        {
            'target_name': 'webkit',
            'type': '<(component)',
            'variables': { 'enable_wexit_time_destructors': 1, },
            'dependencies': [
                '../../core/core.gyp:webcore',
                '../../modules/modules.gyp:modules',
                '<(DEPTH)/skia/skia.gyp:skia',
                '<(DEPTH)/third_party/angle_dx11/src/build_angle.gyp:translator_glsl',
                '<(DEPTH)/third_party/icu/icu.gyp:icuuc',
                '<(DEPTH)/third_party/npapi/npapi.gyp:npapi',
                '<(DEPTH)/v8/tools/gyp/v8.gyp:v8',
                'blink_common',
            ],
            'export_dependent_settings': [
                '<(DEPTH)/skia/skia.gyp:skia',
                '<(DEPTH)/third_party/icu/icu.gyp:icuuc',
                '<(DEPTH)/third_party/npapi/npapi.gyp:npapi',
                '<(DEPTH)/v8/tools/gyp/v8.gyp:v8',
            ],
            'include_dirs': [
                '../../../public/web',
                '../../web',
                '<(DEPTH)/third_party/angle_dx11/include',
                '<(DEPTH)/third_party/skia/include/utils',
            ],
            'defines': [
                'WEBKIT_IMPLEMENTATION=1',
                'INSIDE_WEBKIT',
            ],
            'sources': [
                '<@(webcore_platform_support_files)',
                '<@(web_files)',
            ],
            'conditions': [
                ['component=="shared_library"', {
                    'dependencies': [
                        '../../core/core.gyp:webcore_derived',
                        '../../core/core.gyp:webcore_test_support',
                        '<(DEPTH)/base/base.gyp:test_support_base',
                        '<(DEPTH)/testing/gmock.gyp:gmock',
                        '<(DEPTH)/testing/gtest.gyp:gtest',
                        '<(DEPTH)/third_party/icu/icu.gyp:*',
                        '<(libjpeg_gyp_path):libjpeg',
                        '<(DEPTH)/third_party/libpng/libpng.gyp:libpng',
                        '<(DEPTH)/third_party/libwebp/libwebp.gyp:libwebp',
                        '<(DEPTH)/third_party/libxml/libxml.gyp:libxml',
                        '<(DEPTH)/third_party/libxslt/libxslt.gyp:libxslt',
                        '<(DEPTH)/third_party/modp_b64/modp_b64.gyp:modp_b64',
                        '<(DEPTH)/third_party/ots/ots.gyp:ots',
                        '<(DEPTH)/third_party/zlib/zlib.gyp:zlib',
                        '<(DEPTH)/url/url.gyp:url_lib',
                        '<(DEPTH)/v8/tools/gyp/v8.gyp:v8',
                        # We must not add webkit_support here because of cyclic dependency.
                    ],
                    'export_dependent_settings': [
                        '<(DEPTH)/url/url.gyp:url_lib',
                        '<(DEPTH)/v8/tools/gyp/v8.gyp:v8',
                    ],
                    'include_dirs': [
                        # WARNING: Do not view this particular case as a precedent for
                        # including WebCore headers in DumpRenderTree project.
                        '../../core/testing/v8', # for WebCoreTestSupport.h, needed to link in window.internals code.
                    ],
                    'sources': [
                        '<@(bindings_unittest_files)',
                        '<@(core_unittest_files)',
                        '<@(modules_unittest_files)',
                        '<@(web_unittest_files)',
                        '../../web/WebTestingSupport.cpp',
                        '../../web/tests/WebUnitTests.cpp',   # Components test runner support.
                    ],
                    'conditions': [
                        ['OS=="win" or OS=="mac"', {
                            'dependencies': [
                                '<(DEPTH)/third_party/nss/nss.gyp:*',
                            ],
                        }],
                        ['clang==1', {
                            # FIXME: It would be nice to enable this in shared builds too,
                            # but the test files have global constructors from the GTEST macro
                            # and we pull in the test files into the webkit target in the
                            # shared build.
                            'cflags!': ['-Wglobal-constructors'],
                            'xcode_settings': {
                              'WARNING_CFLAGS!': ['-Wglobal-constructors'],
                            },
                        }],
                    ],
                    'msvs_settings': {
                      'VCLinkerTool': {
                        'conditions': [
                          ['incremental_chrome_dll==1', {
                            'UseLibraryDependencyInputs': "true",
                          }],
                        ],
                      },
                    },
                }],
                ['OS == "linux"', {
                    'dependencies': [
                        '<(DEPTH)/build/linux/system.gyp:fontconfig',
                    ],
                    'include_dirs': [
                        '../../../public/web/linux',
                    ],
                }, {
                    'sources/': [
                        ['exclude', '/linux/'],
                    ],
                }],
                ['use_x11 == 1', {
                    'dependencies': [
                        '<(DEPTH)/build/linux/system.gyp:x11',
                    ],
                    'include_dirs': [
                        '../../../public/web/x11',
                    ],
                }, {
                    'sources/': [
                        ['exclude', '/x11/'],
                    ]
                }],
                ['toolkit_uses_gtk == 1', {
                    'dependencies': [
                        '<(DEPTH)/build/linux/system.gyp:gtk',
                    ],
                    'include_dirs': [
                        '../../../public/web/gtk',
                    ],
                }, { # else: toolkit_uses_gtk != 1
                    'sources/': [
                        ['exclude', '/gtk/'],
                    ],
                }],
                ['OS=="android"', {
                    'include_dirs': [
                        '../../../public/web/android',
                        '../../../public/web/linux', # We need linux/WebFontRendering.h on Android.
                    ],
                }, { # else: OS!="android"
                    'sources/': [
                        ['exclude', '/android/'],
                    ],
                }],
                ['OS=="mac"', {
                    'include_dirs': [
                        '../../../public/web/mac',
                    ],
                    'link_settings': {
                        'libraries': [
                            '$(SDKROOT)/System/Library/Frameworks/Accelerate.framework',
                            '$(SDKROOT)/System/Library/Frameworks/OpenGL.framework',
                        ],
                    },
                }, { # else: OS!="mac"
                    'sources/': [
                        ['exclude', '/mac/'],
                    ],
                }],
                ['OS=="win"', {
                    'include_dirs': [
                        '../../../public/web/win',
                    ],
                }, { # else: OS!="win"
                    'sources/': [['exclude', '/win/']],
                    'variables': {
                        # FIXME: Turn on warnings on Windows.
                        'chromium_code': 1,
                    }
                }],
                ['use_default_render_theme==1', {
                    'include_dirs': [
                        '../../../public/web/default',
                    ],
                }, { # else use_default_render_theme==0
                    'sources/': [
                        ['exclude', '../../web/default/WebRenderTheme.cpp'],
                    ],
                }],
            ],
            'direct_dependent_settings': {
                'include_dirs': [
                    '../../../',
                ],
            },
            'target_conditions': [
                ['OS=="android"', {
                    'sources/': [
                        ['include', '^../../web/linux/WebFontRendering\\.cpp$'],
                        ['include', '^../../web/linux/WebFontRenderStyle\\.cpp$'],
                    ],
                }],
            ],
        },
        {
            'target_name': 'webkit_test_support',
            'conditions': [
                ['component=="shared_library"', {
                    'type': 'none',
                }, {
                    'type': 'static_library',
                    'dependencies': [
                        '../../wtf/wtf.gyp:wtf',
                        '../../core/core.gyp:webcore_test_support',
                    ],
                    'include_dirs': [
                        '../../../public/web',
                        '../../core/testing/v8', # for WebCoreTestSupport.h, needed to link in window.internals code.
                        '../../../',
                    ],
                    'sources': [
                        '../../web/WebTestingSupport.cpp',
                    ],
                }],
            ],
        },
        {
            'target_name': 'blink_common',
            'type': '<(component)',
            'variables': { 'enable_wexit_time_destructors': 1 },
            'dependencies': [
                '../../wtf/wtf.gyp:wtf',
                '<(DEPTH)/skia/skia.gyp:skia',
                '<(DEPTH)/v8/tools/gyp/v8.gyp:v8',
            ],
            'export_dependent_settings': [
              '<(DEPTH)/v8/tools/gyp/v8.gyp:v8',
            ],
            'defines': [
                'INSIDE_WEBKIT',
                'BLINK_COMMON_IMPLEMENTATION=1',
            ],
            'include_dirs': [
                '../..',
                '../../..',
            ],
            'sources': [
                '../../core/platform/chromium/support/WebCString.cpp',
                '../../core/platform/chromium/support/WebString.cpp',
                '../../web/WebCommon.cpp',
            ],
        },
    ], # targets
    'conditions': [
        ['gcc_version>=46', {
            'target_defaults': {
                # Disable warnings about c++0x compatibility, as some names (such
                # as nullptr) conflict with upcoming c++0x types.
                'cflags_cc': ['-Wno-c++0x-compat'],
            },
        }],
        ['OS=="mac"', {
            'targets': [
                {
                    'target_name': 'copy_mesa',
                    'type': 'none',
                    'dependencies': ['<(DEPTH)/third_party/mesa/mesa.gyp:osmesa'],
                    'copies': [{
                        'destination': '<(PRODUCT_DIR)/DumpRenderTree.app/Contents/MacOS/',
                        'files': ['<(PRODUCT_DIR)/osmesa.so'],
                    }],
                },
            ],
        }],
        ['clang==1', {
            'target_defaults': {
                'cflags': ['-Wglobal-constructors'],
                'xcode_settings': {
                    'WARNING_CFLAGS': ['-Wglobal-constructors'],
                },
            },
        }],
    ], # conditions
}
<|endoftext|>


<commit_before>from datetime import datetime
import socket

from utilities.toUTC import toUTC

class Event(dict):

    # We set default vaules so that we can later
    # create an alert around these, and know when events
    # have to use defaults
    DEFAULT_STRING = 'UNKNOWN'

    def add_required_fields(self):
        if 'receivedtimestamp' not in self:
            self['receivedtimestamp'] = toUTC(datetime.now()).isoformat()
        if 'utctimestamp' not in self:
            self['utctimestamp'] = toUTC(datetime.now()).isoformat()
        if 'timestamp' not in self:
            self['timestamp'] = toUTC(datetime.now()).isoformat()
        if 'mozdefhostname' not in self:
            self['mozdefhostname'] = socket.gethostname()
        if 'tags' not in self:
            self['tags'] = []
        if 'category' not in self:
            self['category'] = self.DEFAULT_STRING
        if 'hostname' not in self:
            self['hostname'] = self.DEFAULT_STRING
        if 'processid' not in self:
            self['processid'] = self.DEFAULT_STRING
        if 'processname' not in self:
            self['processname'] = self.DEFAULT_STRING
        if 'severity' not in self:
            self['severity'] = self.DEFAULT_STRING
        if 'source' not in self:
            self['source'] = self.DEFAULT_STRING
        if 'summary' not in self:
            self['summary'] = self.DEFAULT_STRING
        if 'plugins' not in self:
            self['plugins'] = []
        if 'details' not in self:
            self['details'] = {}
<commit_msg>Set default type for events to event<commit_after>from datetime import datetime
import socket

from utilities.toUTC import toUTC

class Event(dict):

    # We set default vaules so that we can later
    # create an alert around these, and know when events
    # have to use defaults
    DEFAULT_STRING = 'UNKNOWN'
    DEFAULT_TYPE = 'event'

    def add_required_fields(self):
        if 'receivedtimestamp' not in self:
            self['receivedtimestamp'] = toUTC(datetime.now()).isoformat()
        if 'utctimestamp' not in self:
            self['utctimestamp'] = toUTC(datetime.now()).isoformat()
        if 'timestamp' not in self:
            self['timestamp'] = toUTC(datetime.now()).isoformat()
        if 'mozdefhostname' not in self:
            self['mozdefhostname'] = socket.gethostname()
        if 'type' not in self:
            self['type'] = self.DEFAULT_TYPE
        if 'tags' not in self:
            self['tags'] = []
        if 'category' not in self:
            self['category'] = self.DEFAULT_STRING
        if 'hostname' not in self:
            self['hostname'] = self.DEFAULT_STRING
        if 'processid' not in self:
            self['processid'] = self.DEFAULT_STRING
        if 'processname' not in self:
            self['processname'] = self.DEFAULT_STRING
        if 'severity' not in self:
            self['severity'] = self.DEFAULT_STRING
        if 'source' not in self:
            self['source'] = self.DEFAULT_STRING
        if 'summary' not in self:
            self['summary'] = self.DEFAULT_STRING
        if 'plugins' not in self:
            self['plugins'] = []
        if 'details' not in self:
            self['details'] = {}
<|endoftext|>


<commit_before>"""
Top-level URL lookup for InvenTree application.

Passes URL lookup downstream to each app as required.
"""

from django.conf.urls import url, include
from django.urls import path
from django.contrib import admin

from company.urls import company_urls
from company.urls import manufacturer_part_urls
from company.urls import supplier_part_urls

from common.urls import common_urls
from part.urls import part_urls
from stock.urls import stock_urls
from build.urls import build_urls
from order.urls import order_urls
from plugin.urls import plugin_urls, PLUGIN_BASE

from barcodes.api import barcode_api_urls
from common.api import common_api_urls
from part.api import part_api_urls, bom_api_urls
from company.api import company_api_urls
from stock.api import stock_api_urls
from build.api import build_api_urls
from order.api import order_api_urls
from label.api import label_api_urls
from report.api import report_api_urls
from plugin.api import plugin_api_urls

from django.conf import settings
from django.conf.urls.static import static

from django.views.generic.base import RedirectView
from rest_framework.documentation import include_docs_urls

from .views import auth_request
from .views import IndexView, SearchView, DatabaseStatsView
from .views import SettingsView, EditUserView, SetPasswordView, CustomEmailView, CustomConnectionsView, CustomPasswordResetFromKeyView
from .views import CurrencyRefreshView
from .views import AppearanceSelectView, SettingCategorySelectView
from .views import DynamicJsView

from .api import InfoView, NotFoundView
from .api import ActionPluginView

from users.api import user_urls

admin.site.site_header = "InvenTree Admin"

apipatterns = [
    url(r'^barcode/', include(barcode_api_urls)),
    url(r'^settings/', include(common_api_urls)),
    url(r'^part/', include(part_api_urls)),
    url(r'^bom/', include(bom_api_urls)),
    url(r'^company/', include(company_api_urls)),
    url(r'^stock/', include(stock_api_urls)),
    url(r'^build/', include(build_api_urls)),
    url(r'^order/', include(order_api_urls)),
    url(r'^label/', include(label_api_urls)),
    url(r'^report/', include(report_api_urls)),
    url(r'^plugin/', include(plugin_api_urls)),

    # User URLs
    url(r'^user/', include(user_urls)),

    # Plugin endpoints
    url(r'^action/', ActionPluginView.as_view(), name='api-action-plugin'),

    # Webhook enpoint
    path('', include(common_api_urls)),

    # InvenTree information endpoint
    url(r'^$', InfoView.as_view(), name='api-inventree-info'),

    # Unknown endpoint
    url(r'^.*$', NotFoundView.as_view(), name='api-404'),
]

settings_urls = [

    url(r'^i18n/?', include('django.conf.urls.i18n')),

    url(r'^appearance/?', AppearanceSelectView.as_view(), name='settings-appearance'),
    url(r'^currencies-refresh/', CurrencyRefreshView.as_view(), name='settings-currencies-refresh'),

    url(r'^category/', SettingCategorySelectView.as_view(), name='settings-category'),

    # Catch any other urls
    url(r'^.*$', SettingsView.as_view(template_name='InvenTree/settings/settings.html'), name='settings'),
]

# These javascript files are served "dynamically" - i.e. rendered on demand
dynamic_javascript_urls = [
    url(r'^calendar.js', DynamicJsView.as_view(template_name='js/dynamic/calendar.js'), name='calendar.js'),
    url(r'^nav.js', DynamicJsView.as_view(template_name='js/dynamic/nav.js'), name='nav.js'),
    url(r'^settings.js', DynamicJsView.as_view(template_name='js/dynamic/settings.js'), name='settings.js'),
]

# These javascript files are pased through the Django translation layer
translated_javascript_urls = [
    url(r'^api.js', DynamicJsView.as_view(template_name='js/translated/api.js'), name='api.js'),
    url(r'^attachment.js', DynamicJsView.as_view(template_name='js/translated/attachment.js'), name='attachment.js'),
    url(r'^barcode.js', DynamicJsView.as_view(template_name='js/translated/barcode.js'), name='barcode.js'),
    url(r'^bom.js', DynamicJsView.as_view(template_name='js/translated/bom.js'), name='bom.js'),
    url(r'^build.js', DynamicJsView.as_view(template_name='js/translated/build.js'), name='build.js'),
    url(r'^company.js', DynamicJsView.as_view(template_name='js/translated/company.js'), name='company.js'),
    url(r'^filters.js', DynamicJsView.as_view(template_name='js/translated/filters.js'), name='filters.js'),
    url(r'^forms.js', DynamicJsView.as_view(template_name='js/translated/forms.js'), name='forms.js'),
    url(r'^helpers.js', DynamicJsView.as_view(template_name='js/translated/helpers.js'), name='helpers.js'),
    url(r'^label.js', DynamicJsView.as_view(template_name='js/translated/label.js'), name='label.js'),
    url(r'^model_renderers.js', DynamicJsView.as_view(template_name='js/translated/model_renderers.js'), name='model_renderers.js'),
    url(r'^modals.js', DynamicJsView.as_view(template_name='js/translated/modals.js'), name='modals.js'),
    url(r'^order.js', DynamicJsView.as_view(template_name='js/translated/order.js'), name='order.js'),
    url(r'^part.js', DynamicJsView.as_view(template_name='js/translated/part.js'), name='part.js'),
    url(r'^report.js', DynamicJsView.as_view(template_name='js/translated/report.js'), name='report.js'),
    url(r'^stock.js', DynamicJsView.as_view(template_name='js/translated/stock.js'), name='stock.js'),
    url(r'^plugin.js', DynamicJsView.as_view(template_name='js/translated/plugin.js'), name='plugin.js'),
    url(r'^tables.js', DynamicJsView.as_view(template_name='js/translated/tables.js'), name='tables.js'),
    url(r'^table_filters.js', DynamicJsView.as_view(template_name='js/translated/table_filters.js'), name='table_filters.js'),
]

urlpatterns = [
    url(r'^part/', include(part_urls)),
    url(r'^manufacturer-part/', include(manufacturer_part_urls)),
    url(r'^supplier-part/', include(supplier_part_urls)),

    # "Dynamic" javascript files which are rendered using InvenTree templating.
    url(r'^js/dynamic/', include(dynamic_javascript_urls)),
    url(r'^js/i18n/', include(translated_javascript_urls)),

    url(r'^common/', include(common_urls)),

    url(r'^stock/', include(stock_urls)),

    url(r'^company/', include(company_urls)),
    url(r'^order/', include(order_urls)),

    url(r'^build/', include(build_urls)),

    url(r'^auth/', include('rest_framework.urls', namespace='rest_framework')),

    url(r'^settings/', include(settings_urls)),

    url(r'^edit-user/', EditUserView.as_view(), name='edit-user'),
    url(r'^set-password/', SetPasswordView.as_view(), name='set-password'),

    url(r'^admin/error_log/', include('error_report.urls')),
    url(r'^admin/shell/', include('django_admin_shell.urls')),
    url(r'^admin/', admin.site.urls, name='inventree-admin'),

    url(r'^index/', IndexView.as_view(), name='index'),
    url(r'^search/', SearchView.as_view(), name='search'),
    url(r'^stats/', DatabaseStatsView.as_view(), name='stats'),

    url(r'^auth/?', auth_request),

    url(r'^api/', include(apipatterns)),
    url(r'^api-doc/', include_docs_urls(title='InvenTree API')),

    # plugin urls
    url(r'^plugins/', include(plugin_urls)),
    url(f'^{PLUGIN_BASE}/', include(([], 'plugin'))),  # on startup we do not have any plugins enabled

    url(r'^markdownx/', include('markdownx.urls')),

    # Single Sign On / allauth
    # overrides of urlpatterns
    url(r'^accounts/email/', CustomEmailView.as_view(), name='account_email'),
    url(r'^accounts/social/connections/', CustomConnectionsView.as_view(), name='socialaccount_connections'),
    url(r"^accounts/password/reset/key/(?P<uidb36>[0-9A-Za-z]+)-(?P<key>.+)/$", CustomPasswordResetFromKeyView.as_view(), name="account_reset_password_from_key"),
    url(r'^accounts/', include('allauth.urls')),  # included urlpatterns
]

# Server running in "DEBUG" mode?
if settings.DEBUG:
    # Static file access
    urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

    # Media file access
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

    # Debug toolbar access (only allowed in DEBUG mode)
    if 'debug_toolbar' in settings.INSTALLED_APPS:
        import debug_toolbar
        urlpatterns = [
            path('__debug/', include(debug_toolbar.urls)),
        ] + urlpatterns

# Send any unknown URLs to the parts page
urlpatterns += [url(r'^.*$', RedirectView.as_view(url='/index/', permanent=False), name='index')]
<commit_msg>remove url<commit_after>"""
Top-level URL lookup for InvenTree application.

Passes URL lookup downstream to each app as required.
"""

from django.conf.urls import url, include
from django.urls import path
from django.contrib import admin

from company.urls import company_urls
from company.urls import manufacturer_part_urls
from company.urls import supplier_part_urls

from common.urls import common_urls
from part.urls import part_urls
from stock.urls import stock_urls
from build.urls import build_urls
from order.urls import order_urls
from plugin.urls import plugin_urls, PLUGIN_BASE

from barcodes.api import barcode_api_urls
from common.api import common_api_urls
from part.api import part_api_urls, bom_api_urls
from company.api import company_api_urls
from stock.api import stock_api_urls
from build.api import build_api_urls
from order.api import order_api_urls
from label.api import label_api_urls
from report.api import report_api_urls
from plugin.api import plugin_api_urls

from django.conf import settings
from django.conf.urls.static import static

from django.views.generic.base import RedirectView
from rest_framework.documentation import include_docs_urls

from .views import auth_request
from .views import IndexView, SearchView, DatabaseStatsView
from .views import SettingsView, EditUserView, SetPasswordView, CustomEmailView, CustomConnectionsView, CustomPasswordResetFromKeyView
from .views import CurrencyRefreshView
from .views import AppearanceSelectView, SettingCategorySelectView
from .views import DynamicJsView

from .api import InfoView, NotFoundView
from .api import ActionPluginView

from users.api import user_urls

admin.site.site_header = "InvenTree Admin"

apipatterns = [
    url(r'^barcode/', include(barcode_api_urls)),
    url(r'^settings/', include(common_api_urls)),
    url(r'^part/', include(part_api_urls)),
    url(r'^bom/', include(bom_api_urls)),
    url(r'^company/', include(company_api_urls)),
    url(r'^stock/', include(stock_api_urls)),
    url(r'^build/', include(build_api_urls)),
    url(r'^order/', include(order_api_urls)),
    url(r'^label/', include(label_api_urls)),
    url(r'^report/', include(report_api_urls)),
    url(r'^plugin/', include(plugin_api_urls)),

    # User URLs
    url(r'^user/', include(user_urls)),

    # Plugin endpoints
    url(r'^action/', ActionPluginView.as_view(), name='api-action-plugin'),

    # InvenTree information endpoint
    url(r'^$', InfoView.as_view(), name='api-inventree-info'),

    # Unknown endpoint
    url(r'^.*$', NotFoundView.as_view(), name='api-404'),
]

settings_urls = [

    url(r'^i18n/?', include('django.conf.urls.i18n')),

    url(r'^appearance/?', AppearanceSelectView.as_view(), name='settings-appearance'),
    url(r'^currencies-refresh/', CurrencyRefreshView.as_view(), name='settings-currencies-refresh'),

    url(r'^category/', SettingCategorySelectView.as_view(), name='settings-category'),

    # Catch any other urls
    url(r'^.*$', SettingsView.as_view(template_name='InvenTree/settings/settings.html'), name='settings'),
]

# These javascript files are served "dynamically" - i.e. rendered on demand
dynamic_javascript_urls = [
    url(r'^calendar.js', DynamicJsView.as_view(template_name='js/dynamic/calendar.js'), name='calendar.js'),
    url(r'^nav.js', DynamicJsView.as_view(template_name='js/dynamic/nav.js'), name='nav.js'),
    url(r'^settings.js', DynamicJsView.as_view(template_name='js/dynamic/settings.js'), name='settings.js'),
]

# These javascript files are pased through the Django translation layer
translated_javascript_urls = [
    url(r'^api.js', DynamicJsView.as_view(template_name='js/translated/api.js'), name='api.js'),
    url(r'^attachment.js', DynamicJsView.as_view(template_name='js/translated/attachment.js'), name='attachment.js'),
    url(r'^barcode.js', DynamicJsView.as_view(template_name='js/translated/barcode.js'), name='barcode.js'),
    url(r'^bom.js', DynamicJsView.as_view(template_name='js/translated/bom.js'), name='bom.js'),
    url(r'^build.js', DynamicJsView.as_view(template_name='js/translated/build.js'), name='build.js'),
    url(r'^company.js', DynamicJsView.as_view(template_name='js/translated/company.js'), name='company.js'),
    url(r'^filters.js', DynamicJsView.as_view(template_name='js/translated/filters.js'), name='filters.js'),
    url(r'^forms.js', DynamicJsView.as_view(template_name='js/translated/forms.js'), name='forms.js'),
    url(r'^helpers.js', DynamicJsView.as_view(template_name='js/translated/helpers.js'), name='helpers.js'),
    url(r'^label.js', DynamicJsView.as_view(template_name='js/translated/label.js'), name='label.js'),
    url(r'^model_renderers.js', DynamicJsView.as_view(template_name='js/translated/model_renderers.js'), name='model_renderers.js'),
    url(r'^modals.js', DynamicJsView.as_view(template_name='js/translated/modals.js'), name='modals.js'),
    url(r'^order.js', DynamicJsView.as_view(template_name='js/translated/order.js'), name='order.js'),
    url(r'^part.js', DynamicJsView.as_view(template_name='js/translated/part.js'), name='part.js'),
    url(r'^report.js', DynamicJsView.as_view(template_name='js/translated/report.js'), name='report.js'),
    url(r'^stock.js', DynamicJsView.as_view(template_name='js/translated/stock.js'), name='stock.js'),
    url(r'^plugin.js', DynamicJsView.as_view(template_name='js/translated/plugin.js'), name='plugin.js'),
    url(r'^tables.js', DynamicJsView.as_view(template_name='js/translated/tables.js'), name='tables.js'),
    url(r'^table_filters.js', DynamicJsView.as_view(template_name='js/translated/table_filters.js'), name='table_filters.js'),
]

urlpatterns = [
    url(r'^part/', include(part_urls)),
    url(r'^manufacturer-part/', include(manufacturer_part_urls)),
    url(r'^supplier-part/', include(supplier_part_urls)),

    # "Dynamic" javascript files which are rendered using InvenTree templating.
    url(r'^js/dynamic/', include(dynamic_javascript_urls)),
    url(r'^js/i18n/', include(translated_javascript_urls)),

    url(r'^common/', include(common_urls)),

    url(r'^stock/', include(stock_urls)),

    url(r'^company/', include(company_urls)),
    url(r'^order/', include(order_urls)),

    url(r'^build/', include(build_urls)),

    url(r'^auth/', include('rest_framework.urls', namespace='rest_framework')),

    url(r'^settings/', include(settings_urls)),

    url(r'^edit-user/', EditUserView.as_view(), name='edit-user'),
    url(r'^set-password/', SetPasswordView.as_view(), name='set-password'),

    url(r'^admin/error_log/', include('error_report.urls')),
    url(r'^admin/shell/', include('django_admin_shell.urls')),
    url(r'^admin/', admin.site.urls, name='inventree-admin'),

    url(r'^index/', IndexView.as_view(), name='index'),
    url(r'^search/', SearchView.as_view(), name='search'),
    url(r'^stats/', DatabaseStatsView.as_view(), name='stats'),

    url(r'^auth/?', auth_request),

    url(r'^api/', include(apipatterns)),
    url(r'^api-doc/', include_docs_urls(title='InvenTree API')),

    # plugin urls
    url(r'^plugins/', include(plugin_urls)),
    url(f'^{PLUGIN_BASE}/', include(([], 'plugin'))),  # on startup we do not have any plugins enabled

    url(r'^markdownx/', include('markdownx.urls')),

    # Single Sign On / allauth
    # overrides of urlpatterns
    url(r'^accounts/email/', CustomEmailView.as_view(), name='account_email'),
    url(r'^accounts/social/connections/', CustomConnectionsView.as_view(), name='socialaccount_connections'),
    url(r"^accounts/password/reset/key/(?P<uidb36>[0-9A-Za-z]+)-(?P<key>.+)/$", CustomPasswordResetFromKeyView.as_view(), name="account_reset_password_from_key"),
    url(r'^accounts/', include('allauth.urls')),  # included urlpatterns
]

# Server running in "DEBUG" mode?
if settings.DEBUG:
    # Static file access
    urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

    # Media file access
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

    # Debug toolbar access (only allowed in DEBUG mode)
    if 'debug_toolbar' in settings.INSTALLED_APPS:
        import debug_toolbar
        urlpatterns = [
            path('__debug/', include(debug_toolbar.urls)),
        ] + urlpatterns

# Send any unknown URLs to the parts page
urlpatterns += [url(r'^.*$', RedirectView.as_view(url='/index/', permanent=False), name='index')]
<|endoftext|>


<commit_before>"""
FOIA views for composing
"""

# Django
from django.conf import settings
from django.contrib import messages
from django.contrib.auth import login
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.urlresolvers import reverse
from django.http import Http404, HttpResponse, HttpResponseBadRequest
from django.shortcuts import get_object_or_404, redirect
from django.utils.encoding import smart_text
from django.views.decorators.http import require_POST
from django.views.generic import CreateView, UpdateView

# Standard Library
import re

# Third Party
import requests

# MuckRock
from muckrock.accounts.mixins import BuyRequestsMixin, MiniregMixin
from muckrock.accounts.utils import mixpanel_event
from muckrock.agency.models import Agency
from muckrock.foia.exceptions import InsufficientRequestsError
from muckrock.foia.forms import BaseComposerForm, ComposerForm, ContactInfoForm
from muckrock.foia.models import FOIAComposer, FOIARequest

class GenericComposer(BuyRequestsMixin):
    """Shared functionality between create and update composer views"""
    template_name = 'forms/foia/create.html'
    form_class = ComposerForm
    context_object_name = 'composer'

    def _get_organizations(self, user):
        """Get the active organization and which organization should pay
        This is the active org if the user is an admin of that org,
        else it is the user's individual org
        """
        if not user.is_authenticated:
            return None, None
        active = user.profile.organization
        if active.has_admin(user):
            payer = active
        else:
            payer = user.profile.individual_organization
        return (active, payer)

    def get_form_kwargs(self):
        """Add request to the form kwargs"""
        kwargs = super(GenericComposer, self).get_form_kwargs()
        kwargs['user'] = self.request.user
        kwargs['request'] = self.request
        _, payer = self._get_organizations(self.request.user)
        kwargs['organization'] = payer

        if len(self.request.POST.getlist('agencies')) == 1:
            # pass the agency for contact info form
            try:
                kwargs['agency'] = Agency.objects.get(
                    pk=self.request.POST.get('agencies')
                )
            except (Agency.DoesNotExist, ValueError):
                # ValueError for new agency format
                pass
        return kwargs

    def get_context_data(self, **kwargs):
        """Extra context"""
        context = super(GenericComposer, self).get_context_data(**kwargs)
        if self.request.user.is_authenticated:
            foias_filed = (
                self.request.user.composers.exclude(status='started').count()
            )
            organization, payer = self._get_organizations(self.request.user)
            context['organization'] = organization
            context['payer'] = payer
            requests_left = {
                'regular': organization.number_requests,
                'monthly': organization.monthly_requests,
            }
            context['sidebar_admin_url'] = reverse(
                'admin:foia_foiacomposer_change',
                args=(self.object.pk,),
            )
        else:
            foias_filed = 0
            requests_left = {}
        context.update({
            'settings': settings,
            'foias_filed': foias_filed,
            'requests_left': requests_left,
            'stripe_pk': settings.STRIPE_PUB_KEY,
        })
        return context

    def _submit_composer(self, composer, form):
        """Submit a composer"""
        # pylint: disable=not-an-iterable
        if composer.attachments_over_size_limit(self.request.user):
            messages.error(
                self.request, 'Total attachment size must be less than 20MB'
            )
            return
        if form.cleaned_data.get('num_requests', 0) > 0:
            active, payer = self._get_organizations(self.request.user)
            self.buy_requests(form, active, payer)
        if (
            form.cleaned_data.get('use_contact_information')
            and self.request.user.has_perm('foia.set_info_foiarequest')
            and len(composer.agencies.all()) == 1
        ):
            contact_info = {
                k: form.cleaned_data.get(k)
                for k in ContactInfoForm.base_fields
            }
        else:
            contact_info = None
        try:
            composer.submit(contact_info)
        except InsufficientRequestsError:
            messages.warning(self.request, 'You need to purchase more requests')
        else:
            messages.success(self.request, 'Request submitted')
            self.request.session['ga'] = 'request_submitted'
            mixpanel_event(
                self.request,
                'Request Submitted',
                self._composer_mixpanel_properties(composer),
            )
            warning = self._proxy_warnings(composer)
            if warning:
                messages.warning(self.request, warning)

    def _proxy_warnings(self, composer):
        """Check composer's agencies for proxy status"""
        proxies = {'missing': 0, 'non-missing': 0}
        for agency in composer.agencies.all():
            proxy_info = agency.get_proxy_info()
            if proxy_info['proxy'] and proxy_info['missing_proxy']:
                proxies['missing'] += 1
            elif proxy_info['proxy'] and not proxy_info['missing_proxy']:
                proxies['non-missing'] += 1
        if proxies['missing'] and proxies['non-missing']:
            return (
                'Some of the agencies you are requesting from require '
                'requestors to be in-state citizens.  We will file these '
                'with volunteer filers in states in which we have a '
                'volunteer available.  If we do not have a volunteer '
                'available, your request will be filed once we find a '
                'suitable volunteer.'
            )
        elif proxies['missing']:
            return (
                'Some of the agencies you are requesting from require '
                'requestors to be in-state citizens.  We do not currently '
                'have a citizen proxy requestor on file for these '
                'agencies, but will attempt to find one to submit these '
                'requests on your behalf.'
            )
        elif proxies['non-missing']:
            return (
                'Some of the agencies you are requesting from require '
                'requestors to be in-state citizens.  These requests will '
                'be filed in the name of one of our volunteer files for '
                'these states.'
            )
        else:
            return ''

    def _composer_mixpanel_properties(self, composer):
        """Get properties for tracking composer events in mixpanel"""
        return {
            'Number': len(composer.agencies.all()),
            'Title': composer.title,
            'Agencies': [a.name for a in composer.agencies.all()],
            'Embargo': composer.embargo,
            'Permanent Embargo': composer.permanent_embargo,
            'Created At': composer.datetime_created.isoformat(),
            'Parent': composer.parent.pk if composer.parent else None,
            'ID': composer.pk,
        }

class CreateComposer(MiniregMixin, GenericComposer, CreateView):
    """Create a new composer"""
    minireg_source = 'Composer'
    field_map = {
        'email': 'register_email',
        'name': 'register_full_name',
    }

    # pylint: disable=attribute-defined-outside-init

    def get_initial(self):
        """Get initial data from clone, if there is one"""
        self.clone = None
        # set title to blank, as if we create a new empty draft, it will
        # set the title to 'Untitled'
        data = {'title': ''}
        clone_pk = self.request.GET.get('clone')
        if clone_pk is not None:
            data.update(self._get_clone_data(clone_pk))
        agency_pks = self.request.GET.getlist('agency')
        agency_pks = [pk for pk in agency_pks if re.match('^[0-9]+$', pk)]
        if agency_pks:
            agencies = Agency.objects.filter(
                pk__in=agency_pks,
                status='approved',
            )
            data.update({'agencies': agencies})
        return data

    def _get_clone_data(self, clone_pk):
        """Get the intial data for a clone"""
        try:
            composer = get_object_or_404(FOIAComposer, pk=clone_pk)
        except ValueError:
            # non integer passed in as clone_pk
            return {}
        if not composer.has_perm(self.request.user, 'view'):
            raise Http404()
        initial_data = {
            'title': composer.title,
            'requested_docs': smart_text(composer.requested_docs),
            'agencies': composer.agencies.all(),
            'tags': composer.tags.all(),
            'edited_boilerplate': composer.edited_boilerplate,
            'parent': composer,
        }
        self.clone = composer
        return initial_data

    def get_context_data(self, **kwargs):
        """Extra context"""
        # if user is authenticated, save an empty draft to the database
        # so that autosaving and file uploading will work
        if self.request.user.is_authenticated:
            self.object = (
                FOIAComposer.objects.get_or_create_draft(
                    user=self.request.user,
                    organization=self.request.user.profile.organization,
                )
            )
        context = super(CreateComposer, self).get_context_data(**kwargs)
        context.update({
            'clone': self.clone,
            'featured': FOIARequest.objects.get_featured(self.request.user),
        })
        return context

    def _handle_anonymous_submitter(self, form):
        """Handle a submission from an anonymous user"""
        # form validation guarentees we have either registration or login info
        if form.cleaned_data.get('register_full_name'):
            user = self.miniregister(
                form,
                form.cleaned_data['register_full_name'],
                form.cleaned_data['register_email'],
                form.cleaned_data.get('register_newsletter'),
            )
            return user
        else:
            login(self.request, form.user)
            return form.user

    def form_valid(self, form):
        """Create the request"""
        if self.request.user.is_authenticated:
            user = self.request.user
        else:
            try:
                user = self._handle_anonymous_submitter(form)
            except requests.exceptions.RequestException:
                return self.form_invalid(form)
        if form.cleaned_data['action'] in ('save', 'submit'):
            composer = form.save(commit=False)
            composer.user = user
            composer.organization = user.profile.organization
            composer.save()
            form.save_m2m()
            # if a new agency is added while the user is anonymous,
            # we want to associate that agency to the user once they
            # login or register
            composer.agencies.filter(
                user=None, status='pending'
            ).update(user=user)
        if form.cleaned_data['action'] == 'save':
            self.request.session['ga'] = 'request_drafted'
            mixpanel_event(
                self.request,
                'Request Saved',
                self._composer_mixpanel_properties(composer),
            )
            messages.success(self.request, 'Request saved')
        elif form.cleaned_data['action'] == 'submit':
            self._submit_composer(composer, form)
        return redirect(composer)

class UpdateComposer(LoginRequiredMixin, GenericComposer, UpdateView):
    """Update a composer"""
    # pylint: disable=attribute-defined-outside-init
    pk_url_kwarg = 'idx'

    def get_queryset(self):
        """Restrict to composers you can view"""
        kwargs = {}
        # non staff may only see their own requests
        if not self.request.user.is_staff:
            kwargs['user'] = self.request.user
        # can only post to draft composers
        if self.request.method == 'POST':
            kwargs['status'] = 'started'
        return FOIAComposer.objects.filter(**kwargs)

    def get_object(self, queryset=None):
        """Convert object back to draft if it has been submitted recently"""
        composer = super(UpdateComposer, self).get_object(queryset)
        if composer.revokable():
            composer.revoke()
            messages.warning(
                self.request,
                'This request\'s submission has been cancelled.  You may now '
                'edit it and submit it again when ready',
            )
        return composer

    def post(self, request, *args, **kwargs):
        """Allow deletion regardless of form validation"""
        self.object = self.get_object()
        if request.POST.get('action') == 'delete':
            if self.object.has_perm(request.user, 'delete'):
                self.object.delete()
                messages.success(self.request, 'Draft deleted')
                return redirect('foia-mylist-drafts')
            else:
                messages.success(
                    self.request,
                    'You do not have permission to delete that draft',
                )
                return redirect('foia-mylist-drafts')
        return super(UpdateComposer, self).post(request, *args, **kwargs)

    def get(self, request, *args, **kwargs):
        """Redirect if this composer is not a draft"""
        self.object = self.get_object()
        if self.object.status != 'started':
            messages.warning(
                self.request, 'This request can no longer be updated.'
            )
            return redirect(self.object)
        else:
            return self.render_to_response(self.get_context_data())

    def form_valid(self, form):
        """Update the request"""
        if form.cleaned_data['action'] == 'save':
            composer = form.save()
            self.request.session['ga'] = 'request_drafted'
            mixpanel_event(
                self.request,
                'Request Saved',
                self._composer_mixpanel_properties(composer),
            )
            messages.success(self.request, 'Request saved')
        elif form.cleaned_data['action'] == 'submit':
            composer = form.save()
            self._submit_composer(composer, form)
        return redirect(composer)

@login_required
@require_POST
def autosave(request, idx):
    """Save the composer via AJAX"""
    composer = get_object_or_404(
        FOIAComposer,
        pk=idx,
        status='started',
        user=request.user,
    )
    old_agencies = set(composer.agencies.all())
    data = request.POST.copy()
    # we are always just saving
    data['action'] = 'save'
    form = BaseComposerForm(
        data,
        instance=composer,
        user=request.user,
        request=request,
    )
    if form.is_valid():
        composer = form.save()
        new_agencies = set(composer.agencies.all())
        removed_agencies = old_agencies - new_agencies
        # delete pending agencies which have been removed from composers and requests
        for agency in removed_agencies:
            if (
                agency.status == 'pending' and agency.composers.count() == 0
                and agency.foiarequest_set.count() == 0
            ):
                agency.delete()
        return HttpResponse('OK')
    else:
        return HttpResponseBadRequest(form.errors.as_json())
<commit_msg>fix logging in & saving request at same time<commit_after>"""
FOIA views for composing
"""

# Django
from django.conf import settings
from django.contrib import messages
from django.contrib.auth import login
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.urlresolvers import reverse
from django.http import Http404, HttpResponse, HttpResponseBadRequest
from django.shortcuts import get_object_or_404, redirect
from django.utils.encoding import smart_text
from django.views.decorators.http import require_POST
from django.views.generic import CreateView, UpdateView

# Standard Library
import re

# Third Party
import requests

# MuckRock
from muckrock.accounts.mixins import BuyRequestsMixin, MiniregMixin
from muckrock.accounts.utils import mixpanel_event
from muckrock.agency.models import Agency
from muckrock.foia.exceptions import InsufficientRequestsError
from muckrock.foia.forms import BaseComposerForm, ComposerForm, ContactInfoForm
from muckrock.foia.models import FOIAComposer, FOIARequest

class GenericComposer(BuyRequestsMixin):
    """Shared functionality between create and update composer views"""
    template_name = 'forms/foia/create.html'
    form_class = ComposerForm
    context_object_name = 'composer'

    def _get_organizations(self, user):
        """Get the active organization and which organization should pay
        This is the active org if the user is an admin of that org,
        else it is the user's individual org
        """
        if not user.is_authenticated:
            return None, None
        active = user.profile.organization
        if active.has_admin(user):
            payer = active
        else:
            payer = user.profile.individual_organization
        return (active, payer)

    def get_form_kwargs(self):
        """Add request to the form kwargs"""
        kwargs = super(GenericComposer, self).get_form_kwargs()
        kwargs['user'] = self.request.user
        kwargs['request'] = self.request
        _, payer = self._get_organizations(self.request.user)
        kwargs['organization'] = payer

        if len(self.request.POST.getlist('agencies')) == 1:
            # pass the agency for contact info form
            try:
                kwargs['agency'] = Agency.objects.get(
                    pk=self.request.POST.get('agencies')
                )
            except (Agency.DoesNotExist, ValueError):
                # ValueError for new agency format
                pass
        return kwargs

    def get_context_data(self, **kwargs):
        """Extra context"""
        context = super(GenericComposer, self).get_context_data(**kwargs)
        if self.request.user.is_authenticated:
            foias_filed = (
                self.request.user.composers.exclude(status='started').count()
            )
            organization, payer = self._get_organizations(self.request.user)
            context['organization'] = organization
            context['payer'] = payer
            requests_left = {
                'regular': organization.number_requests,
                'monthly': organization.monthly_requests,
            }
            context['sidebar_admin_url'] = reverse(
                'admin:foia_foiacomposer_change',
                args=(self.object.pk,),
            )
        else:
            foias_filed = 0
            requests_left = {}
        context.update({
            'settings': settings,
            'foias_filed': foias_filed,
            'requests_left': requests_left,
            'stripe_pk': settings.STRIPE_PUB_KEY,
        })
        return context

    def _submit_composer(self, composer, form):
        """Submit a composer"""
        # pylint: disable=not-an-iterable
        if composer.attachments_over_size_limit(self.request.user):
            messages.error(
                self.request, 'Total attachment size must be less than 20MB'
            )
            return
        if form.cleaned_data.get('num_requests', 0) > 0:
            active, payer = self._get_organizations(self.request.user)
            self.buy_requests(form, active, payer)
        if (
            form.cleaned_data.get('use_contact_information')
            and self.request.user.has_perm('foia.set_info_foiarequest')
            and len(composer.agencies.all()) == 1
        ):
            contact_info = {
                k: form.cleaned_data.get(k)
                for k in ContactInfoForm.base_fields
            }
        else:
            contact_info = None
        try:
            composer.submit(contact_info)
        except InsufficientRequestsError:
            messages.warning(self.request, 'You need to purchase more requests')
        else:
            messages.success(self.request, 'Request submitted')
            self.request.session['ga'] = 'request_submitted'
            mixpanel_event(
                self.request,
                'Request Submitted',
                self._composer_mixpanel_properties(composer),
            )
            warning = self._proxy_warnings(composer)
            if warning:
                messages.warning(self.request, warning)

    def _proxy_warnings(self, composer):
        """Check composer's agencies for proxy status"""
        proxies = {'missing': 0, 'non-missing': 0}
        for agency in composer.agencies.all():
            proxy_info = agency.get_proxy_info()
            if proxy_info['proxy'] and proxy_info['missing_proxy']:
                proxies['missing'] += 1
            elif proxy_info['proxy'] and not proxy_info['missing_proxy']:
                proxies['non-missing'] += 1
        if proxies['missing'] and proxies['non-missing']:
            return (
                'Some of the agencies you are requesting from require '
                'requestors to be in-state citizens.  We will file these '
                'with volunteer filers in states in which we have a '
                'volunteer available.  If we do not have a volunteer '
                'available, your request will be filed once we find a '
                'suitable volunteer.'
            )
        elif proxies['missing']:
            return (
                'Some of the agencies you are requesting from require '
                'requestors to be in-state citizens.  We do not currently '
                'have a citizen proxy requestor on file for these '
                'agencies, but will attempt to find one to submit these '
                'requests on your behalf.'
            )
        elif proxies['non-missing']:
            return (
                'Some of the agencies you are requesting from require '
                'requestors to be in-state citizens.  These requests will '
                'be filed in the name of one of our volunteer files for '
                'these states.'
            )
        else:
            return ''

    def _composer_mixpanel_properties(self, composer):
        """Get properties for tracking composer events in mixpanel"""
        return {
            'Number': len(composer.agencies.all()),
            'Title': composer.title,
            'Agencies': [a.name for a in composer.agencies.all()],
            'Embargo': composer.embargo,
            'Permanent Embargo': composer.permanent_embargo,
            'Created At': composer.datetime_created.isoformat(),
            'Parent': composer.parent.pk if composer.parent else None,
            'ID': composer.pk,
        }

class CreateComposer(MiniregMixin, GenericComposer, CreateView):
    """Create a new composer"""
    minireg_source = 'Composer'
    field_map = {
        'email': 'register_email',
        'name': 'register_full_name',
    }

    # pylint: disable=attribute-defined-outside-init

    def get_initial(self):
        """Get initial data from clone, if there is one"""
        self.clone = None
        # set title to blank, as if we create a new empty draft, it will
        # set the title to 'Untitled'
        data = {'title': ''}
        clone_pk = self.request.GET.get('clone')
        if clone_pk is not None:
            data.update(self._get_clone_data(clone_pk))
        agency_pks = self.request.GET.getlist('agency')
        agency_pks = [pk for pk in agency_pks if re.match('^[0-9]+$', pk)]
        if agency_pks:
            agencies = Agency.objects.filter(
                pk__in=agency_pks,
                status='approved',
            )
            data.update({'agencies': agencies})
        return data

    def _get_clone_data(self, clone_pk):
        """Get the intial data for a clone"""
        try:
            composer = get_object_or_404(FOIAComposer, pk=clone_pk)
        except ValueError:
            # non integer passed in as clone_pk
            return {}
        if not composer.has_perm(self.request.user, 'view'):
            raise Http404()
        initial_data = {
            'title': composer.title,
            'requested_docs': smart_text(composer.requested_docs),
            'agencies': composer.agencies.all(),
            'tags': composer.tags.all(),
            'edited_boilerplate': composer.edited_boilerplate,
            'parent': composer,
        }
        self.clone = composer
        return initial_data

    def get_context_data(self, **kwargs):
        """Extra context"""
        # if user is authenticated, save an empty draft to the database
        # so that autosaving and file uploading will work
        if self.request.user.is_authenticated:
            self.object = (
                FOIAComposer.objects.get_or_create_draft(
                    user=self.request.user,
                    organization=self.request.user.profile.organization,
                )
            )
        context = super(CreateComposer, self).get_context_data(**kwargs)
        context.update({
            'clone': self.clone,
            'featured': FOIARequest.objects.get_featured(self.request.user),
        })
        return context

    def _handle_anonymous_submitter(self, form):
        """Handle a submission from an anonymous user"""
        # form validation guarentees we have either registration or login info
        # pylint: disable=protected-access
        if form.cleaned_data.get('register_full_name'):
            user = self.miniregister(
                form,
                form.cleaned_data['register_full_name'],
                form.cleaned_data['register_email'],
                form.cleaned_data.get('register_newsletter'),
            )
            return user
        else:
            login(self.request, form._user)
            return form._user

    def form_valid(self, form):
        """Create the request"""
        if self.request.user.is_authenticated:
            user = self.request.user
        else:
            try:
                user = self._handle_anonymous_submitter(form)
            except requests.exceptions.RequestException:
                return self.form_invalid(form)
        if form.cleaned_data['action'] in ('save', 'submit'):
            composer = form.save(commit=False)
            composer.user = user
            composer.organization = user.profile.organization
            composer.save()
            form.save_m2m()
            # if a new agency is added while the user is anonymous,
            # we want to associate that agency to the user once they
            # login or register
            composer.agencies.filter(
                user=None, status='pending'
            ).update(user=user)
        if form.cleaned_data['action'] == 'save':
            self.request.session['ga'] = 'request_drafted'
            mixpanel_event(
                self.request,
                'Request Saved',
                self._composer_mixpanel_properties(composer),
            )
            messages.success(self.request, 'Request saved')
        elif form.cleaned_data['action'] == 'submit':
            self._submit_composer(composer, form)
        return redirect(composer)

class UpdateComposer(LoginRequiredMixin, GenericComposer, UpdateView):
    """Update a composer"""
    # pylint: disable=attribute-defined-outside-init
    pk_url_kwarg = 'idx'

    def get_queryset(self):
        """Restrict to composers you can view"""
        kwargs = {}
        # non staff may only see their own requests
        if not self.request.user.is_staff:
            kwargs['user'] = self.request.user
        # can only post to draft composers
        if self.request.method == 'POST':
            kwargs['status'] = 'started'
        return FOIAComposer.objects.filter(**kwargs)

    def get_object(self, queryset=None):
        """Convert object back to draft if it has been submitted recently"""
        composer = super(UpdateComposer, self).get_object(queryset)
        if composer.revokable():
            composer.revoke()
            messages.warning(
                self.request,
                'This request\'s submission has been cancelled.  You may now '
                'edit it and submit it again when ready',
            )
        return composer

    def post(self, request, *args, **kwargs):
        """Allow deletion regardless of form validation"""
        self.object = self.get_object()
        if request.POST.get('action') == 'delete':
            if self.object.has_perm(request.user, 'delete'):
                self.object.delete()
                messages.success(self.request, 'Draft deleted')
                return redirect('foia-mylist-drafts')
            else:
                messages.success(
                    self.request,
                    'You do not have permission to delete that draft',
                )
                return redirect('foia-mylist-drafts')
        return super(UpdateComposer, self).post(request, *args, **kwargs)

    def get(self, request, *args, **kwargs):
        """Redirect if this composer is not a draft"""
        self.object = self.get_object()
        if self.object.status != 'started':
            messages.warning(
                self.request, 'This request can no longer be updated.'
            )
            return redirect(self.object)
        else:
            return self.render_to_response(self.get_context_data())

    def form_valid(self, form):
        """Update the request"""
        if form.cleaned_data['action'] == 'save':
            composer = form.save()
            self.request.session['ga'] = 'request_drafted'
            mixpanel_event(
                self.request,
                'Request Saved',
                self._composer_mixpanel_properties(composer),
            )
            messages.success(self.request, 'Request saved')
        elif form.cleaned_data['action'] == 'submit':
            composer = form.save()
            self._submit_composer(composer, form)
        return redirect(composer)

@login_required
@require_POST
def autosave(request, idx):
    """Save the composer via AJAX"""
    composer = get_object_or_404(
        FOIAComposer,
        pk=idx,
        status='started',
        user=request.user,
    )
    old_agencies = set(composer.agencies.all())
    data = request.POST.copy()
    # we are always just saving
    data['action'] = 'save'
    form = BaseComposerForm(
        data,
        instance=composer,
        user=request.user,
        request=request,
    )
    if form.is_valid():
        composer = form.save()
        new_agencies = set(composer.agencies.all())
        removed_agencies = old_agencies - new_agencies
        # delete pending agencies which have been removed from composers and requests
        for agency in removed_agencies:
            if (
                agency.status == 'pending' and agency.composers.count() == 0
                and agency.foiarequest_set.count() == 0
            ):
                agency.delete()
        return HttpResponse('OK')
    else:
        return HttpResponseBadRequest(form.errors.as_json())
<|endoftext|>