text:
"""Тесты для разнообразных запросов."""
import pandas as pd
import pytest
from requests import Session

from apimoex import client
from apimoex import requests

@pytest.fixture(scope="module", name="session")
def make_session():
    """Создание http сессии."""
    with Session() as session:
        yield session

def test_make_query_empty():
    # noinspection PyProtectedMember
    query = requests._make_query()
    assert query == {}

def test_make_query_full():
    # noinspection PyProtectedMember
    query = requests._make_query(
        q="GAZP",
        interval=60,
        start="2019-10-09",
        end="2019-11-12",
        table="new_table",
        columns=("4", "a"),
    )
    assert isinstance(query, dict)
    assert len(query) == 6
    assert query["q"] == "GAZP"
    assert query["interval"] == 60
    assert query["from"] == "2019-10-09"
    assert query["till"] == "2019-11-12"
    assert query["iss.only"] == f"new_table,history.cursor"
    assert query["new_table.columns"] == "4,a"

def test_get_table():
    # noinspection PyProtectedMember
    query = requests._get_table(
        dict(a="b"), "a"
    )
    assert query == "b"

def test_get_table_notable():
    with pytest.raises(client.ISSMoexError) as error:
        # noinspection PyProtectedMember
        requests._get_table(
            dict(a="b"), "b"
        )
    assert "Отсутствует таблица b в данных" in str(error.value)

def test_get_reference(session):
    data = requests.get_reference(session, "engines")
    assert isinstance(data, list)
    assert len(data) == 9
    assert data == [
        {"id": 1, "name": "stock", "title": "Фондовый рынок и рынок депозитов"},
        {"id": 2, "name": "state", "title": "Рынок ГЦБ (размещение)"},
        {"id": 3, "name": "currency", "title": "Валютный рынок"},
        {"id": 4, "name": "futures", "title": "Срочный рынок"},
        {"id": 5, "name": "commodity", "title": "Товарный рынок"},
        {"id": 6, "name": "interventions", "title": "Товарные интервенции"},
        {"id": 7, "name": "offboard", "title": "ОТС-система"},
        {'id': 9, 'name': 'agro', 'title': 'Агро'},
        {'id': 1012, 'name': 'otc', 'title': 'ОТС с ЦК'}
    ]

check_points = [
    ("1-02-65104-D", {"UPRO", "EONR", "OGK4"}),
    ("10301481B", {"SBER", "SBER03"}),
    ("20301481B", {"SBERP", "SBERP03"}),
    ("1-02-06556-A", {"PHOR"}),
]

@pytest.mark.parametrize("reg_number, expected", check_points)
def test_find_securities(session, reg_number, expected):
    data = requests.find_securities(session, reg_number)
    assert isinstance(data, list)
    assert expected == {row["secid"] for row in data if row["regnumber"] == reg_number}

def test_find_security_description(session):
    data = requests.find_security_description(session, "IRAO")
    print(data)
    assert isinstance(data, list)
    assert len(data) == 19
    assert data[8] == dict(name="ISSUEDATE", title="Дата начала торгов", value="2009-12-01")
    # data = [
    #     {'name': 'SECID', 'title': 'Код ценной бумаги', 'value': 'IRAO'},
    #     {'name': 'NAME', 'title': 'Полное наименование', 'value': '"Интер РАО" ПАО ао'},
    #     {'name': 'SHORTNAME', 'title': 'Краткое наименование', 'value': 'ИнтерРАОао'},
    #     {'name': 'ISIN', 'title': 'ISIN код', 'value': 'RU000A0JPNM1'},
    #     {'name': 'REGNUMBER', 'title': 'Номер государственной регистрации', 'value': '1-04-33498-E'},
    #     {'name': 'ISSUESIZE', 'title': 'Объем выпуска', 'value': '104400000000'},
    #     {'name': 'FACEVALUE', 'title': 'Номинальная стоимость', 'value': '2.80977'},
    #     {'name': 'FACEUNIT', 'title': 'Валюта номинала', 'value': 'SUR'},
    #     {'name': 'ISSUEDATE', 'title': 'Дата начала торгов', 'value': '2009-12-01'},
    #     {'name': 'LATNAME', 'title': 'Английское наименование', 'value': 'Inter RAO ao'},
    #     {'name': 'LISTLEVEL', 'title': 'Уровень листинга', 'value': '1'},
    #     {'name': 'ISQUALIFIEDINVESTORS', 'title': 'Бумаги для квалифицированных инвесторов', 'value': '0'},
    #     {'name': 'MORNINGSESSION', 'title': 'Допуск к утренней дополнительной торговой сессии', 'value': '1'},
    #     {'name': 'EVENINGSESSION', 'title': 'Допуск к вечерней дополнительной торговой сессии', 'value': '1'},
    #     {'name': 'TYPENAME', 'title': 'Вид/категория ценной бумаги', 'value': 'Акция обыкновенная'},
    #     {'name': 'GROUP', 'title': 'Код типа инструмента', 'value': 'stock_shares'},
    #     {'name': 'TYPE', 'title': 'Тип бумаги', 'value': 'common_share'},
    #     {'name': 'GROUPNAME', 'title': 'Типа инструмента', 'value': 'Акции'},
    #     {'name': 'EMITTER_ID', 'title': 'Код эмитента', 'value': '2140'}
    # ]

def test_get_market_candle_borders(session):
    data = requests.get_market_candle_borders(session, "SNGSP")
    assert isinstance(data, list)
    assert len(data) == 7
    for i in data:
        del i["end"]
    assert data == [
        {"begin": "2011-12-15 10:00:00", "interval": 1, "board_group_id": 57},
        {"begin": "2003-07-01 00:00:00", "interval": 4, "board_group_id": 57},
        {"begin": "2003-07-28 00:00:00", "interval": 7, "board_group_id": 57},
        {"begin": "2011-12-08 10:00:00", "interval": 10, "board_group_id": 57},
        {"begin": "2003-07-31 00:00:00", "interval": 24, "board_group_id": 57},
        {"begin": "2003-07-01 00:00:00", "interval": 31, "board_group_id": 57},
        {"begin": "2011-11-17 10:00:00", "interval": 60, "board_group_id": 57},
    ]

def test_get_board_candle_borders(session):
    data = requests.get_board_candle_borders(session, "UPRO")
    assert isinstance(data, list)
    assert len(data) == 7
    for i in data:
        del i["end"]
    assert data == [
        {"begin": "2016-07-01 09:59:00", "interval": 1},
        {"begin": "2016-07-01 00:00:00", "interval": 4},
        {"begin": "2016-06-27 00:00:00", "interval": 7},
        {"begin": "2016-07-01 09:50:00", "interval": 10},
        {"begin": "2016-07-01 00:00:00", "interval": 24},
        {"begin": "2016-07-01 00:00:00", "interval": 31},
        {"begin": "2016-07-01 09:00:00", "interval": 60},
    ]

def test_get_market_candles_from_beginning(session):
    data = requests.get_market_candles(session, "RTKM", interval=1, end="2011-12-16")
    assert isinstance(data, list)
    assert len(data) > 500
    df = pd.DataFrame(data)
    assert df.columns.size == 6
    assert df.loc[0, "open"] == pytest.approx(141.55)
    assert df.loc[1, "close"] == pytest.approx(141.59)
    assert df.loc[2, "high"] == pytest.approx(142.4)
    assert df.loc[3, "low"] == pytest.approx(140.81)
    assert df.loc[4, "value"] == pytest.approx(2_586_296.9)
    assert df.loc[6, "begin"] == "2011-12-15 10:06:00"

def test_get_market_candles_to_end(session):
    data = requests.get_market_candles(session, "LSRG", interval=4, start="2008-01-01")
    assert isinstance(data, list)
    assert len(data) > 47
    df = pd.DataFrame(data)
    assert df.columns.size == 6
    assert df.loc[0, "open"] == pytest.approx(1130)
    assert df.loc[1, "close"] == pytest.approx(970)
    assert df.loc[2, "high"] == pytest.approx(1045)
    assert df.loc[3, "low"] == pytest.approx(429.9)
    assert df.loc[4, "value"] == pytest.approx(1109833660.9)
    assert df.loc[6, "begin"] == "2012-07-01 00:00:00"

def test_get_market_candles_empty_history(session):
    data = requests.get_market_candles(session, "KSGR", interval=24)
    assert isinstance(data, list)
    assert len(data) == 0

def test_get_board_candles_from_beginning(session):
    data = requests.get_board_candles(session, "MTSS", interval=10, end="2011-12-22")
    assert isinstance(data, list)
    assert len(data) > 500
    df = pd.DataFrame(data)
    assert df.columns.size == 6
    assert df.loc[0, "open"] == pytest.approx(202.7)
    assert df.loc[1, "close"] == pytest.approx(204.12)
    assert df.loc[2, "high"] == pytest.approx(205)
    assert df.loc[3, "low"] == pytest.approx(204.93)
    assert df.loc[4, "value"] == pytest.approx(3_990_683.9)
    assert df.loc[6, "begin"] == "2011-12-08 11:00:00"

def test_get_board_candles_to_end(session):
    data = requests.get_board_candles(session, "TTLK", interval=31, start="2014-07-01")
    assert isinstance(data, list)
    assert len(data) > 52
    df = pd.DataFrame(data)
    assert df.columns.size == 6
    assert df.loc[0, "open"] == pytest.approx(0.152)
    assert df.loc[1, "close"] == pytest.approx(0.15689)
    assert df.loc[2, "high"] == pytest.approx(0.15998)
    assert df.loc[3, "low"] == pytest.approx(0.149)
    assert df.loc[4, "value"] == pytest.approx(2_713_625)
    assert df.loc[6, "begin"] == "2015-01-01 00:00:00"

def test_get_board_dates(session):
    data = requests.get_board_dates(session)
    assert isinstance(data, list)
    assert len(data) == 1
    assert isinstance(data[0]['from'], str)
    assert isinstance(data[0]['till'], str)
    assert len(data[0]['from']) == 10
    assert len(data[0]['till']) == 10

def test_get_board_securities(session):
    data = requests.get_board_securities(session)
    assert isinstance(data, list)
    assert len(data) > 200
    df = pd.DataFrame(data)
    df.set_index("SECID", inplace=True)
    assert df.loc["AKRN", "SHORTNAME"] == "Акрон"
    assert df.loc["GAZP", "REGNUMBER"] == "1-02-00028-A"
    assert df.loc["TTLK", "LOTSIZE"] == 1000
    assert df.loc["MRSB", "SHORTNAME"] == "МордЭнСб"
    assert df.loc["MRSB", "REGNUMBER"] == "1-01-55055-E"
    assert df.loc["MRSB", "LOTSIZE"] == 10000
    assert df.index[0] == "ABRD"
    assert df["SHORTNAME"].iat[0] == "АбрауДюрсо"
    assert df["REGNUMBER"].iat[0] == "1-02-12500-A"
    assert df["LOTSIZE"].iat[0] == 10
    assert df["SHORTNAME"].iat[-1] == "ЗВЕЗДА ао"
    assert df["REGNUMBER"].iat[-1] == "1-01-00169-D"
    assert df["LOTSIZE"].iat[-1] == 1000
    assert df.index[-1] == "ZVEZ"

def test_get_market_history_from_beginning(session):
    data = requests.get_market_history(session, "AKRN", end="2006-12-01")
    assert isinstance(data, list)
    assert data[0]["TRADEDATE"] == "2006-10-11"
    assert data[-1]["TRADEDATE"] == "2006-12-01"
    assert len(data[-2]) == 5
    assert "CLOSE" in data[2]
    assert "VOLUME" in data[3]

def test_get_market_history_to_end(session):
    data = requests.get_market_history(session, "MOEX", start="2017-10-02")
    assert isinstance(data, list)
    assert len(data) > 100
    assert data[0]["TRADEDATE"] == "2017-10-02"
    assert data[-1]["TRADEDATE"] >= "2018-11-19"

def test_get_board_history_from_beginning(session):
    data = requests.get_board_history(session, "LSNGP", end="2014-08-01")
    df = pd.DataFrame(data)
    df.set_index("TRADEDATE", inplace=True)
    assert len(df.columns) == 4
    assert df.index[0] == "2014-06-09"
    assert df.at["2014-06-09", "CLOSE"] == pytest.approx(14.7)
    assert df.at["2014-08-01", "VOLUME"] == 4000

def test_get_board_history_to_end(session):
    data = requests.get_board_history(session, "LSRG", start="2018-08-07")
    df = pd.DataFrame(data)
    df.set_index("TRADEDATE", inplace=True)
    assert len(df.columns) == 4
    assert df.index[0] == "2018-08-07"
    assert df.index[-1] >= "2018-11-19"
    assert df.at["2018-08-07", "CLOSE"] == 777
    assert df.at["2018-08-10", "VOLUME"] == 11313
    assert df.at["2018-08-10", "BOARDID"] == "TQBR"
    assert df.at["2018-08-10", "VALUE"] == pytest.approx(8_626_464.5)
    assert df.at["2018-09-06", "CLOSE"] == pytest.approx(660)
    assert df.at["2018-08-28", "VOLUME"] == 47428

def test_get_index_tickers(session):
    data = requests.get_index_tickers(session, index='IMOEX', date='2023-03-03')
    assert len(data) == 40
    assert data[15]['ticker'] == 'MAGN'
    assert data[25]['till'] == '2023-03-03'
    assert data[35]['tradingsession'] == 3


text:
import typing

import pytest
import requests

from apimoex import client

@pytest.fixture(scope="module", name="session")
def make_session():
    with requests.Session() as session:
        yield session

def test_iss_client_iterable():
    assert issubclass(client.ISSClient, typing.Iterable)

def test_repr(session):
    iss = client.ISSClient(session, "test_url", dict(a="b"))
    assert str(iss) == "ISSClient(url=test_url, query={'a': 'b'})"

def test_get(session):
    url = "https://iss.moex.com/iss/securities.json"
    query = dict(q="1-02-65104-D")
    iss = client.ISSClient(session, url, query)
    raw = iss.get()
    assert isinstance(raw, dict)
    assert len(raw) == 1
    data = raw["securities"]
    assert isinstance(data, list)
    assert len(data) == 4
    assert isinstance(data[0], dict)
    assert data[1]["regnumber"] == "1-02-65104-D"

def test_get_with_start(session):
    url = "https://iss.moex.com/iss/securities.json"
    query = dict(q="1-02-65104-D")
    iss = client.ISSClient(session, url, query)
    raw = iss.get(1)
    assert isinstance(raw, dict)
    assert len(raw) == 1
    data = raw["securities"]
    assert isinstance(data, list)
    assert len(data) == 3
    assert isinstance(data[0], dict)
    assert data[1]["regnumber"] == "1-02-65104-D"

def test_get_wrong_url(session):
    url = "https://iss.moex.com/iss/securities1.json"
    iss = client.ISSClient(session, url)
    with pytest.raises(client.ISSMoexError) as error:
        iss.get()
    assert "Неверный url" in str(error.value)
    assert "https://iss.moex.com/iss/securities1.json?iss.json=extended&iss.meta=off" in str(error.value)

def test_get_wrong_json(monkeypatch, session):
    url = "https://iss.moex.com/iss/securities.json"
    iss = client.ISSClient(session, url)
    # noinspection PyProtectedMember
    monkeypatch.setattr(requests.Response, "json", lambda x: [0, 1, 2])
    with pytest.raises(client.ISSMoexError) as error:
        iss.get()
    assert "Ответ содержит некорректные данные" in str(error.value)
    assert "https://iss.moex.com/iss/securities.json?iss.json=extended&iss.meta=off" in str(error.value)

def test_make_query_empty(session):
    iss = client.ISSClient(session, "test_url")
    # noinspection PyProtectedMember
    query = iss._make_query()
    assert isinstance(query, dict)
    assert len(query) == 2
    assert query["iss.json"] == "extended"
    assert query["iss.meta"] == "off"

def test_make_query_not_empty(session):
    iss = client.ISSClient(session, "test_url", dict(test_param="test_value"))
    # noinspection PyProtectedMember
    query = iss._make_query()
    assert isinstance(query, dict)
    assert len(query) == 3
    assert query["iss.json"] == "extended"
    assert query["iss.meta"] == "off"
    assert query["test_param"] == "test_value"

def test_make_query_not_empty_with_start(session):
    iss = client.ISSClient(session, "test_url", dict(test_param="test_value"))
    # noinspection PyProtectedMember
    query = iss._make_query(704)
    assert isinstance(query, dict)
    assert len(query) == 4
    assert query["iss.json"] == "extended"
    assert query["iss.meta"] == "off"
    assert query["test_param"] == "test_value"
    assert query["start"] == 704

def test_get_all_with_cursor(session):
    url = "https://iss.moex.com/iss/history/engines/stock/markets/shares/securities/SNGSP.json"
    query = {"from": "2018-01-01", "till": "2018-03-01"}
    iss = client.ISSClient(session, url, query)
    raw = iss.get_all()
    assert isinstance(raw, dict)
    assert len(raw) == 1
    data = raw["history"]
    assert isinstance(data, list)
    assert len(data) > 100
    assert data[0]["TRADEDATE"] == "2018-01-03"
    assert data[-1]["TRADEDATE"] == "2018-03-01"
    for row in data:
        for column in ["TRADEDATE", "OPEN", "LOW", "HIGH", "CLOSE", "VOLUME"]:
            assert column in row

def test_get_all_without_cursor(session):
    url = "https://iss.moex.com/iss/history/engines/stock/markets/shares/boards/TQBR/securities/SNGSP.json"
    query = {"from": "2018-01-03", "till": "2018-06-01"}
    iss = client.ISSClient(session, url, query)
    raw = iss.get_all()
    assert isinstance(raw, dict)
    assert len(raw) == 1
    data = raw["history"]
    assert isinstance(data, list)
    assert len(data) > 100
    assert data[0]["TRADEDATE"] == "2018-01-03"
    assert data[-1]["TRADEDATE"] == "2018-06-01"
    for row in data:
        for column in ["TRADEDATE", "OPEN", "LOW", "HIGH", "CLOSE", "VOLUME"]:
            assert column in row

def test_wrong_cursor_size(monkeypatch, session):
    iss = client.ISSClient(session, "")
    fake_cursor = {"history.cursor": [0, 1]}

    monkeypatch.setattr(iss, "get", lambda x: fake_cursor)
    with pytest.raises(client.ISSMoexError) as error:
        iss.get_all()
    assert f"Некорректные данные history.cursor [0, 1] для начальной позиции 0" in str(
        error.value
    )

def test_wrong_cursor_index(monkeypatch, session):
    iss = client.ISSClient(session, "")
    fake_cursor = {"history.cursor": [{"INDEX": 1}]}

    monkeypatch.setattr(iss, "get", lambda x: fake_cursor)
    with pytest.raises(client.ISSMoexError) as error:
        iss.get_all()
    assert "Некорректные данные history.cursor [{'INDEX': 1}] для начальной позиции 0" in str(
        error.value
    )

repo_language: Python

mime_type: text/plain


text:
"""Клиент для MOEX ISS."""
from collections import abc
from typing import Dict
from typing import List
from typing import Optional
from typing import Union

import requests

# Стандартные настройки для запроса расширенного представления json без дополнительных метаданных.
BASE_QUERY = {"iss.json": "extended", "iss.meta": "off"}

class ISSMoexError(Exception):
    """Базовое исключение."""
    pass

class ISSClient(abc.Iterable):
    """Клиент для MOEX ISS.

    Для работы клиента необходимо передать requests.Session.

    Загружает данные для простых ответов с помощью метода get. Для ответов состоящих из нескольких блоков данных
    поддерживается протокол итерируемого для отдельных блоков или метод get_all для их автоматического сбора.
    """

    def __init__(self, session: requests.Session, url: str, query: dict = None):
        """MOEX ISS является REST сервером.

        Полный перечень запросов и параметров к ним https://iss.moex.com/iss/reference/
        Дополнительное описание https://fs.moex.com/files/6523

        :param session:
            Сессия интернет соединения.
        :param url:
            Адрес запроса.
        :param query:
            Перечень дополнительных параметров запроса. К списку дополнительных параметров всегда добавляется
            требование предоставить ответ в виде расширенного json без метаданных.
        """
        self._session = session
        self._url = url
        self._query = query or dict()

    def __repr__(self) -> str:
        return f"{self.__class__.__name__}(url={self._url}, query={self._query})"

    def __iter__(self) -> Dict[str, List[Dict[str, Union[str, int, float]]]]:
        """Генератор по ответам состоящим из нескольких блоков.

        На часть запросов выдается только начальный блок данных (обычно из 100 элементов). Генератор обеспечивает
        загрузку всех блоков. При этом в ответах на некоторые запросы может содержаться курсор с положением текущего
        блока данных (позволяет сэкономить один запрос). Генератор обеспечивает обработку ответов как с курсором, так и
        без него.

        Ответ представляет словарь, каждый из ключей которого отдельная таблица с данными. Таблица представлена в виде
        списка словарей, где каждый ключ словаря соответствует отдельному столбцу.
        """
        start = 0
        while True:
            data = self.get(start)
            if "history.cursor" in data:
                cursor, *wrong_data = data["history.cursor"]
                if len(wrong_data) != 0 or cursor["INDEX"] != start:
                    raise ISSMoexError(
                        f"Некорректные данные history.cursor {data['history.cursor']} для начальной позиции {start}"
                    )
                del data["history.cursor"]
                yield data
                start += cursor["PAGESIZE"]
                if start >= cursor["TOTAL"]:
                    return
            else:
                # Наименование ключа может быть любым
                key = next(iter(data))
                block_size = len(data[key])
                yield data
                if not block_size:
                    return
                start += block_size

    def get(
        self, start: Optional[int] = None
    ) -> Dict[str, List[Dict[str, Union[str, int, float]]]]:
        """Загрузка данных.

        :param start:
            Номер элемента с которого нужно загрузить данные. Используется для дозагрузки данных, состоящих из
            нескольких блоков. При отсутствии данные загружаются с начального элемента.
        :return:
            Блок данных с отброшенной вспомогательной информацией - словарь, каждый ключ которого
            соответствует одной из таблиц с данными. Таблицы являются списками словарей, которые напрямую конвертируются
            в pandas.DataFrame.
        """
        query = self._make_query(start)
        with self._session.get(self._url, params=query) as respond:
            try:
                respond.raise_for_status()
            except requests.HTTPError:
                raise ISSMoexError("Неверный url", respond.url)
            else:
                _, data, *wrong_data = respond.json()
        if len(wrong_data) != 0:
            raise ISSMoexError("Ответ содержит некорректные данные", respond.url)
        return data

    def _make_query(self, start=None) -> Dict[str, Union[str, int]]:
        """К общему набору параметров запроса добавляется требование предоставить ответ в виде расширенного json."""
        query = dict(**BASE_QUERY, **self._query)
        if start:
            query["start"] = start
        return query

    def get_all(self) -> Dict[str, List[Dict[str, Union[str, int, float]]]]:
        """Собирает все блоки данных для запросов, ответы на которые выдаются по частям отдельными блоками.

        :return:
            Объединенные из всех блоков данные с отброшенной вспомогательной информацией - словарь, каждый ключ которого
            соответствует одной из таблиц с данными. Таблицы являются списками словарей, которые напрямую конвертируются
            в pandas.DataFrame.
        """
        all_data = dict()
        for data in self:
            # noinspection PyUnresolvedReferences
            for key, value in data.items():
                all_data.setdefault(key, []).extend(value)
        return all_data


text:
"""Реализовано несколько функций-запросов информации о торгуемых акциях и их исторических котировках, результаты которых
напрямую конвертируются в pandas.DataFrame.

Работа функций базируется на универсальном клиенте, позволяющем осуществлять произвольные запросы к MOEX ISS, поэтому
перечень доступных функций-запросов может быть легко расширен.
"""
from .client import ISSClient

from .requests import *

__version__ = "1.3.0"


text:
"""Реализация части запросов к MOEX ISS.

При необходимости могут быть дополнены:
    Полный перечень запросов https://iss.moex.com/iss/reference/
    Дополнительное описание https://fs.moex.com/files/6523
"""
from typing import Dict
from typing import List
from typing import Optional
from typing import Tuple
from typing import Union

import requests

from . import client

__all__ = [
    "get_reference",
    "find_securities",
    "find_security_description",
    "get_market_candle_borders",
    "get_board_candle_borders",
    "get_market_candles",
    "get_board_candles",
    "get_board_dates",
    "get_board_securities",
    "get_market_history",
    "get_board_history",
    "get_index_tickers",
]

def _make_query(
    *,
    q: Optional[str] = None,
    interval: Optional[int] = None,
    start: Optional[str] = None,
    end: Optional[str] = None,
    date: Optional[str] = None,
    table: Optional[str] = None,
    columns: Optional[Tuple[str, ...]] = None,
) -> Dict[str, Union[str, int]]:
    """Формирует дополнительные параметры запроса к MOEX ISS.

    В случае None значений не добавляются в запрос.

    :param q:
        Строка с частью характеристик бумаги для поиска.
    :param interval:
        Размер свечки.
    :param start:
        Начальная дата котировок.
    :param end:
        Конечная дата котировок.
    :param date:
        Точная дата (используется при получении тикеров в индексе).
    :param table:
        Таблица, которую нужно загрузить (для запросов, предполагающих наличие нескольких таблиц).
    :param columns:
        Кортеж столбцов, которые нужно загрузить.

    :return:
        Словарь с дополнительными параметрами запроса.
    """
    query = dict()
    if q:
        query["q"] = q
    if interval:
        query["interval"] = interval
    if start:
        query["from"] = start
    if end:
        query["till"] = end
    if date:
        query["date"] = date
    if table:
        query["iss.only"] = f"{table},history.cursor"
    if columns:
        query[f"{table}.columns"] = ",".join(columns)
    return query

def _get_table(data: dict, table: str) -> list:
    """Извлекает конкретную таблицу из данных."""
    try:
        data = data[table]
    except KeyError:
        raise client.ISSMoexError(f"Отсутствует таблица {table} в данных")
    return data

def _get_short_data(
    session: requests.Session, url: str, table: str, query: Optional[dict] = None
) -> List[Dict[str, Union[str, int, float]]]:
    """Получить данные для запроса с выдачей всей информации за раз.

    :param session:
        Сессия интернет соединения.
    :param url:
        URL запроса.
    :param table:
        Таблица, которую нужно выбрать.
    :param query:
        Дополнительные параметры запроса.

    :return:
        Конкретная таблица из запроса.
    """
    iss = client.ISSClient(session, url, query)
    data = iss.get()
    return _get_table(data, table)

def _get_long_data(
    session: requests.Session, url, table, query=None
) -> List[Dict[str, Union[str, int, float]]]:
    """Получить данные для запроса, в котором информация выдается несколькими блоками.

    :param session:
        Сессия интернет соединения.
    :param url:
        URL запроса.
    :param table:
        Таблица, которую нужно выбрать.
    :param query:
        Дополнительные параметры запроса.

    :return:
        Конкретная таблица из запроса.
    """
    iss = client.ISSClient(session, url, query)
    data = iss.get_all()
    return _get_table(data, table)

def get_reference(
    session: requests.Session, placeholder: str = "boards"
) -> List[Dict[str, Union[str, int, float]]]:
    """Получить перечень доступных значений плейсхолдера в адресе запроса.

    Например в описание запроса https://iss.moex.com/iss/reference/32 присутствует следующий адрес
    /iss/engines/[engine]/markets/[market]/boards/[board]/securities с плейсхолдерами engines, markets и boards.

    Описание запроса - https://iss.moex.com/iss/reference/28

    :param session:
        Сессия интернет соединения.
    :param placeholder:
        Наименование плейсхолдера в адресе запроса: engines, markets, boards, boardgroups, durations, securitytypes,
        securitygroups, securitycollections

    :return:
        Список словарей, которые напрямую конвертируется в pandas.DataFrame.
    """
    url = "https://iss.moex.com/iss/index.json"
    return _get_short_data(session, url, placeholder)

def find_securities(
    session: requests.Session,
    string: str,
    columns: Optional[Tuple[str, ...]] = ("secid", "regnumber"),
) -> List[Dict[str, Union[str, int, float]]]:
    """Найти инструменты по части Кода, Названию, ISIN, Идентификатору Эмитента, Номеру гос.регистрации.

    Один из вариантов использования - по регистрационному номеру узнать предыдущие тикеры эмитента, и с помощью
    нескольких запросов об истории котировок собрать длинную историю с использованием всех предыдущих тикеров.

    Описание запроса - https://iss.moex.com/iss/reference/5

    :param session:
        Сессия интернет соединения.
    :param string:
        Часть Кода, Названия, ISIN, Идентификатора Эмитента, Номера гос.регистрации.
    :param columns:
        Кортеж столбцов, которые нужно загрузить - по умолчанию тикер и номер государственно регистрации.
        Если пустой или None, то загружаются все столбцы.

    :return:
        Список словарей, которые напрямую конвертируется в pandas.DataFrame.
    """
    url = "https://iss.moex.com/iss/securities.json"
    table = "securities"
    query = _make_query(q=string, table=table, columns=columns)
    return _get_short_data(session, url, table, query)

def find_security_description(
    session: requests.Session,
    security: str,
    columns: Optional[Tuple[str, ...]] = ("name", "title", "value"),
) -> List[Dict[str, Union[str, int, float]]]:
    """Получить спецификацию инструмента.

    Один из вариантов использования - по тикеру узнать дату начала торгов.

    Описание запроса - https://iss.moex.com/iss/reference/13

    :param session:
        Сессия интернет соединения.
    :param security:
        Тикер ценной бумаги.
    :param columns:
        Кортеж столбцов, которые нужно загрузить - по умолчанию краткое название, длинное название на русском и значение
        показателя.

    :return:
        Список словарей, которые напрямую конвертируется в pandas.DataFrame.
    """
    url = f"https://iss.moex.com/iss/securities/{security}.json"
    table = "description"
    query = _make_query(table=table, columns=columns)
    return _get_short_data(session, url, table, query)

def get_market_candle_borders(
    session: requests.Session,
    security: str,
    market: str = "shares",
    engine: str = "stock",
) -> List[Dict[str, Union[str, int, float]]]:
    """Получить таблицу интервалов доступных дат для свечей различного размера на рынке для всех режимов торгов.

    Описание запроса - https://iss.moex.com/iss/reference/156

    :param session:
        Сессия интернет соединения.
    :param security:
        Тикер ценной бумаги.
    :param market:
        Рынок - по умолчанию акции.
    :param engine:
        Движок - по умолчанию акции.

    :return:
        Список словарей, которые напрямую конвертируется в pandas.DataFrame.
    """
    url = f"https://iss.moex.com/iss/engines/{engine}/markets/{market}/securities/{security}/candleborders.json"
    table = "borders"
    return _get_short_data(session, url, table)

def get_board_candle_borders(
    session: requests.Session,
    security: str,
    board: str = "TQBR",
    market: str = "shares",
    engine: str = "stock",
) -> List[Dict[str, Union[str, int, float]]]:
    """Получить таблицу интервалов доступных дат для свечей различного размера в указанном режиме торгов.

    Описание запроса - https://iss.moex.com/iss/reference/48

    :param session:
        Сессия интернет соединения.
    :param security:
        Тикер ценной бумаги.
    :param board:
        Режим торгов - по умолчанию основной режим торгов T+2.
    :param market:
        Рынок - по умолчанию акции.
    :param engine:
        Движок - по умолчанию акции.

    :return:
        Список словарей, которые напрямую конвертируется в pandas.DataFrame.
    """
    url = (
        f"https://iss.moex.com/iss/engines/{engine}/markets/{market}/"
        f"boards/{board}/securities/{security}/candleborders.json"
    )
    table = "borders"
    return _get_short_data(session, url, table)

def get_market_candles(
    session: requests.Session,
    security: str,
    interval: int = 24,
    start: Optional[str] = None,
    end: Optional[str] = None,
    columns: Optional[Tuple[str, ...]] = (
            "begin",
            "open",
            "close",
            "high",
            "low",
            "value",
    ),
    market: str = "shares",
    engine: str = "stock",
) -> List[Dict[str, Union[str, int, float]]]:
    """Получить свечи в формате HLOCV указанного инструмента на рынке для основного режима торгов за интервал дат.

    Если торговля идет в нескольких основных режимах, то на один интервал времени может быть выдано несколько свечек -
    по свечке на каждый режим. Предположительно такая ситуация может произойти для свечек длиннее 1 дня в периоды, когда
    менялся режим торгов.

    Описание запроса - https://iss.moex.com/iss/reference/155

    :param session:
        Сессия интернет соединения.
    :param security:
        Тикер ценной бумаги.
    :param interval:
        Размер свечки - целое число 1 (1 минута), 10 (10 минут), 60 (1 час), 24 (1 день), 7 (1 неделя), 31 (1 месяц) или
        4 (1 квартал). По умолчанию дневные данные.
    :param start:
        Дата вида ГГГГ-ММ-ДД. При отсутствии данные будут загружены с начала истории.
    :param end:
        Дата вида ГГГГ-ММ-ДД. При отсутствии данные будут загружены до конца истории. Для текущего дня будут
        загружены не окончательные данные, если торги продолжаются.
    :param columns:
        Кортеж столбцов, которые нужно загрузить - по умолчанию момент начала свечки и HLOCV. Если пустой или None, то
        загружаются все столбцы.
    :param market:
        Рынок - по умолчанию акции.
    :param engine:
        Движок - по умолчанию акции.

    :return:
        Список словарей, которые напрямую конвертируется в pandas.DataFrame.
    """
    url = f"https://iss.moex.com/iss/engines/{engine}/markets/{market}/securities/{security}/candles.json"
    table = "candles"
    query = _make_query(interval=interval, start=start, end=end, table=table, columns=columns)
    return _get_long_data(session, url, table, query)

def get_board_candles(
    session: requests.Session,
    security: str,
    interval: int = 24,
    start: Optional[str] = None,
    end: Optional[str] = None,
    columns: Optional[Tuple[str, ...]] = (
            "begin",
            "open",
            "close",
            "high",
            "low",
            "value",
    ),
    board: str = "TQBR",
    market: str = "shares",
    engine: str = "stock",
) -> List[Dict[str, Union[str, int, float]]]:
    """Получить свечи в формате HLOCV указанного инструмента в указанном режиме торгов за интервал дат.

    Описание запроса - https://iss.moex.com/iss/reference/46

    :param session:
        Сессия интернет соединения.
    :param security:
        Тикер ценной бумаги.
    :param interval:
        Размер свечки - целое число 1 (1 минута), 10 (10 минут), 60 (1 час), 24 (1 день), 7 (1 неделя), 31 (1 месяц) или
        4 (1 квартал). По умолчанию дневные данные.
    :param start:
        Дата вида ГГГГ-ММ-ДД. При отсутствии данные будут загружены с начала истории.
    :param end:
        Дата вида ГГГГ-ММ-ДД. При отсутствии данные будут загружены до конца истории. Для текущего дня будут
        загружены не окончательные данные, если торги продолжаются.
    :param columns:
        Кортеж столбцов, которые нужно загрузить - по умолчанию момент начала свечки и HLOCV. Если пустой или None, то
        загружаются все столбцы.
    :param board:
        Режим торгов - по умолчанию основной режим торгов T+2.
    :param market:
        Рынок - по умолчанию акции.
    :param engine:
        Движок - по умолчанию акции.

    :return:
        Список словарей, которые напрямую конвертируется в pandas.DataFrame.
    """
    url = (
        f"https://iss.moex.com/iss/engines/{engine}/markets/{market}/"
        f"boards/{board}/securities/{security}/candles.json"
    )
    table = "candles"
    query = _make_query(interval=interval, start=start, end=end, table=table, columns=columns)
    return _get_long_data(session, url, table, query)

def get_board_dates(
    session: requests.Session,
    board: str = "TQBR",
    market: str = "shares",
    engine: str = "stock",
) -> List[Dict[str, Union[str, int, float]]]:
    """Получить интервал дат, доступных в истории для рынка по заданному режиму торгов.

    Описание запроса - https://iss.moex.com/iss/reference/26

    :param session:
        Сессия интернет соединения.
    :param board:
        Режим торгов - по умолчанию основной режим торгов T+2.
    :param market:
        Рынок - по умолчанию акции.
    :param engine:
        Движок - по умолчанию акции.

    :return:
        Список из одного элемента - словаря с ключами 'from' и 'till'.
    """
    url = f"https://iss.moex.com/iss/history/engines/{engine}/markets/{market}/boards/{board}/dates.json"
    table = "dates"
    return _get_short_data(session, url, table)

def get_board_securities(
    session: requests.Session,
    table: str = "securities",
    columns: Optional[Tuple[str, ...]] = ("SECID", "REGNUMBER", "LOTSIZE", "SHORTNAME"),
    board: str = "TQBR",
    market: str = "shares",
    engine: str = "stock",
) -> List[Dict[str, Union[str, int, float]]]:
    """Получить таблицу инструментов по режиму торгов со вспомогательной информацией.

    Описание запроса - https://iss.moex.com/iss/reference/32

    :param session:
        Сессия интернет соединения.
    :param table:
        Таблица с данными, которую нужно вернуть: securities - справочник торгуемых ценных бумаг, marketdata -
        данные с результатами торгов текущего дня.
    :param columns:
        Кортеж столбцов, которые нужно загрузить - по умолчанию тикер, номер государственно регистрации,
        размер лота и краткое название. Если пустой или None, то загружаются все столбцы.
    :param board:
        Режим торгов - по умолчанию основной режим торгов T+2.
    :param market:
        Рынок - по умолчанию акции.
    :param engine:
        Движок - по умолчанию акции.

    :return:
        Список словарей, которые напрямую конвертируется в pandas.DataFrame.
    """
    url = f"https://iss.moex.com/iss/engines/{engine}/markets/{market}/boards/{board}/securities.json"
    query = _make_query(table=table, columns=columns)
    return _get_short_data(session, url, table, query)

def get_market_history(
    session: requests.Session,
    security: str,
    start: Optional[str] = None,
    end: Optional[str] = None,
    columns: Optional[Tuple[str, ...]] = (
        "BOARDID",
        "TRADEDATE",
        "CLOSE",
        "VOLUME",
        "VALUE",
    ),
    market: str = "shares",
    engine: str = "stock",
) -> List[Dict[str, Union[str, int, float]]]:
    """Получить историю по одной бумаге на рынке для всех режимов торгов за интервал дат.

    На одну дату может приходиться несколько значений, если торги шли в нескольких режимах.

    Описание запроса - https://iss.moex.com/iss/reference/63

    :param session:
        Сессия интернет соединения.
    :param security:
        Тикер ценной бумаги.
    :param start:
        Дата вида ГГГГ-ММ-ДД. При отсутствии данные будут загружены с начала истории.
    :param end:
        Дата вида ГГГГ-ММ-ДД. При отсутствии данные будут загружены до конца истории.
    :param columns:
        Кортеж столбцов, которые нужно загрузить - по умолчанию режим торгов, дата торгов, цена закрытия и объем в
        штуках и стоимости. Если пустой или None, то загружаются все столбцы.
    :param market:
        Рынок - по умолчанию акции.
    :param engine:
        Движок - по умолчанию акции.

    :return:
        Список словарей, которые напрямую конвертируется в pandas.DataFrame.
    """
    url = f"https://iss.moex.com/iss/history/engines/{engine}/markets/{market}/securities/{security}.json"
    table = "history"
    query = _make_query(start=start, end=end, table=table, columns=columns)
    return _get_long_data(session, url, table, query)

def get_board_history(
    session: requests.Session,
    security: str,
    start: Optional[str] = None,
    end: Optional[str] = None,
    columns: Optional[Tuple[str, ...]] = (
        "BOARDID",
        "TRADEDATE",
        "CLOSE",
        "VOLUME",
        "VALUE",
    ),
    board: str = "TQBR",
    market: str = "shares",
    engine: str = "stock",
):
    """Получить историю торгов для указанной бумаги в указанном режиме торгов за указанный интервал дат.

    Описание запроса - https://iss.moex.com/iss/reference/65

    :param session:
        Сессия интернет соединения.
    :param security:
        Тикер ценной бумаги.
    :param start:
        Дата вида ГГГГ-ММ-ДД. При отсутствии данные будут загружены с начала истории.
    :param end:
        Дата вида ГГГГ-ММ-ДД. При отсутствии данные будут загружены до конца истории.
    :param columns:
        Кортеж столбцов, которые нужно загрузить - по умолчанию режим торгов, дата торгов, цена закрытия и объем в
        штуках и стоимости. Если пустой или None, то загружаются все столбцы.
    :param board:
        Режим торгов - по умолчанию основной режим торгов T+2.
    :param market:
        Рынок - по умолчанию акции.
    :param engine:
        Движок - по умолчанию акции.

    :return:
        Список словарей, которые напрямую конвертируется в pandas.DataFrame.
    """
    url = (
        f"https://iss.moex.com/iss/history/engines/{engine}/markets/{market}/"
        f"boards/{board}/securities/{security}.json"
    )
    table = "history"
    query = _make_query(start=start, end=end, table=table, columns=columns)
    return _get_long_data(session, url, table, query)

def get_index_tickers(
    session: requests.Session,
    index: str,
    date: Optional[str] = None,
    columns: Optional[Tuple[str, ...]] = (
        "ticker",
        "from",
        "till",
        "tradingsession",
    ),
    market: str = "index",
    engine: str = "stock",
):
    """Получить информацию по составу указанного индекса за указанную дату.

    Описание запроса - https://iss.moex.com/iss/reference/148

    Список индексов - https://iss.moex.com/iss/statistics/engines/stock/markets/index/analytics

    :param session:
        Сессия интернет соединения.
    :param index:
        Название индекса. Например, IMOEX.
    :param date:
        Дата вида ГГГГ-ММ-ДД. Если указано, то будут показаны только активные инструменты, по которым тогда рассчитывалось значение индекса. Если в указанный день не было торгов, то вернёт пустой список! При отсутствии данные будут загружены с начала истории.
    :param columns:
        Кортеж столбцов, которые нужно загрузить - по умолчанию режим торгов, дата торгов, цена закрытия и объем в
        штуках и стоимости. Если пустой или None, то загружаются все столбцы.
    :param market:
        Рынок - по умолчанию индексы.
    :param engine:
        Движок - по умолчанию акции.

    :return:
        Список словарей, которые напрямую конвертируется в pandas.DataFrame.
    """
    url = (
        f"https://iss.moex.com/iss/statistics/engines/{engine}/markets/{market}/"
        f"analytics/{index}/tickers.json"
    )
    table = "tickers"
    query = _make_query(date=date, table=table, columns=columns)
    return _get_short_data(session, url, table, query)


text:
var path = require( 'path' );
var webpack = require( 'webpack' );
var CompressionPlugin = require('compression-webpack-plugin');

var webpackProdConfig = {
    overrides: {
        entry: {
            app: [
                './src/app/app.js'
            ]
        }
    },

    plugins: [
        new webpack.DefinePlugin( {
            'process.env': {
                NODE_ENV: JSON.stringify( 'production' )
            }
        } ),
        new webpack.optimize.DedupePlugin(),
        new webpack.optimize.UglifyJsPlugin(),
        new CompressionPlugin({
            asset: '{file}.gz',
            algorithm: 'gzip',
            threshold: 10240,
            minRatio: 0.8
        })
    ],

    loaders: [
        {
            test: /\.jsx?$/,
            loaders: [ 'babel' ],
            include: path.join( __dirname, 'src', 'app' ),
            exclude: path.join( __dirname, 'node_modules' )
        }
    ]
};

module.exports = require( './webpack.config' )( webpackProdConfig );

repo_name: nazar/soapee-ui

repo_language: JavaScript


text:
# -*- mode: ruby -*-
# vi: set ft=ruby :

Vagrant.configure(2) do |config|
  config.vm.box = "bento/debian-7.8-i386"
  config.vm.box_version = "2.2.0"
  config.vm.network :forwarded_port, guest: 22, host: 3020, id: 'ssh'

  # 192.168.30.20 - UI server
  # 192.168.30.21 - api server
  config.vm.network "private_network", ip: "192.168.30.20"

  #RSYNC based one-way file sharing
  config.vm.synced_folder ".", "/home/vagrant/files",
    type: "rsync",
    rsync__exclude: [".git/", "node_modules", ".idea"],
    rsync__auto: false

  config.vm.provider "virtualbox" do |vb|
      # Customize the amount of memory on the VM:
      vb.memory = "384"
      vb.cpus = 1
  end

  # provision VM
  config.vm.provision "shell", path: "provision/base.sh", privileged: false

 if RUBY_PLATFORM =~ /mingw/
   config.vm.provision "shell", path: "provision/client.sh", privileged: false
 end

end


text:
var path = require( 'path' );
var webpack = require( 'webpack' );

var webpackDevConfig = {
    overrides: {
        devtool: 'eval',
        debug: true,
        entry: {
            app: [
                'webpack-dev-server/client?http://192.168.30.20:8080',
                'webpack/hot/only-dev-server',
                './src/app/app.js'
            ]
        }
    },

    plugins: [
        new webpack.DefinePlugin( {
            'process.env': {
                NODE_ENV: JSON.stringify( 'development' )
            }
        } )
    ],

    loaders: [
        {
            test: /\.jsx?$/,
            loaders: [ 'react-hot', 'babel' ],
            include: path.join( __dirname, 'src', 'app' ),
            exclude: path.join( __dirname, 'node_modules' )
        }
    ]
};

module.exports = require( './webpack.config' )( webpackDevConfig );


text:
var _ = require( 'lodash' );
var path = require( 'path' );
var webpack = require( 'webpack' );

var pathAppTo;

function pathTo() {
    return path.join( __dirname, 'src', path.join.apply( path, arguments ) );
}

pathAppTo = _.partial( pathTo, 'app' );

module.exports = function ( options ) {
    var config = _.merge( {}, {
        entry: {
            vendor: [
                'bootstrap/dist/js/bootstrap.min.js',
                'assets/css/theme.css',
                'assets/css/animate.css',
                'font-awesome/css/font-awesome.min.css',
                'react-dropzone-component/node_modules/dropzone/dist/dropzone.css',
                'assets/css/filepicker.css',
                'checkit',
                'classnames',
                'events',
                'jquery',
                'lib/animatedModal.js',
                'lib/growl.js',
                'bootstrap-markdown/js/bootstrap-markdown',
                'load-script',
                'lodash',
                'griddle-react',
                'marked',
                'moment',
                'react',
                'react/addons',
                'react-bootstrap',
                'react-cookie',
                'react-ga',
                'react-doc-meta',
                'react-dropzone-component',
                'react-pager',
                'react-portal',
                'react-router',
                'react-social',
                'react-textarea-autosize',
                'reflux',
                'url',
                'when'
            ]
        },

        output: {
            path: path.join( __dirname, 'bundle' ),
            filename: 'app.js',
            publicPath: '/bundle/'
        },
        plugins: [
            new webpack.HotModuleReplacementPlugin(),
            new webpack.NoErrorsPlugin(),
            new webpack.ProvidePlugin( {
                jQuery: 'jquery',
                $: 'jquery',
                'window.jQuery': 'jquery'
            } ),
            new webpack.ProvidePlugin( {
                React: 'react',
                'window.React': 'react'
            } ),
            new webpack.optimize.CommonsChunkPlugin( 'vendor', 'vendor.js' )
        ],
        resolve: {
            extensions: [ '', '.js', '.jsx' ],
            alias: {
                //application aliases
                actions: pathAppTo( 'actions' ),
                components: pathAppTo( 'components' ),
                lib: pathAppTo( 'lib' ),
                mixins: pathAppTo( 'mixins' ),
                modals: pathAppTo( 'modals' ),
                models: pathAppTo( 'models' ),
                resources: pathAppTo( 'resources' ),
                services: pathAppTo( 'services' ),
                stores: pathAppTo( 'stores' ),
                views: pathAppTo( 'views' ),
                utils: pathAppTo( 'utils' ),

                assets: pathTo( 'assets' ),
                config: pathAppTo( 'config.js' ),

                //vendor aliases
                jquery: 'jquery/dist/jquery.min.js'
            }
        },
        module: {
            loaders: [
                { test: /\.css$/, loader: 'style-loader!css-loader' },
                { test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: 'url-loader?limit=10000&mimetype=application/font-woff' },
                { test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: 'file-loader' },
                {
                    test: /\.jpg|\.png|\.mp3/,
                    loader: 'file-loader'
                }

            ]
        },
        resolveLoader: {
            root: path.join( __dirname, 'node_modules' )
        }
    }, options.overrides );

    config.module.loaders = _.union( config.module.loaders, options.loaders );
    config.plugins = _.union( config.plugins, options.plugins );

    return config;
};


text:
# About

The ReactJS based front end for [Soapee](http://soapee.com), a Soap Lye calculator.

# Development Environment Provisioning

Please read the [Provisioning](./doc/provision.md) section for setting up the development environment.

# Linting

An [.eslintrc](./eslintrc) is provided for [ESLint](http://eslint.org/) but the eslint packages are not listed in [package.json](./package.json) as
  the eslint modules are required for the client machine (and not the development server).

To install JSX compatible ESLint modules:

```
npm install -g eslint eslint-plugin-react
```


text:
var path = require( 'path' );
var gulp = require( 'gulp' );
var gutil = require( 'gulp-util' );
var webpack = require( 'webpack' );
var gulpWebpack = require( 'gulp-webpack' );
var WebpackDevServer = require( 'webpack-dev-server' );
var stylus = require( 'gulp-stylus' );
var clean = require( 'gulp-clean' );
var runSequence = require( 'run-sequence' );
var imagemin = require( 'gulp-imagemin' );
var shipitCaptain = require('shipit-captain');

function handleError( task ) {
    return function ( err ) {
        this.emit( 'end' );
        gutil.log( 'Error handler for', task, err.toString() );
    };
}

// The development server (the recommended option for development)
gulp.task( 'default', [ 'webpack-dev-server', 'stylus:compile' ] );

gulp.task( 'webpack-dev-server', function ( callback ) {
    var config = Object.create( require( './webpack.dev.js' ) );

    // Start a webpack-dev-server
    new WebpackDevServer( webpack( config ), {
        contentBase: path.join( __dirname, 'src' ),
        publicPath: config.output.publicPath,
        hot: true,
        historyApiFallback: true,
        stats: {
            colors: true
        }
    } ).listen( 8080, '0.0.0.0', function ( err ) {
            if ( err ) {
                throw new gutil.PluginError( 'webpack-dev-server', err );
            }
            gutil.log( '[webpack-dev-server]', 'http://192.168.30.20:8080' );
            callback();
        } );

    //setup stylus watcher
    gulp.watch( [ 'src/assets/stylus/*.styl', 'src/assets/stylus/**/*.styl' ], [ 'stylus:compile' ] );
} );

gulp.task( 'stylus:compile', function () {
    return gulp.src( './src/assets/stylus/main.styl' )
        .pipe( stylus().on( 'error', handleError( 'stylus:compile' ) ) )
        .pipe( gulp.dest( './src/assets' ) );
} );

gulp.task( 'clean:build', function () {
    return gulp.src( 'build/*', { read: false } )
        .pipe( clean() );
} );

gulp.task( 'build:image:min', function () {
    return gulp.src( './build/bundle/*.jpg' )
        .pipe( imagemin( {
            progressive: true,
            svgoPlugins: [ { removeViewBox: false } ]
        } ) )
        .pipe( gulp.dest( 'build/bundle' ) );
} );

gulp.task( 'build:cp:index', function () {
    return gulp.src( [
        './src/index.html',
        './src/favicon.png',
        './src/assets/img/logo.jpg'
    ] )
        .pipe( gulp.dest( 'build/' ) );
} );

gulp.task( 'build:webpack', function () {
    return gulp.src( 'src/app/app.js' )
        .pipe( gulpWebpack( require( './webpack.prod.js' ), webpack ) )
        .pipe( gulp.dest( 'build/bundle/' ) );
} );

gulp.task( 'build', function ( cb ) {
    runSequence(
        'clean:build',
        [ 'stylus:compile', 'build:cp:index' ],
        'build:webpack',
        'build:image:min',
        cb
    );
} );

gulp.task( 'deploy', [ 'build' ], function ( cb ) {
    var options = {
        init: require( './deploy/shipit' ).init,
        run: 'deploy-local',
        targetEnv: 'production',
        confirm: false
    };

    shipitCaptain( require( './deploy/shipit' ).config, options, cb  );
} );


text:
<!doctype html>

    <meta charset="utf-8">
    <title>Soapee Lye Calculator</title>
    <meta name="description" content="Soapee is a Soap Lye Calculator and a Soap Recipe Database">
    <meta name="viewport" content="width=device-width">
    <meta name="google-site-verification" content="awwz5nNBHKNEN-NMvBsQp2oPOSUHqJ86MeBvRXnM2oQ" />
    <meta property="og:image" content="http://soapee.com/logo.jpg" />
    <link rel='shortcut icon' type='image/png' href='/favicon.png' />


text:
media-common-styles() {
    p {
        margin-bottom: 5px;

        &:last-child {
            margin-bottom: 0;
        }
    }

    .about {
        font-size: 12px;
        height: 18px;
    }

    span.time, span.link {
        margin-left: 5px;
        border-bottom: 1px dotted silver;
    }
}


text:
fast-transition-speed = 0.2s;
very-fast-transition-speed = 0.1s;

.fade {

    &-enter {
        opacity: 0.01;
        transition: opacity fast-transition-speed ease-in;

        &-active {
            opacity: 1;
        }
    }

    &-leave {
        opacity: 1;
        transition: opacity very-fast-transition-speed ease-out;

        &-active {
            opacity: 0.01;
        }
    }

}

.zoom {

    &-enter {
        opacity: 0.01;
        transform: scale3d(.3, .3, .3);
        transition: all 0.1s ease-in;

        &-active {
            opacity: 1;
            transform: scale3d(1, 1, 1);
        }

    }

    &-leave {
        opacity: 1;
        transition: all 0.1s ease-in;

        &-active {
            opacity: 0.01;
            transform: scale3d(.3, .3, .3);
        }
    }

}


text:
// for sticky footer
html {
    position: relative;
    min-height: 100%;
}

body {
    margin-bottom: footer-height;
}

//paper theme overrides
.panel-info > .panel-heading {
    background-color: #3434F5;
}

.panel-success > .panel-heading {
    background-color: #00BD6C;
}

.alert {
    padding: 2px 10px;
    margin: 0 15px 15px 15px;
}

.alert-warning {
    background-color: #f28700;
}

.panel-heading {
    .btn-toolbar {
        margin-top: -2px;
    }
}

.alert {
    padding 8px 10px;
}

.label {
    font-size: 90%;
}

.navbar-brand {
    font-size: 30px;
}

.alert-dismissible {
    font-weight: bolder;

    .close {
        right: 0;
    }
}

.tab-pane {
    padding-top: 15px;
}

.popover {
    max-width: 100%;
}

.carousel {
    .carousel-inner {
        .item {
            img {
                margin: auto;
            }
        }
    }

}


text:
.strike {
    position: relative;

    border-bottom: 1px #ddd solid;
    height: 20px;

    margin-bottom: 20px;

    .or {
        position: absolute;
        left: 48%;
        top: 7px;

        padding: 0 3px;

        background-color: #f9f9f9;
    }

}

.btn-numbered-page, .btn-next-page, .btn-prev-page, .btn-first-page, .btn-last-page {
    a {
        cursor: pointer;
    }
}


text:
@import 'variables';
@import 'mixins';

//views
@import 'views/application';
@import 'views/calculator';
@import 'views/forgot';
@import 'views/main-landing';
@import 'views/my-status-updates';
@import 'views/oil';
@import 'views/profile';
@import 'views/recipe';
@import 'views/recipes';
@import 'views/recipe-journal-edit';
@import 'views/status-update';
@import 'views/status-update-edit';
@import 'views/user-profile';

//modals
@import 'modals/select-oils';
@import 'modals/signup-or-login-to-save-changes';

//components
@import 'components/add-comment';
@import 'components/animated-modal';
@import 'components/bootstrap-modal';
@import 'components/button-user-notifications';
@import 'components/comment';
@import 'components/facebook-comments';
@import 'components/feed-item';
@import 'components/footer';
@import 'components/form-save-recipe';
@import 'components/google-comments';
@import 'components/grid-oil';
@import 'components/imageable';
@import 'components/imageable-edit';
@import 'components/imageable-thumbnails';
@import 'components/list-oils-recipe';
@import 'components/list-oils-selector';
@import 'components/markdown-editor';
@import 'components/media-signin-buttons';
@import 'components/navbar-component';
@import 'components/properties-oil';
@import 'components/recipe-fatty-acids';
@import 'components/recipe-journal-add';
@import 'components/recipe-journal-item';
@import 'components/recipe-list-item';
@import 'components/recipe-print-area';
@import 'components/sap-calculator';
@import 'components/share-this';
@import 'components/spinner';
@import 'components/text-editor';
@import 'components/tooltip-question';
@import 'components/user-avatar';
@import 'components/user-notifications';

// application wide styles and overrides
@import 'global';
@import 'overrides';
@import 'transitions';


text:
.spinner {
    text-align: center;

    .fa {
        font-size: 30px;
    }
}


text:
.list-oils-selector {
    .oils-container {
        overflow-y: auto;

        td {
            &:hover {
                cursor: pointer;
                background-color: #ebebeb;
            }

            &.selected {
                background-color: #d5edf2;
            }
        }
    }
}


text:
.imageable-thumbnails {
    margin-top: 10px;

    img.thumbnail {
        margin-left: 10px;
    }
}


text:
.user-avatar {

    .avatar-border {
        display: inline-block;

        padding: 3px;

        border: 1px solid silver;
        border-radius: 3px;

        img {
            width: 30px;
            height: 30px;

            border-radius: 3px;
        }
    }

    .fa-user {
        padding: 0 5px 0 5px;
        font-size: 30px;
    }

}


text:
.facebook-comments {

    .fb_iframe_widget,
    .fb_iframe_widget span,
    .fb_iframe_widget span iframe[style] {
        min-width: 100% !important;
        width: 100% !important;
    }

}


text:
.media-signin-buttons {

    .btn-signup {
        margin-top: 15px;
    }

    .action-button {
        margin-top: 15px;
        text-align: center;
    }

    .link-signup {
        padding-top: 10px;
        padding-bottom: 10px;
        padding-left: 10px;

        width: 250px;

        .action {
            font-size: 16px;
        }

        .fa {
            font-size: 20px;
            padding-right: 15px;
        }
    }
}


text:
.recipe-list-item {

    padding-bottom: 15px;

    .user-avatar {
        float: right;
    }

    .recipe-name {
        font-size: 20px;
    }

    .iodine {
        padding-right: 10px;
    }

    .properties, .fats, .oils {

        li {
            float: left;
            padding-right: 10px;
        }
    }

}


text:
.footer {
    position: absolute;
    bottom: 0;

    padding-top: 5px;
    width: 100%;
    height: footer-height;

    color: #AAD8FF;
    background-color: #2925FF; //##58A6E6 //#4EE9AB //#6966EA //#55ffdc
    border-color: transparent;

    box-shadow: 0px -1px 2px rgba(0, 0, 0, 0.3);

    a {
        color: #FFC000;
    }

    .content {
        font-size: 15px;
    }

    .share-this {
        padding-top: 10px;

        [id^=___plusone] {
            width:72px !important;
        }
    }
}


text:
.navbar-component {

    .btn-profile {
        padding-top: 6px;
        padding-bottom: 6px;
        padding-left: 6px;
        padding-right: 6px;

        color: white;
        background-color: #58A6E6;
    }

    .caption {
        padding-right: 5px;

        .user-avatar {
            height: 25px;
            border-radius: 3px;
        }

    }

}


text:
.markdown-editor {

    textarea {
        padding: 5px;
        line-height: 22px;

        border: 1px solid #e5e5e5;
        border-radius: 3px;
    }

    .md-header {
        padding-bottom: 4px;
    }

    .md-preview {
        height: 100% !important;
        font-size: 16px;
    }

}


text:
.text-editor {

    .btn-toolbar {
        padding-bottom: 3px;
    }

    .editor {
        border: 1px solid #e5e5e5;
        border-radius: 3px;

        padding: 5px;
    }
}


text:
.grid-oil {

    .griddle {
        th {
            cursor: pointer;
        }

        .top-section {
            clear: both;
            display: table;
            width: 100%;
            padding-bottom: 3px;

            .filter-container {
                input {
                    height: 30px;
                }
            }
        }

        .griddle-filter {
            float: left;
            width: 50%;
            text-align: left;
            color: #222;
            min-height: 1px;
        }

        .griddle-settings-toggle {
            float: left;
            width: 50%;
            text-align: right;
            padding-right: 2px;
        }

        .griddle-settings {
            background-color: #FFF;
            border: 1px solid #DDD;
            color: #222;
            padding: 10px;
            margin-bottom: 10px;
        }

        .griddle-settings .griddle-columns {
            clear: both;
            display: table;
            width: 100%;
            border-bottom: 1px solid #EDEDED;
            margin-bottom: 10px;
        }

        .griddle-settings .griddle-column-selection {
            float: left;
            width: 20%;
        }

        .footer-container {
            padding: 0;
            border: 0;
            background-color: #EDEDED;
            color: #222;
        }

        .griddle-previous, .griddle-page, .griddle-next {
            float: left;
            width: 33%;
            min-height: 1px;
            margin-top: 5px;
        }

        .griddle-page {
            text-align: center;
        }

        .griddle-next {
            text-align: right;
        }

        .pagination {
            margin: 15px 0 15px 0;
        }
    }

    .toolbar {
        margin-bottom: 10px;
        padding-right: 2px;

        button {
            margin-left: 5px;
            .fa {
                padding-right: 4px;
            }
        }
    }

}


text:
.list-oils-recipe {

    td.value {
        min-width: 83px;

        .short-numeric {
            width: 50px !important;
        }
    }

    .unit {
        color: #aeaeae;
        padding-left: 5px;
    }
}


text:
.animated-modal {

    .close-modal {
        cursor: pointer;
        text-align: center;

        font-size: 50px;
    }

    .animated-modal-content {
        padding: 20px;
    }

    .fa {
        opacity: 0.6;
        transition: opacity 0.1s;

        &:hover {
            opacity: 1.0;
        }
    }

}


text:
.comment {

    margin-top: 10px;
    margin-bottom: 10px;

    padding: 3px;
    border: 1px solid silver + 50%;
    border-radius: 3px;

    box-shadow: 0 1px 4px rgba(0,0,0,0.1);

    media-common-styles();

}


text:
.add-comment {

    textarea {
        width: 50%;
    }

    .alert {
        padding: 2px 3px;
        margin: 0 0 15px 0;
        width: 50%;
    }

}


text:
.recipe-journal-add {

    .markdown-editor {
        textarea {
            width: 50%;
        }
    }

    .dropzone {
        margin-bottom: 20px;
    }

}


text:
.imageable {

    .dz-image-preview {
        background: transparent !important;
    }

}


text:
.recipe-fatty-acids
    .property-cell
        position relative

        .tooltip-question
            position absolute
            top 0
            left 75px

            @media screen and (min-width: 768px) and (max-width: 992px)
                left 65px


text:
.tooltip-question {
    color: #2196f3 - 30%;
    margin-left: 5px;

    font-size: 16px;
}


text:
.sap-calculator {

    .list-oils-selector {
        .oils-container {
            height: 250px;
            max-height: 250px;
        }
    }

    .properties-oil, .recipe-oils-container {
        max-height: 285px;
        overflow-y: auto;
    }

    .recipe-oils {
        input {
            height: 25px;
        }
    }

    .panel-heading {
        .openByClickOn {
            float: right;
        }
    }

    #mixed-fields {
        .left-spacer {
            padding-left: 20px;
        }

        .alert {
            margin-top: 20px;
            padding: 4px 5px;
        }
    }

    .weights-water {
        padding-top: 5px;
    }

    .superfat-after {
        padding-top: 5px;
    }

}


text:
.recipe-journal-item {

    margin-top: 10px;
    margin-bottom: 10px;

    padding: 3px;
    border: 1px solid silver + 50%;
    border-radius: 3px;

    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);

    media-common-styles();

    .actions {
        height: 23px;
    }

    &.view {
        border: none;
        box-shadow: none;
    }
}


text:
.recipe-print-area {

    position: absolute;

    top: 0;
    bottom: 0;
    left: 0;
    right: 0;

    background-color: white;
    color: black !important;

    z-index: 5432;

    font-size: 16px;

    table {
        margin-bottom: 0;

        td {
            padding: 2px !important;
            margin: 0 !important;
        }
    }

    .recipe-breakdown, .recipe-totals, .summary {
        padding: 5px;
        border: 1px solid #e0e0e0;
        border-radius: 5px;
    }

    .notes {
        padding: 10px;
        border: 1px solid e0e0e0;
        border-radius: 5px;

        p {
            margin-bottom: 0.1em;
        }
    }
}


text:
.button-user-notifications {

    height: 38px;
    margin-right: 5px;

    .badge {
        top: -2;
        padding: 4px 6px 4px 6px;
        margin-left: 3px;
        color: #2196f3;
        background-color: white;

        font-weight: bold;
    }

    .fa {
        font-size: 18px;
        line-height: 24px;
    }

    .fa.active {
        color: #ff9800;

    }

}


text:
.feed-item {

    margin-top: 10px;
    margin-bottom: 10px;

    padding: 3px;
    border: 1px solid silver + 50%;
    border-radius: 3px;

    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);

    media-common-styles();

    .thumbnail {
        margin-bottom: 10px;
        margin-left: 0 !important;
        margin-right: 10px;
    }

}


text:
.form-save-recipe {

    .input-description {
        padding: 4px;
        min-height: 38px;
        width: 100%;
    }

    .action-buttons {
        margin-top: 20px;
    }

    .editor {
        p {
            margin-bottom: 3px;
        }
    }

}


text:
.properties-oil {

    .title {
        font-style:italic;
    }

}


text:
.share-this {
    display: table;

    .t-cell {
        padding-left: 10px;
        vertical-align: top;

        .btn-pinterest {
            background-color: #bd081c;
        }

        .btn-reddit {
            background-color: #ff5700;
        }

    }
}


text:
.imageable-edit {

    .thumbnail {
        &.deleting {
            border-color: red;
        }

        img {
            margin-top: 8px;
            border-radius: 5px;
        }

        .caption {
            text-align: center;

            p {
                margin-bottom: 0;
            }
        }
    }

}


text:
.google-comments {

    width: 100% !important;

    & > iframe {
        width: 100% !important;
    }

}


text:
.bootstrap-modal {

    .modal {
        z-index: 10000;
    }

}


text:
.user-notifications {

    width: 260px;

    .user-notification {
        padding: 6px;

        padding-top: 10px;
        padding-bottom: 10px;

        border-bottom: 1px solid #f1f1f1;

        &:last-child {
            border-bottom: none;
        }

        &.unread {
            background-color: #58A6E6 + 95%;
        }

        .actions {
            padding-top: 5px;

            .btn {
                padding-top: 0;
                padding-bottom: 0;

                .fa {
                    margin-right: 10px;
                }
            }
        }

        .user-avatar {
            float: left;
            padding-right: 5px;
        }

    }

}


text:
.signup-or-login-to-save-changes {

    .alert {
        margin: 0;
        margin-bottom: 30px;
    }

    .strike {
        .or {
            background-color: white;
        }
    }

}


text:
.select-oils {

    td.selected {
        padding-left: 6px;
        padding-right: 0;
    }
}


text:
#status-update-edit {

    .markdown-editor {
        textarea {
            width: 50%;
        }
    }

    .dropzone {
        margin-bottom: 20px;
    }
}


text:
#profile {

    .input-description {
        width: 100%;
        min-height: 40px;

        padding: 5px;

        background-color: transparent;
    }

    .username {
        padding-bottom: 20px;

        legend {
            margin-bottom: 10px;
        }
    }

    .friend-profile-link {
        display: inline-block;
        float: left;
        margin-right: 5px;
    }

}


text:
#calculator {

}


text:
h1-font-size = 30px;

#user-profile {

    .profile-avatar {
        .user-avatar {
            float: right;
            padding-right: 20px;
            padding-top: 20px;
        }

    }

    .jumbotron.compact {
        padding-top: 10px;
        padding-left: 20px;

        h1 {
            font-size: h1-font-size;
        }

        h2 {
            font-size: h1-font-size - 6px;
        }

        h3, h4, h5, h6 {
            font-size: h1-font-size - 10px;
        }
    }

    .friends {
        .button-add-friend {
            float: left;
            margin-right: 5px;
        }
        .friend-profile-link {
            display: inline-block;
            float: left;
            margin-right: 5px;
        }
    }

    .user-recipes {
        padding-top: 20px;
    }

}


text:
#status-update {

    .status-update-content {
        padding: 10px;
        margin-top: 10px;
        margin-bottom: 20px;
        border: 1px solid #eeeeee;
        border-radius: 3px;
    }

    .carousel {
        margin-bottom: 30px;
    }

}


text:
header-height = 800px;

#main-landing {

    .header-container {
        height: header-height;

        .header {
            position: absolute;

            top: 64px;
            left: 0;
            right: 0;

            width: 100%;
            height: header-height;

            display: table;

            -webkit-background-size: cover;
            -moz-background-size: cover;
            background-size: cover;

            .text-vertical-center {
                display: table-cell;
                text-align: center;
                vertical-align: middle;

                h1 {
                    font-size: 120px;
                }

                h1, h2 {
                    color: white;

                    text-shadow: -1px -1px 0 #000,
                    1px -1px 0 #000,
                    -1px 1px 0 #000,
                    1px 1px 0 #000;
                }
            }
        }
    }

    .start-item {
        text-align: center;
        padding-bottom: 50px;
    }

    #start {
        margin-top: 50px;
        margin-bottom: 50px;

        .fa {
            color: #77BEF5 + 30%;
        }

        .fa-circle {
            color: #098BEF;
        }
    }

}


text:
#forgot {

    label {
        font-weight: bold;
        margin-right: 5px;
    }

    .spacer {
        margin-top 15px;
    }

}


text:
#recipe-journal-edit {

    .markdown-editor {
        textarea {
            width: 50%;
        }
    }

    .dropzone {
        margin-bottom: 20px;
    }

}


text:
#recipe {

    h1 {
        font-size: 30px;
    }

    .description {
        font-size: 16px;
        line-height: 24px;

        padding-bottom: 2px;
        margin-bottom: 20px;

        border-bottom: 1px solid #e5e5e5;

        .marked-display {
            p {
                margin-bottom: 10px;

                &:last-child {
                    margin-bottom: 5px;
                }
            }
        }
    }

    .openByClickOn {
        display: inline-block;
        margin-left: 5px;
    }

    #notes {
        p {
            margin-bottom: 3px;
        }
    }

    .fb-like {
        margin-bottom: 20px;
    }

    .private {
        padding-top: 10px;

        .fa-lock {
            color: #FF9200;
            font-size: 40px;
        }
    }

    .private-friends {
        padding-top: 10px;

        .fa-lock {
            color: #00C274;
            font-size: 40px;
        }
    }

    .carousel {
        margin-bottom: 30px;
    }

}


text:
#application {

    button.btn-xxs {
        padding: 1px 5px;
        font-size: 10px;
    }

    input.short-numeric {
        height: 25px;
        width: 60px;
        text-align: center;
    }

    input.compact-numeric {
        height: 25px;
        width: 45px;
        text-align: center;
    }

    input.super-short-numeric {
        height: 25px;
        width: 30px;
        text-align: center;
    }

    table.table-super-condensed {
        td {
            padding-top: 2px;
            padding-bottom: 2px;
        }
    }

    .t-table {
        display: table;
    }

    .t-cell {
        display: table-cell;
    }

    .t-row {
        display: table-row;
    }

    .truncate {
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
    }

    .no-select {
        -webkit-user-select: none;
        -moz-user-select: none;
        -khtml-user-select: none;
        -ms-user-select: none;
    }

    #application-view > .container {
        padding-bottom: 20px;
    }

}


text:
#my-status-updates {

    .markdown-editor {
        textarea {
            width: 50%;
        }
    }

    .add-status-update {
        padding-bottom: 40px;
    }

    .status-update {
        margin-top: 10px;
        margin-bottom: 10px;

        padding: 3px;
        border: 1px solid silver + 50%;
        border-radius: 3px;

        box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);

        p {
            margin-bottom: 5px;

            &:last-child {
                margin-bottom: 0;
            }
        }
    }

    .actions {
        span {
            margin-left: 10px;
        }
    }

    .dropzone {
        margin-bottom: 20px;
    }

}


text:
#recipes {

    h1 {
        font-size: 30px;
    }

    .marked-display {
        p {
            line-height: 17px;
            margin-bottom: 6px;

            &:last-child {
                margin-bottom: 0;
            }
        }
    }
}


text:
#oil {

    h1 {
        font-size: 30px;
    }

    .fb-like {
        margin-bottom: 20px;
    }

}


text:
/* Filepicker CSS */
.filepicker {
    font-family: sans-serif;
}

.filepicker {
    text-align: center;
    padding: 5px;
    background-color: #E1E1E1;
    border-radius: 5px;
    min-height: 60px;
    border: 2px dashed #C7C7C7;
}

/* Icon */
.filepicker-file-icon
{
    position: relative;

    display: inline-block;

    margin: 1.5em 0 2.5em 0;
    padding-left: 45px;

    color: black;
}
.filepicker-file-icon::before
{
    position: absolute;
    top: -7px;
    left: 0;

    width: 29px;
    height: 34px;

    content: '';

    border: solid 2px #7F7F7F;
    border-radius: 2px;
}
.filepicker-file-icon::after
{
    font-size: 11px;
    line-height: 1.3;

    position: absolute;
    top: 9px;
    left: -4px;

    padding: 0 2px;

    content: 'file';
    content: attr(data-filetype);
    text-align: right;
    letter-spacing: 1px;
    text-transform: uppercase;

    color: #fff;
    background-color: #000;
}
.filepicker-file-icon .fileCorner
{
    position: absolute;
    top: -7px;
    left: 22px;

    width: 0;
    height: 0;

    border-width: 11px 0 0 11px;
    border-style: solid;
    border-color: white transparent transparent #920035;
}


text:
/*global FB*/

//load CSS assets first
require( '../assets/main.css' );

import _ from 'lodash';
import React from 'react';
import Reflux from 'reflux';
import Router from 'react-router';
import ga from 'react-ga';

import config from 'config';
import routes from './routes';

///////////////////
/// INITIALISE

let analytics = _.get( config, 'analytics.google' );

if ( analytics ) {
    ga.initialize( analytics );
}

FB.init( {
    appId: config.auth.facebookClientId,
    cookie: true,
    version: 'v2.10',
    xfbml: true
} );

Reflux.setPromiseFactory( require( 'when' ).promise );

Router.run( routes, Router.HistoryLocation, function ( Handler, state ) {
    if ( analytics ) {
        ga.pageview( state.pathname );
    }

    React.render( <Handler/>, document.getElementById( 'application' ) );
} );


text:
import _ from 'lodash';

let env = process.env;

let config = {

    general: {},

    development: {
        homeUrl: 'http://dev.soapee.com/',
        baseUrl: 'http://192.168.30.21:3000/api/',
        imageUrl: 'http://192.168.30.21:4000/imageables/',
        auth: {
            facebookClientId: '696173290526521',
            googleClientId: '161187113696-q5edt0566uiu0pbjvc87653utl678tdm.apps.googleusercontent.com'
        }
    },

    production: {
        homeUrl: 'http://soapee.com/',
        baseUrl: 'http://soapee.com/api/',
        imageUrl: 'http://soapee.com/imageables/',
        auth: {
            facebookClientId: '696173107193206',
            googleClientId: '454314644896-jj8kjlbq9edd1k0e5hbrvfo3jq73succ.apps.googleusercontent.com'
        },
        analytics: {
            google: 'UA-55368874-3'
        }
    }
};

export default _.extend( {}, config.general, config[ env.NODE_ENV ] );


text:
import React from 'react';
import {Route, DefaultRoute} from 'react-router';

import Login from 'views/login';
import Signup from 'views/signup';
import Forgot from 'views/forgot';

import Account from 'views/account';
import Application from 'views/application';
import Calculator from 'views/calculator';
import Logout from 'views/logout';
import Feed from 'views/feed';
import MainLanding from 'views/mainLanding';
import Oil from 'views/oil';
import Oils from 'views/oils';
import PrintCalculation from 'views/printCalculation';
import Recipe from 'views/recipe';
import RecipeEdit from 'views/recipeEdit';
import RecipePrint from 'views/recipePrint';
import Recipes from 'views/recipes';
import RecipeJournal from 'views/recipeJournal';
import RecipeJournalEdit from 'views/recipeJournalEdit';
import MyFriendsRecipes from 'views/myFriendsRecipes';
import MyStatusUpdates from 'views/myStatusUpdates';
import Resources from 'views/resources';
import StatusUpdate from 'views/statusUpdate';
import StatusUpdateEdit from 'views/statusUpdateEdit';
import UserProfile from 'views/userProfile';

import MyProfile from 'views/myProfile';
import MyComments from 'views/myComments';
import MyRecipes from 'views/myRecipes';
import SavedRecipes from 'views/savedRecipes';

let routes = (
    <Route name="home" path="/" handler={Application}>

        <DefaultRoute handler={MainLanding} />

        <Route name="login" handler={Login} />
        <Route name="signup" handler={Signup} />
        <Route name="logout" handler={Logout} />
        <Route name="forgot" handler={Forgot} />

        <Route name="calculator" handler={Calculator} />

        <Route name="feed" handler={Feed} />
        <Route name="status-update" path="status-update/:id"  handler={StatusUpdate} />
        <Route name="status-update-edit" path="status-update/:id/edit"  handler={StatusUpdateEdit} />

        <Route name="recipes" handler={Recipes} />
        <Route name="recipe" path="recipes/:id" handler={Recipe} />
        <Route name="editRecipe" path="recipes/:id/edit" handler={RecipeEdit} />
        <Route name="printRecipe" path="recipes/:id/print" handler={RecipePrint} />

        <Route name="recipe-journal" path="recipes/:recipeId/journals/:journalId" handler={RecipeJournal} />
        <Route name="recipe-journal-edit" path="recipes/:recipeId/journals/:journalId/edit" handler={RecipeJournalEdit} />

        <Route name="oils" handler={Oils} />
        <Route name="oil" path="oils/:id" handler={Oil} />

        <Route name="print" handler={PrintCalculation} />

        <Route name="resources" handler={Resources} />

        <Route name="userProfile" path="users/:id" handler={UserProfile} />

        <Route name="account" path="/my" handler={Account}>
            <Route name="profile" handler={MyProfile} />
            <Route name="my-recipes" handler={MyRecipes} />
            <Route name="my-friend-recipes" handler={MyFriendsRecipes} />
            <Route name="saved-recipes" handler={SavedRecipes} />
            <Route name="my-comments" handler={MyComments} />
            <Route name="my-status-updates" handler={MyStatusUpdates} />
        </Route>

    </Route>
);

export default routes;


text:
import Reflux from 'reflux';

import {
    getFeed
} from 'resources/feed';

let actions = Reflux.createActions( {
    getFeed: { asyncResult: true }
} );

export default actions;

actions.getFeed.listenAndPromise( getFeed );


text:
import Reflux from 'reflux';

import {
    getMyProfile,
    updateMyProfile,

    getMyRecipes,
    getMyFavouriteRecipes,

    getComments,

    addRecipeToFavourites,
    removeRecipeFromFavourites,

    getMyUserNotifications,
    dismissNotification,

    getMyFriends,
    getMyFriendsRecipes,
    addFriend,

    getMyStatusUpdates
} from 'resources/me';

let actions = Reflux.createActions( {
    getMyProfile: { asyncResult: true },
    getMyRecipes: { asyncResult: true },
    getComments: { asyncResult: true },
    getMyFavouriteRecipes: { asyncResult: true },

    updateMyProfile: { asyncResult: true },
    addRecipeToFavourites: { asyncResult: true },
    removeRecipeFromFavourites: { asyncResult: true },

    getMyUserNotifications: { asyncResult: true },
    dismissNotification: { asyncResult: true },

    getMyFriends: { asyncResult: true },
    getMyFriendsRecipes: { asyncResult: true },
    addFriend: { asyncResult: true },

    getMyStatusUpdates: { asyncResult: true }
} );

export default actions;

actions.getMyProfile.listenAndPromise( getMyProfile );
actions.getMyRecipes.listenAndPromise( getMyRecipes );
actions.getComments.listenAndPromise( getComments );
actions.getMyFavouriteRecipes.listenAndPromise( getMyFavouriteRecipes );

actions.updateMyProfile.listenAndPromise( updateMyProfile );
actions.addRecipeToFavourites.listenAndPromise( addRecipeToFavourites );
actions.removeRecipeFromFavourites.listenAndPromise( removeRecipeFromFavourites );

actions.dismissNotification.listenAndPromise( dismissNotification );
actions.getMyUserNotifications.listenAndPromise( getMyUserNotifications );

actions.getMyFriends.listenAndPromise( getMyFriends );
actions.getMyFriendsRecipes.listenAndPromise( getMyFriendsRecipes );
actions.addFriend.listenAndPromise( addFriend );

actions.getMyStatusUpdates.listenAndPromise( getMyStatusUpdates );


text:
import Reflux from 'reflux';

import {
    getStatusUpdate,
    addStatusUpdate,
    updateStatusUpdate,
    deleteStatusUpdate,

    getStatusUpdateComments,
    addComment
} from 'resources/statusUpdates';

let actions = Reflux.createActions( {
    getStatusUpdate: { asyncResult: true },
    addStatusUpdate: { asyncResult: true },
    updateStatusUpdate: { asyncResult: true },
    deleteStatusUpdate: { asyncResult: true },

    getStatusUpdateComments: { asyncResult: true },
    addComment: { asyncResult: true }

} );

export default actions;

actions.getStatusUpdate.listenAndPromise( getStatusUpdate );
actions.addStatusUpdate.listenAndPromise( addStatusUpdate );
actions.updateStatusUpdate.listenAndPromise( updateStatusUpdate );
actions.deleteStatusUpdate.listenAndPromise( deleteStatusUpdate );

actions.getStatusUpdateComments.listenAndPromise( getStatusUpdateComments );
actions.addComment.listenAndPromise( addComment );


text:
import Reflux from 'reflux';

import {
    getProfile,
    getRecipes,
    getFriends
} from 'resources/users';

let actions = Reflux.createActions( {
    getProfile: { asyncResult: true },
    getRecipes: { asyncResult: true },
    getFriends: { asyncResult: true }
} );

export default actions;

actions.getProfile.listenAndPromise( getProfile );
actions.getRecipes.listenAndPromise( getRecipes );
actions.getFriends.listenAndPromise( getFriends );


text:
import Reflux from 'reflux';

import {
    requestPasswordReset,
    verifyCode,
    resetPassword
} from 'resources/resets';

let actions = Reflux.createActions( {
    //async action
    requestPasswordReset: { asyncResult: true },
    verifyCode: { asyncResult: true },
    resetPassword: { asyncResult: true }
} );

export default actions;

actions.requestPasswordReset.listenAndPromise( requestPasswordReset );
actions.verifyCode.listenAndPromise( verifyCode );
actions.resetPassword.listenAndPromise( resetPassword );


text:
import Reflux from 'reflux';

import {
    getRecipes,
    createRecipe,
    getRecipeById,
    updateRecipe,
    deleteRecipe,

    getRecipeComments,
    addCommentToRecipe,

    getRecipeJournal,
    getRecipeJournals,
    addRecipeJournal,
    updateRecipeJournal,
    deleteRecipeJournal,
    getRecipeJournalComments,
    addCommentToRecipeJournal
} from 'resources/recipes';

let actions = Reflux.createActions( {
    //ui actions
    resetRecipe: {},

    //async action
    getRecipes: { asyncResult: true },
    createRecipe: { asyncResult: true },
    updateRecipe: { asyncResult: true },
    deleteRecipe: { asyncResult: true },
    getRecipeById: { asyncResult: true },
    editRecipeById: { asyncResult: true },

    getRecipeComments: { asyncResult: true },
    addCommentToRecipe: { asyncResult: true },

    getRecipeJournal: { asyncResult: true },
    getRecipeJournals: { asyncResult: true },
    addRecipeJournal: { asyncResult: true },
    updateRecipeJournal: { asyncResult: true },
    deleteRecipeJournal: { asyncResult: true },

    getRecipeJournalComments: { asyncResult: true },
    addCommentToRecipeJournal: { asyncResult: true }
} );

export default actions;

actions.getRecipes.listenAndPromise( getRecipes );
actions.createRecipe.listenAndPromise( createRecipe );
actions.updateRecipe.listenAndPromise( updateRecipe );
actions.deleteRecipe.listenAndPromise( deleteRecipe );

actions.getRecipeById.listenAndPromise( getRecipeById );
actions.editRecipeById.listenAndPromise( getRecipeById );

actions.getRecipeComments.listenAndPromise( getRecipeComments );
actions.addCommentToRecipe.listenAndPromise( addCommentToRecipe );

actions.getRecipeJournal.listenAndPromise( getRecipeJournal );
actions.getRecipeJournals.listenAndPromise( getRecipeJournals );
actions.addRecipeJournal.listenAndPromise( addRecipeJournal );
actions.updateRecipeJournal.listenAndPromise( updateRecipeJournal );
actions.deleteRecipeJournal.listenAndPromise( deleteRecipeJournal );

actions.getRecipeJournalComments.listenAndPromise( getRecipeJournalComments );
actions.addCommentToRecipeJournal.listenAndPromise( addCommentToRecipeJournal );


text:
import Reflux from 'reflux';

import {
    getOilById,

    getOilComments,
    addCommentToOil
} from 'resources/oils';

let actions = Reflux.createActions( {
    getOilById: { asyncResult: true },

    getOilComments: { asyncResult: true },
    addCommentToOil: { asyncResult: true }
} );

export default actions;

actions.getOilById.listenAndPromise( getOilById );

actions.getOilComments.listenAndPromise( getOilComments );
actions.addCommentToOil.listenAndPromise( addCommentToOil );


text:
import Reflux from 'reflux';

import {
    signupOrLoginThirdParty,
    signupLocal,
    loginLocal,
    logout
} from 'resources/auths';

let actions = Reflux.createActions( {
    //async action
    signupOrLoginThirdParty: { asyncResult: true },
    signupLocal: { asyncResult: true },
    loginLocal: { asyncResult: true },
    logout: { asyncResult: true },

    //ui actions
    SignedUpToLoggedInToSaveRecipe: {}
} );

export default actions;

actions.signupOrLoginThirdParty.listenAndPromise( signupOrLoginThirdParty );
actions.signupLocal.listenAndPromise( signupLocal );
actions.loginLocal.listenAndPromise( loginLocal );
actions.logout.listenAndPromise( logout );


text:
import _ from 'lodash';

export default {

    radioState( key, checkedValue ) {
        return {
            value: this.state[ key ] === checkedValue,
            requestChange: checked => {
                let newState = {
                    [key]: checkedValue
                };

                if ( checked ) {
                    this.setState( newState );
                }
            }
        };
    },

    radioStore( store, key, checkedValue ) {
        return {
            value: store.getStoreValue( key ) === checkedValue,
            requestChange: checked => {
                if ( checked ) {
                    store.setStoreValue( key, checkedValue );
                }
            }
        };
    },

    radioModel( model, key, checkedValue ) {
        return {
            value: model.getModelValue( key ) === checkedValue,
            requestChange: checked => {
                if ( checked ) {
                    model.setModelValue( key, checkedValue );
                }
            }
        };
    },

    checkboxModel( model, key, checkedValue ) {
        return {
            value: model.getModelValue( key ) === checkedValue,
            requestChange: checked => {
                model.setModelValue( key, checked );
            }
        };
    },

    linkStore( store, key ) {
        return {
            value: store.getStoreValue( key ),
            requestChange: v => {
                store.setStoreValue( key, v );
            }
        };
    },

    linkComponentState( component, key ) {
        return {
            value: _.get( component.state, key ),
            requestChange: value => {
                _.set( component.state, key, value );
                component.setState( component.state );
            }
        };
    },

    linkModel( model, key, options = {} ) {
        let value;

        if ( options.round ) {
            //_.round will swallow any decimal points entered so ignore any xyz. numerics
            value = model.getModelValue( key );

            if ( value && !(value.toString().match(/\.$/)) ) {
                value = _.round( model.getModelValue( key ), options.round );
            }
        } else {
            value = model.getModelValue( key );
        }

        return {
            value,
            requestChange: v => {
                model.setModelValue( key, v );
            }
        };
    }

};