diff --git a/README.md b/README.md index f415b61..a0feeae 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,6 @@ This command will run Clusterix on [http://127.0.0.1:5000](http://127.0.0.1:5000 #### File input (CSV only currently) * Data Preview * Field selection -* Field Scaling #### Vectorizers * Count Vactorizer @@ -42,10 +41,9 @@ This command will run Clusterix on [http://127.0.0.1:5000](http://127.0.0.1:5000 #### Algorithms * K-Means -* Hierarchical Clustering (with various distance/linkage options) #### Plot Features -* Scatterplot/Treemap vizualizations +* Scatterplot vizualizations * Full text search for nodes * Brushing and zoom for targeted inspection * Various clustering metrics (TF-IDF, etc) @@ -57,7 +55,4 @@ This command will run Clusterix on [http://127.0.0.1:5000](http://127.0.0.1:5000 ![alt tag](http://i.imgur.com/AAudgAD.png) -![alt tag](http://i.imgur.com/DsDXYct.png) - - -### Wine Data +![alt tag](http://i.imgur.com/DsDXYct.png) \ No newline at end of file diff --git a/clusterix/__init__.py b/clusterix/__init__.py deleted file mode 100644 index d2357dd..0000000 --- a/clusterix/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from flask import Flask -from clusterix.controllers.blueprints import main - -app = Flask(__name__) -app.register_blueprint(main) diff --git a/clusterix/assets.py b/clusterix/assets.py deleted file mode 100644 index 72bf77a..0000000 --- a/clusterix/assets.py +++ /dev/null @@ -1,20 +0,0 @@ -from flask_assets import Bundle - - -common_css = Bundle( - 'css/vendor/bootstrap.min.css', - 'css/vendor/helper.css', - 'css/main.css', - filters='cssmin', - output='public/css/common.css' -) - -common_js = Bundle( - 'js/vendor/jquery.min.js', - 'js/vendor/bootstrap.min.js', - Bundle( - 'js/main.js', - filters='jsmin' - ), - output='public/js/common.js' -) \ No newline at end of file diff --git a/clusterix/clustering/__init__.py b/clusterix/clustering/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/clusterix/clustering/algorithms.py b/clusterix/clustering/algorithms.py deleted file mode 100644 index 1d69800..0000000 --- a/clusterix/clustering/algorithms.py +++ /dev/null @@ -1,78 +0,0 @@ -from beard.clustering import ScipyHierarchicalClustering -from sklearn.cluster import KMeans - -from ..clustering.results.plots import scatterplot -from ..clustering.utils import save_clusterer - - -def kmeans(X, attrs): - """ - K-Means clustering. - Return example: - {'k_num': 2, - 'nodes': [ - {'cluster': 0, 'isCentroid': False, 'id': 2, - 'x': 2.658062848683116e-16, 'y': 1.7320508075688774}, - {'cluster': 0, 'isCentroid': True, - 'x': 2.220446049250313e-16, 'y': 1.7320508075688774}, - {'cluster': 1, 'isCentroid': False, 'id': 1, - 'x': 1.5034503553765397, 'y': 0.0}, - ..............]} - """ - n_clusters = int(attrs['kNumber']) - kmeans = KMeans(n_clusters=n_clusters, n_jobs=-1) - kmeans.fit(X) - - labels = kmeans.labels_ - centroids = kmeans.cluster_centers_ - - # save_clusterer(kmeans) - return scatterplot(X, labels, n_clusters, centroids=centroids) - - -def hcluster(X, attrs): - """ - Hierarchical Clustering. - Return Example: - {'children': [ - {'children': [], 'name': 2, 'value': 150.0039243544126}, - {'children': [ - {'children': [], 'name': 1, 'value': 2.509279181210386}, - {'children': [ - {'children': [], 'name': 0, 'value': 2.4987419269136737}, - {'children': [], 'name': 3, 'value': 2.4987419269136737} - ], 'name': 4,'value': 4.997483853827347} - ], 'name': 5, 'value': 5.018558362420772} - ], 'name': 6, 'value': 300.0078487088252} - """ - n_clusters = int(attrs['kNumber']) - hcluster = ScipyHierarchicalClustering(method=attrs['distance'], - affinity=attrs['affinity'], - n_clusters=n_clusters) - - hcluster.fit(X) - labels = hcluster.labels_ - - # Z = hcluster.linkage_ - # return HClusterTree(Z).to_dict() - - # save_clusterer(hcluster) - return scatterplot(X, labels, n_clusters) - -# def block_clustering(X, original_items, data): -# # Cluster the data using the given block. -# clustering = ScipyHierarchicalClustering(method=data['distance'], -# affinity=data['affinity'], -# threshold=100.) -# bcluster = BlockClustering(base_estimator=clustering, -# blocking="precomputed", -# verbose=3, -# n_jobs=cpu_count() - 1) -# -# block_key = data['blockBy'] -# blocks = [item[block_key] for item in original_items] -# -# bcluster.fit(X, blocks=blocks) -# -# # Get the json needed for d3 visualization. -# return get_bcluster_json(blocks, bcluster, block_key, original_items) diff --git a/clusterix/clustering/cluster.py b/clusterix/clustering/cluster.py deleted file mode 100644 index 2f9c97e..0000000 --- a/clusterix/clustering/cluster.py +++ /dev/null @@ -1,34 +0,0 @@ -from ..database.db import processed_db -from ..clustering.utils import get_cluster_attrs, get_input_vector -from ..clustering.algorithms import kmeans, hcluster - -from ..log import log_info - - -def cluster_data(attrs): - """Retrieve items, cluster, and return the result dict.""" - fields, field_names, vec_name, algorithms = get_cluster_attrs(attrs) - - # Vectorize/transform/get the data array and do the transformation - # Then, get the records using only the specified fields (for clustering certain columns) - data = processed_db.get_records_with_fields(field_names) - X = get_input_vector(fields, vec_name, data) - - result = {} - for alg in algorithms: - if alg == 'kmeans': - result['kmeans'] = kmeans(X, attrs['algorithms']['kmeans']) - if alg == 'hcluster': - result['hcluster'] = hcluster(X, attrs['algorithms']['hcluster']) - - log_info('Clustering complete.') - return result - - -def predict_cluster(attrs): - # fields, field_names, vec_name, _ = get_cluster_attrs(attrs) - # X = get_input_vector(fields, field_names, vec_name) - # - # clusterer = load_clusterer() - # clusterer.predict(X) - pass diff --git a/clusterix/clustering/results/__init__.py b/clusterix/clustering/results/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/clusterix/clustering/results/bcluster.py b/clusterix/clustering/results/bcluster.py deleted file mode 100644 index 2253156..0000000 --- a/clusterix/clustering/results/bcluster.py +++ /dev/null @@ -1,81 +0,0 @@ -from scipy.cluster.hierarchy import to_tree - - -def get_bcluster_json(blocks, bcluster, block_by, items): - """ - Using the input, creates a D3 compatible dict, containing the hierarchical clustering results. - - :param blocks: The blocks that are used for the clustering. E.g. by country, by sex, by age, etc. - :type blocks: list - - :param bcluster: An instance of the clusterer (BlockClustering) with all the info (linkage, etc). - :type bcluster: BlockClustering - - :param block_by: The input key that is used for creating the different clustering blocks. - :type block_by: str - - :param items: A list containing dictionaries of all the items (with their metadata). - :type items: list - - :return: A D3 compatible dict. - :rtype: dict - """ - def _delete_keys_from_dict(dict_del): - """Delete node_id, as it is not needed, and use the node_id as name for branches.""" - if dict_del['children']: - dict_del['name'] = dict_del['node_id'] - for v in dict_del['children']: - if isinstance(v, dict): - _delete_keys_from_dict(v) - try: - del dict_del['node_id'] - except KeyError: - pass - return dict_del - - def _add_node(node, parent): - """Create the first node, and iterate the tree in order to create the dict.""" - new_node = {'children': [], 'value': node.dist if node.dist > 0 else 1, 'node_id': node.id} - parent['children'].append(new_node) - - if node.left: - _add_node(node.left, new_node) - if node.right: - _add_node(node.right, new_node) - - def _label_tree(node): - if node['children']: - map(_label_tree, node['children']) - node['name'] = node['node_id'] - else: - node['content'] = id2content[node['node_id']] - - def _process_block(): - """Initialize nested dictionary for d3, then recursively iterate through tree and create the dict.""" - tree = to_tree(linkage, rd=False) - - _add_node(tree, bcluster_dendro) - _label_tree(bcluster_dendro["children"][-1]) # get the last element - return bcluster_dendro - - # Create a root for the tree - # and start iterating through the blocks to get the subtrees. - bcluster_dendro = {'children': [], 'name': block_by, 'node_id': 'Root'} - single_children = [] - - for block in set(blocks): - content = [item for item in items if item[block_by] == block] - clusterer = bcluster.clusterers_[block] - - try: - id2content = dict(zip(range(len(content)), content)) - linkage = clusterer.linkage_ - - # Create a cluster from the current block and append it to the dendrogram - # else (on the exception) get the item and add it immediately - _process_block() - except AttributeError: - single_children.append({'content': content[0], 'value': 2, 'children': []}) # TODO: GET MEAN NUMBER OR SOMETHING HERE - - bcluster_dendro['children'] += single_children - return _delete_keys_from_dict(bcluster_dendro) diff --git a/clusterix/clustering/results/metrics.py b/clusterix/clustering/results/metrics.py deleted file mode 100644 index f778de0..0000000 --- a/clusterix/clustering/results/metrics.py +++ /dev/null @@ -1,29 +0,0 @@ -from operator import itemgetter -from ...database.db import vocab_db - -not_none = lambda item: item is not None -get_words = lambda ids: [vocab_db.get_vocab_by_id(id) for id in ids] -get_tfidf = lambda words: [vocab_db.tfidf(term) for term in words] - - -def tfidf_from_ids(clustered_ids): - """ - Return tfidf scores (by cluster), for the words that belong to the provided ids. E.g: - {0: [{'term': u'lubid', 'tfidf': 3.1364385032730837}, - {'term': u'gemma', 'tfidf': 3.1364385032730837}, - {'term': u'peeper', 'tfidf': 2.0909590021820557}, - ...]} - """ - result = {} - for cluster in clustered_ids.keys(): - total_words = set() - word_lists = get_words(clustered_ids[cluster]) - for w_list in word_lists: - total_words = total_words.union(w_list) - - # get the term-score pairs, sort them and return the first 10 - # check for None, as some values are missing - TODO: look into that - tfidfs = filter(not_none, get_tfidf(total_words)) - result[cluster] = sorted(tfidfs, key=itemgetter('tfidf'), reverse=True)[:10] - - return result \ No newline at end of file diff --git a/clusterix/clustering/transformers.py b/clusterix/clustering/transformers.py deleted file mode 100644 index 07804b2..0000000 --- a/clusterix/clustering/transformers.py +++ /dev/null @@ -1,67 +0,0 @@ -import numpy as np - -from sklearn.base import BaseEstimator, TransformerMixin -from sklearn.feature_extraction.text import HashingVectorizer, TfidfVectorizer, CountVectorizer - -TOKEN_PATTERN = r"\b\w+\b" # Keeps single letter attrs -vectorizers = { - 'hashing': HashingVectorizer(n_features=2 ** 12, non_negative=True, norm=None), - 'tfidf': TfidfVectorizer(max_features=2 ** 12, norm=None, token_pattern=TOKEN_PATTERN), - 'count': CountVectorizer(max_features=2 ** 12, token_pattern=TOKEN_PATTERN) -} - - -class ItemSelector(BaseEstimator, TransformerMixin): - """Selects items from a list of dicts, based on the key.""" - def __init__(self, key): - self.key = key - - def fit(self, X, y=None): - return self - - def transform(self, X, y=None): - return [item[self.key] for item in X] - - -class MissingValuesTransformer(BaseEstimator, TransformerMixin): - """Replace missing values according to the type""" - @staticmethod - def _empty_to_nan(i): - return 'NaN' if i == '' else i - - def __init__(self): - pass - - def fit(self, X, y=None): - return self - - def transform(self, X, y=None): - return [self._empty_to_nan(item) for item in X] - - -class Vectorizer(BaseEstimator, TransformerMixin): - """If the values of the list are numeric, it returns them as they are, else it uses a vectorizer.""" - def __init__(self, vec_name, field_type): - self.vectorizer = vectorizers[vec_name] - self.type = field_type - - def fit(self, X, y=None): - return self - - def transform(self, X, y=None): - if self.type == 'object': - return self.vectorizer.fit_transform(X) - else: - return np.array([[i] for i in X]) - - -class Scaler(BaseEstimator, TransformerMixin): - """Scales a list of features by the provided number.""" - def __init__(self, scale): - self.scale = int(scale) - - def fit(self, X, y=None): - return self - - def transform(self, X, y=None): - return self.scale * X diff --git a/clusterix/clustering/utils.py b/clusterix/clustering/utils.py deleted file mode 100644 index 9932bda..0000000 --- a/clusterix/clustering/utils.py +++ /dev/null @@ -1,73 +0,0 @@ -from sklearn.decomposition import TruncatedSVD -from sklearn.pipeline import make_pipeline, make_union -from sklearn.externals import joblib - -from ..clustering.transformers import ItemSelector, Scaler, Vectorizer -from ..database.db import processed_db -from ..config import CLUSTERER_PATH - -from ..log import log_info -"""General clustering functions.""" - - -def get_input_vector(fields, vec_name, data): - """Transform the input and create a 2D vector to cluster.""" - transformer = create_input_transformer(fields, vec_name) - pipeline = make_pipeline(transformer, TruncatedSVD()) - - log_info('Transformation pipeline complete.') - return pipeline.fit_transform(data) - - -def create_input_transformer(fields, vec_name): - """Create a pipeline of input transformations, allowing to use scaling of input fields.""" - pipeline = [] - for field in fields: - field_name = field['name'] - field_scale = field['scale'] - field_type = processed_db.get_field_type(field_name) - - pipeline.append( - make_pipeline(ItemSelector(field_name), # select the correct column - Vectorizer(vec_name, field_type), # vectorize (depending on str/numeric input) - Scaler(field_scale)) # scale column based on user input - ) - - return make_union(*pipeline) - - -def get_clustered_ids(data): - """ - Get a dict of id-cluster pairs and create a dict of cluster-list[ids]. E.g. - From: {u'1': 2, u'1006': 1, u'191': 1, u'2': 2, u'276': 1, u'3': 2, u'358': 1, u'6': 3} - To: {1: [1006, 191, 276, 358], 2: [1, 2, 3], 3: [6]} - """ - clustered_ids = {} - for key in data.keys(): - val = data[key] - try: - clustered_ids[val].append(int(key)) - except KeyError: - clustered_ids[val] = [int(key)] - - return clustered_ids - - -def get_cluster_attrs(attrs): - """Get some general attributes for clustering/predictions.""" - fields = attrs['csvType']['fieldsWithScaling'] - field_names = [field['name'] for field in fields] - vec_name = attrs['vectorizer'] - algorithms = attrs['algorithms']['algorithmsToUse'] - - return fields, field_names, vec_name, algorithms - - -def save_clusterer(clusterer): - """Pickle clusterer object.""" - joblib.dump(clusterer, open(CLUSTERER_PATH, "wb")) - - -def load_clusterer(): - """Load pickled clusterer object.""" - return joblib.load(open(CLUSTERER_PATH, "rb")) \ No newline at end of file diff --git a/clusterix/config.py b/clusterix/config.py deleted file mode 100644 index bef249e..0000000 --- a/clusterix/config.py +++ /dev/null @@ -1,7 +0,0 @@ -"""App configuration""" - -TEMP_PATH = 'temp_input/' -TEMP_RAW_INPUT = 'temp_input/index/' -TEMP_PROCESSED_INPUT = 'temp_input/processed_index/' -TEMP_VOCABULARY = 'temp_input/vocabulary_index/' -CLUSTERER_PATH = 'temp_input/clusterer.p' diff --git a/clusterix/controllers/__init__.py b/clusterix/controllers/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/clusterix/controllers/blueprints.py b/clusterix/controllers/blueprints.py deleted file mode 100644 index 13290d5..0000000 --- a/clusterix/controllers/blueprints.py +++ /dev/null @@ -1,71 +0,0 @@ -import json -from flask import Blueprint, render_template, request, jsonify - -from ..controllers.utils import get_attrs, save_file_if_exists, \ - replace_spaces_in_keys -from ..clustering.cluster import cluster_data -from ..clustering.utils import get_clustered_ids -from ..clustering.results.metrics import tfidf_from_ids -from ..database.db import vocab_db, processed_db - -main = Blueprint('main', __name__) - - -@main.route('/', methods=['POST', 'GET']) -def index(): - """Main page.""" - return render_template('index.html') - - -@main.route('/upload_and_cluster', methods=['POST']) -def upload_and_cluster(): - """ - Read data attributes and save file (if it exists), do the clustering - and return the correct results to the frontend. - """ - attrs = get_attrs(request) - - for attr in attrs: - print(attr) - if attr == "csvType": - print(attrs[attr]) - attrs[attr]["fieldsWithScaling"] = \ - replace_spaces_in_keys(attrs[attr]["fieldsWithScaling"]) - - save_file_if_exists(attrs) - - # Get the algorithm results - result = cluster_data(attrs) - return jsonify(**result) - - -@main.route('/tfidf', methods=['POST']) -def tfidf(): - """Get a list of id-cluster pairs, and return clustered term-tfidf pairs.""" - clustered_ids = get_clustered_ids( - json.loads(request.form.get('ids')) - ) - - result = tfidf_from_ids(clustered_ids) - return jsonify(**{'tfidf_metrics': result}) - - -@main.route('/search', methods=['POST']) -def search(): - """Search a term and return the ids of the corresponding records.""" - result = vocab_db.search_for(request.data) - return jsonify(**{'ids': result}) - - -@main.route('/content', methods=['POST']) -def content(): - ids = json.loads(request.form.get('ids')) - - content = {} - for id in ids: - content[id] = { - 'text': ' '.join(vocab_db.get_vocab_by_id(id)), - 'content': processed_db.get_records_by_id([id]) - } - - return jsonify(**{'content': content}) diff --git a/clusterix/controllers/utils.py b/clusterix/controllers/utils.py deleted file mode 100644 index 4e20a5d..0000000 --- a/clusterix/controllers/utils.py +++ /dev/null @@ -1,41 +0,0 @@ -import json -import os -from werkzeug.utils import secure_filename - -from ..config import TEMP_PATH -from ..database.db import save_csv - - -def get_attrs(request): - """Get the attributes sent from the frontend.""" - attrs = json.loads(request.form.get('data')) - - if request.files: - attrs.update({ - 'file': request.files.to_dict()['file'], - 'type': request.form.get('type'), - 'timestamp': request.form.get('timestamp'), - }) - - return attrs - - -def save_file_if_exists(attrs): - """Save file to disk, and according to the type, save to the DB.""" - try: - f = attrs['file'] - file_path = os.path.join(TEMP_PATH, secure_filename(f.filename)) - f.save(file_path) - - if attrs['type'] == 'text/csv': - save_csv(file_path, attrs) - - except KeyError: - pass # that means that there was no file sent, so abort - - -def replace_spaces_in_keys(d): - for record in d: - record['name'] = record['name'].replace(" ", "") - - return d diff --git a/clusterix/database/__init__.py b/clusterix/database/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/clusterix/database/db.py b/clusterix/database/db.py deleted file mode 100644 index f559df9..0000000 --- a/clusterix/database/db.py +++ /dev/null @@ -1,50 +0,0 @@ -import pandas as pd - -from ..log import log_info -from ..database.utils import get_whoosh_fields, get_processed, get_vocabulary -from ..database.models import WhooshProcessedDB, WhooshVocabularyDB -from ..config import TEMP_RAW_INPUT, TEMP_PROCESSED_INPUT, TEMP_VOCABULARY - -processed_db = WhooshProcessedDB(TEMP_PROCESSED_INPUT) -vocab_db = WhooshVocabularyDB(TEMP_VOCABULARY) - -def save_csv(file_path, attrs): - """ - Save the items of a new file. - 1. Save original data. - 2. Save processed data for clustering. - """ - df = pd.read_csv(file_path, - sep=attrs['csvType']['delimiter'], - error_bad_lines=False, - encoding='latin-1') - - df.rename(columns=lambda x: x.strip().replace(" ", ""), inplace=True) - - # fill median for missing numbers, NaN for strings - data = df.fillna(df.median())\ - .fillna(u'NaN')\ - .T.to_dict() - - # get fields and types both in Whoosh field types and pandas - # they are used for different things - dtypes = df.dtypes.apply(str).to_dict() - fields = get_whoosh_fields(dtypes) - - # Save original data - # - # log_info('Creating Whoosh DB (raw data)...') - # original_db.initialize(fields, dtypes, data) - - # Save processed data - # - log_info('Creating Whoosh DB (processed data)...') - processed_data = pd.DataFrame( - get_processed(data, dtypes) - ).T.to_dict() - processed_db.initialize(fields, dtypes, processed_data) - - # Save vocabulary data - # - log_info('Creating Whoosh DB (vocabulary)...') - vocab_db.initialize(get_vocabulary(df.values)) \ No newline at end of file diff --git a/clusterix/database/models.py b/clusterix/database/models.py deleted file mode 100644 index ce5335f..0000000 --- a/clusterix/database/models.py +++ /dev/null @@ -1,112 +0,0 @@ -import os -from math import log -from copy import deepcopy - -from whoosh.fields import Schema, TEXT -from whoosh.index import create_in -from whoosh.qparser import QueryParser - -from ..log import log_warn - - -# noinspection PyAttributeOutsideInit -class WhooshProcessedDB(object): - def __init__(self, path): - """Create the index folder and save the path.""" - if not os.path.exists(path): - os.mkdir(path) - self.path = path - - def initialize(self, fields, dtypes, data): - """Actual initialization of WhooshDB.""" - self.schema = Schema(**fields) - self.index = create_in(self.path, self.schema) - - writer = self.index.writer() - for key in data.keys(): - writer.add_document(**data[key]) - writer.commit() - - # Various relevant attributes - self.field_names = dtypes.keys() - self.dtypes = dtypes - self.searcher = self.index.searcher() - self.reader = self.index.reader() - self.all_documents = [item[1] for item in self.reader.iter_docs()] - - def get_field_type(self, field): - """Get the field type by name (used in vectorization etc).""" - return self.dtypes[field] - - def get_records_with_fields(self, fields): - """Get a list of all the records, but containing only the requested fields.""" - results = deepcopy(self.all_documents) - for item in results: - for key in item.keys(): - if key not in fields: - item.pop(key, None) - return results - - def get_records_by_id(self, ids): - """Return records based of the provided ids.""" - return [item[1] for item in self.reader.iter_docs() if item[0] in ids] - - -# noinspection PyAttributeOutsideInit -class WhooshVocabularyDB(object): - def __init__(self, path): - """Create the index folder and save the path.""" - if not os.path.exists(path): - os.mkdir(path) - self.path = path - - def initialize(self, data): - """Initialize with the vocabulary data.""" - self.schema = Schema(content=TEXT(stored=True)) - self.index = create_in(self.path, self.schema) - - writer = self.index.writer() - for doc in data: - if doc: - writer.add_document(content=doc) - writer.commit() - - self.searcher = self.index.searcher() - self.reader = self.index.reader() - self.doc_num = self.reader.doc_count() - self.parser = QueryParser('content', schema=self.schema) - - self.data = [row.split() for row in data] - self.words_per_doc = [len(row) for row in self.data] - - self.containing = lambda term: 1 + self.searcher.doc_frequency("content", term) - - def get_vocab_by_id(self, id): - return self.data[id] - - def search_for(self, term): - query = self.parser.parse(term) - result = self.searcher.search(query, limit=None) - return list(result.docs()) - - def get_relevant_terms(self, term): - docs = self.searcher.document_numbers(content=term) - return self.searcher.key_terms(docs, 'content', numterms=10) - - def tfidf(self, term): - try: - docs = self.search_for(term) - total_words = sum(self.words_per_doc[i] for i in docs) - - # TF: frequency if the term/total words in docs the term appears in - # IDF: total docs/docs containing this word - tf = self.searcher.frequency('content', term) / (total_words * 1.0) - idf = log(self.doc_num / self.containing(term)) - - return { - 'term': term, - 'tfidf': tf * idf - } - except Exception: - log_warn(u'Term not found: {}'.format(term)) - return None diff --git a/clusterix/database/utils.py b/clusterix/database/utils.py deleted file mode 100644 index a7d51a3..0000000 --- a/clusterix/database/utils.py +++ /dev/null @@ -1,89 +0,0 @@ -from sklearn.externals.joblib import Parallel, delayed - -from whoosh.analysis import StemmingAnalyzer, CharsetFilter, StandardAnalyzer -from whoosh.lang import stopwords -from whoosh.support.charset import accent_map -from whoosh.fields import TEXT, NUMERIC -""" -Some regexes for consideration: -split camel case (needs finditer) - regex = r'(?<=[A-Z])(?=[A-Z][a-z])|(?<=[^A-Z])(?=[A-Z])|(?<=[A-Za-z])(?=[^A-Za-z])' -does not return 's (but returns numbers) - regex = r"[\w][\w]*'?\w?" -""" - -stop = stopwords.stoplists['en'] -REGEX = r'\b[^\d\W]+\b' - -analyzer = StemmingAnalyzer(expression=REGEX, - stoplist=stop, - minsize=1) | CharsetFilter(accent_map) - -vocab_analyzer = StandardAnalyzer(expression=REGEX, - stoplist=stop, - minsize=3) | CharsetFilter(accent_map) - - -def get_whoosh_fields(dtypes): - """ - Returns a dict of field-type mappings for whoosh schema use. - From: 'Character': 'object' - To: 'Character': TEXT(stored=True) - """ - mapping = {} - for key in dtypes.keys(): - if dtypes[key] == 'object': - mapping[key] = TEXT(stored=True) - else: - mapping[key] = NUMERIC(stored=True) - - return mapping - - -def get_processed(data, dtypes): - """ - Process the input using Whoosh analysers, returns a list of dicts. - Example: - - CSV | - Results: - Name,Job,City | [{u'City': u'pari', u'Job': u'softwar dev', u'Name': u'john'}] - John,Software Dev.,Paris | - """ - parallel = Parallel(n_jobs=-1, backend='multiprocessing', verbose=1) - return parallel(delayed(_process)(data[i], i, dtypes) for i in data.keys()) - - -def _process(item, i, dtypes): - """Process a single item, to be used in parallel.""" - # log_info('Processing item: {}'.format(i)) - data = {} - - for field in item.keys(): - if dtypes[field] == 'object': - processed = ' '.join([token.text for token in analyzer(item[field])]) - data[field] = processed if processed != '' else u'NaN' - else: - data[field] = float(item[field]) - - return data - - -def get_vocabulary(documents): - """ - Process the vocabulary using Whoosh, returns a vocabulary list. - Example: - [['Chicago', 'Software engineering', 'Dreams to go to australia one day!'], - ['New York City', 'Painting outdoor spaces', 'Own a farm at the woods!']] - Results: - [u'chicago software engineering dreams go australia one day', - u'new york city painting outdoor spaces farm woods'] - """ - parallel = Parallel(n_jobs=-1, backend='multiprocessing', verbose=1) - return parallel(delayed(_process_vocabulary)(doc) for doc in documents) - - -def _process_vocabulary(doc): - row_str = ' '.join(map(unicode, doc)) - return ' '.join([token.text for token in vocab_analyzer(row_str)]) - - - diff --git a/clusterix/log.py b/clusterix/log.py deleted file mode 100644 index 2a6cc5a..0000000 --- a/clusterix/log.py +++ /dev/null @@ -1,29 +0,0 @@ -import logging -from colorlog import ColoredFormatter -"""Clusterix logging with color.""" - -LOG_LEVEL = logging.DEBUG -LOG_FORMAT = " %(log_color)s%(levelname)-8s%(reset)s | %(log_color)s%(message)s%(reset)s" - -logging.root.setLevel(LOG_LEVEL) -formatter = ColoredFormatter(LOG_FORMAT) - -stream = logging.StreamHandler() -stream.setLevel(LOG_LEVEL) -stream.setFormatter(formatter) - -log = logging.getLogger('pythonConfig') -log.setLevel(LOG_LEVEL) -log.addHandler(stream) - - -def log_info(message): - log.info(message) - - -def log_debug(message): - log.debug(message) - - -def log_warn(message): - log.warn(message) diff --git a/clusterix/static/js/init.js b/clusterix/static/js/init.js deleted file mode 100644 index 6635e07..0000000 --- a/clusterix/static/js/init.js +++ /dev/null @@ -1,24 +0,0 @@ -$(function() { - // JQuery slider init (fade in/out) - $.fn.fadeThenSlideToggle = function(buttonID, speed, easing, callback) { - var fadeOutButton = ''; - var fadeInButton = ''; - - if (this.is(":hidden")) { - $(buttonID).html(fadeInButton); - return this.slideDown(speed, easing).fadeTo(speed, 1, easing, callback); - } else { - $(buttonID).html(fadeOutButton); - return this.fadeTo(speed, 0, easing).slideUp(speed, easing, callback); - } - }; - - // Inputs - DataInput.init(); - - // Router & Validation - Router.init(); - - // Search - Search.init(); -}); \ No newline at end of file diff --git a/clusterix/static/js/input/algorithm_input.js b/clusterix/static/js/input/algorithm_input.js deleted file mode 100644 index 3a9b2e0..0000000 --- a/clusterix/static/js/input/algorithm_input.js +++ /dev/null @@ -1,52 +0,0 @@ -var AlgorithmInput = (function() { - var attr = { - algPanel: '#algorithms-panel', - algSelect: '#algorithms-selection', - vecSelect: '#vectorizer-selection' - }; - - /** - * Initialize the algorithm drop-down and notify the Router for changes. - */ - function initAlgorithmSelection() { - $(attr.algSelect).dropdown({ - onChange: function(val, text, selected) { - Router.data().algorithms.algorithmsToUse = val; - Router.checkUploadButton(); - } - }); - } - - function initVectorizerSelection() { - $(attr.vecSelect).dropdown({ - onChange: function(val, text, selected) { - Router.data().vectorizer = val; - } - }); - } - - return { - - /** - * Functionality: - * - Initialize Algorithms panel. - * - Init the respective algorithm options. - * @constructor - */ - init: function () { - initAlgorithmSelection(); - initVectorizerSelection(); - - // init algorithms & options - KMeans.init(); - HCluster.init(); - - // show the panel - $(attr.algPanel).fadeIn(); - - // Panel hide/show init - Utils.attachSliderToPanel('#algorithms-hide', '#algorithms-body', 150); - console.log('Algorithm Input init'); - } - } -})(); \ No newline at end of file diff --git a/clusterix/static/js/input/algorithms/hcluster.js b/clusterix/static/js/input/algorithms/hcluster.js deleted file mode 100644 index a7d29ca..0000000 --- a/clusterix/static/js/input/algorithms/hcluster.js +++ /dev/null @@ -1,66 +0,0 @@ -var HCluster = (function() { - - var attr = { - distanceSelector: '#distance-method-selection', - affinitySelector: '#affinity-selection', - affinityContainerSelector: '#affinity-container', - kSelector: '#hcluster-k-num' - - // Bcluster options - //blockBySelector: '#block-by-field', - //blockByContainer: '#block-by-container', - //blockByTemplate: '#block-by-template' - }; - - ///** - // * Renders the block by field according to the user-chosen fields, - // * and sets the variable to the Router. - // */ - //function renderBlockByField(data) { - // Utils.compileTemplate(attr.blockByTemplate, attr.blockByContainer, { fields: data }, true); - // Router.data().algorithms.bcluster.blockBy = data[0]; - // - // $(attr.blockBySelector).dropdown({ - // onChange: function(val, text, selected) { - // attr.blockBy = text; - // Router.data().algorithms.bcluster.blockBy = attr.blockBy; - // } - // }); - //} - // - //function initBlockClusteringOptions() { - // // Block by - // $(document).on('csv-fields-change', function(ev, fields) { - // renderBlockByField(fields.data); - // }); - //} - - return { - init: function() { - // Distance methods - $(attr.distanceSelector).dropdown({ - onChange: function(val, text, selected) { - if (val === 'centroid' || val === 'median' || val === 'ward') { - $(attr.affinityContainerSelector).hide(); - Router.data().algorithms.bcluster.affinity = 'euclidean'; - } else { - $(attr.affinityContainerSelector).show(); - } - Router.data().algorithms.bcluster.distance = val; - } - }); - - // Affinity - $(attr.affinitySelector).dropdown({ - onChange: function(val, text, selected) { - Router.data().algorithms.bcluster.affinity = val; - } - }); - - // K number - $(attr.kSelector).on('change', function() { - Router.data().algorithms.hcluster.kNumber = $(this).val(); - }); - } - } -})(); \ No newline at end of file diff --git a/clusterix/static/js/input/algorithms/kmeans.js b/clusterix/static/js/input/algorithms/kmeans.js deleted file mode 100644 index a981030..0000000 --- a/clusterix/static/js/input/algorithms/kmeans.js +++ /dev/null @@ -1,11 +0,0 @@ -var KMeans = (function() { - var kmeansSelector = '#kmeans-num'; - - return { - init: function() { - $(kmeansSelector).on('change', function() { - Router.data().algorithms.kmeans.kNumber = $(this).val(); - }); - } - } -})(); \ No newline at end of file diff --git a/clusterix/static/js/input/csv_fields_input.js b/clusterix/static/js/input/csv_fields_input.js deleted file mode 100644 index 8fc5ea9..0000000 --- a/clusterix/static/js/input/csv_fields_input.js +++ /dev/null @@ -1,103 +0,0 @@ -var CsvFieldsInput = (function() { - var attr = { - csvPanel: '#csv-fields-panel', - selectID: '#multiple-fields-csv', - scaleSelectors: '.scaling', - - csvFieldsContainer: '#csv-fields-container', - scaledFieldsContainer: '#scale-fields-container', - - scaleFieldsTemplate: '#fields-to-scale-template', - csvFieldsTemplate: '#fields-to-select-template', - - fields: [], // string array - fieldsWithScaling: [], // object array - blockBy: '' - }; - - /** - * Update the scaling and notify the Router. - */ - function updateFieldsWithScaling() { - var name = $(this).attr('name'); - var scale = $(this).val(); - - var field = attr.fieldsWithScaling.filter(function(field) { - if (field.name == name) return field - })[0]; - - field.scale = scale; - Router.data().csvType.fieldsWithScaling = attr.fieldsWithScaling; - } - - /** - * Gets the default values or the new values from the field selector, - * and sets them on the appropriate variables OR the default ones. - * @param fields - */ - function saveValues(fields) { - attr.fieldsWithScaling = fields.length - ? fields.map(function(f) { return {name: f, scale: 1} }) - : attr.fields.map(function(f) { return {name: f, scale: 1} }); - - // Everytime the values change, we want to change the block-by field - var fieldsToSend = fields.length ? fields : attr.fields; - $(document).trigger('csv-fields-change', {data: fieldsToSend}); - } - - /** - * Render all the chosen fields, with their scaling. - */ - function renderFieldsToScale() { - Utils.compileTemplate(attr.scaleFieldsTemplate, attr.scaledFieldsContainer, { fields: attr.fieldsWithScaling }, true); - $(attr.scaleSelectors).on('change', updateFieldsWithScaling); - - Router.data().csvType.fieldsWithScaling = attr.fieldsWithScaling; - } - - /** - * Every time an item is added/removed, it re-renders the panels. - */ - function renderFieldsToChoose() { - Utils.compileTemplate(attr.csvFieldsTemplate, attr.csvFieldsContainer, { fields: attr.fields }, true); - $(attr.selectID).dropdown({ - onChange: function(fields, item, selected) { - saveValues(fields); - renderFieldsToScale(); - } - }); - } - - return { - - /** - * Functionality: - * - Initializes the needed panels using the headers. - * - Re-renders the panels when a field is selected/disselected. - * - Notifies the Router. - * @constructor - * @param headers - */ - init: function(headers) { - attr.fields = headers; - saveValues(headers); - - renderFieldsToChoose(); - renderFieldsToScale(); - - $(attr.csvPanel).fadeIn(); - - // Panel hide/show - Utils.attachSliderToPanel('#csv-fields-hide', '#csv-fields-body', 150); - console.log('Csv Fields Input init'); - }, - - /** - * Hide the csv fields panel. - */ - hide: function() { - $(attr.csvPanel).fadeOut(); - attr = {}; - } - } -})(); \ No newline at end of file diff --git a/clusterix/static/js/input/data_input.js b/clusterix/static/js/input/data_input.js deleted file mode 100644 index 4c53396..0000000 --- a/clusterix/static/js/input/data_input.js +++ /dev/null @@ -1,67 +0,0 @@ -var DataInput = (function() { - var attr = {}; - var dataInput = '#data-input'; - - var fileInputConfig = { - maxFileCount:1, - allowedFileExtensions: ['csv'], - showPreview: false, - showRemove: false, - - uploadClass: 'btn btn-default', - uploadLabel: 'Preview', - uploadIcon: ' ', - layoutTemplates: { - main1: "{preview}
" + - "
{browse}" + - "{upload}" + // data-toggle='modal' data-target='#data-preview-modal' - "{remove}
{caption}
" - } - }; - - /** - * Initializes the preview button and the contents of the preview modal. - * Depending on the input type, there are different rendering methods. - */ - function initModalPreview() { - switch (attr.fileType) { - case 'text/csv': - PreviewRenderer.renderCsv(attr.file); - break; - } - } - - return { - - /** - * Functionality: - * - 1st step of the workflow. Creates the file input, checks for acceptable files. - * - Renders a preview of the file content, by calling the PreviewRenderer. - * - Sends file to the router for keeping. - * - Calls the next parts of the workflow CSV Field Options (if csv) and Algorithm Input. - * @constructor - */ - init: function() { - $(dataInput) - .fileinput(fileInputConfig) - .on('change', function() { - // Checking for stuff - if (!this.files[0]) return; - - // Save important file attributes - attr.file = this.files[0]; - attr.fileName = attr.file.name; - attr.fileType = attr.file.type; - initModalPreview(); - - // Save the file (to be sent). - Router.setFile(attr.file); - AlgorithmInput.init(); - }); - - // Panel hide/show - Utils.attachSliderToPanel('#data-input-hide', '#data-input-body', 150); - console.log('Data Input init'); - } - } -})(); \ No newline at end of file diff --git a/clusterix/static/js/render/diagram_renderer.js b/clusterix/static/js/render/diagram_renderer.js deleted file mode 100644 index 8d605ea..0000000 --- a/clusterix/static/js/render/diagram_renderer.js +++ /dev/null @@ -1,64 +0,0 @@ -var Renderer = (function() { - var sizeMap = {width: 1000, height: 300}; - var sizeMini = {width: 200, height: 100}; - - var selectors = { - diagramSelector: '#vizualization-area', - infoSelector: '#selections-area ul', - miniSelector: '#viz-mini', - resultsSelector: '#brush-results', - distributionSelector: '#cluster-comparison', - tfidfSelector: '#tf-idf-results' - }; - - var idNumber = 0; - - function getNewId() { - idNumber++; - return idNumber; - } - - return { - - get_selector: function(selector) { - return selectors[selector]; - }, - - /** - * Renders the returned data. - * @param data - */ - render: function(data) { - var keys = Object.keys(data); - var id = getNewId(); - - // Empty the div, we only want a single scatterplot - // Also empty the brushed content - $(selectors.diagramSelector).empty(); - $(selectors.infoSelector).empty(); - $(selectors.tfidfSelector).empty(); - $(selectors.distributionSelector).empty(); - - keys.forEach(function(key) { - switch (key) { - case 'kmeans': - new Scatterplot().init(data[key], sizeMap.width, sizeMap.height, selectors.diagramSelector, id); - new ScatterplotMini().init(data[key], sizeMini.width, sizeMini.height, selectors.miniSelector, id); - - break; - - case 'hcluster': - // new Treemap().init(data[key], sizeMap.width, sizeMap.height, selectors.diagramSelector, id); - // new TreemapMini().init(data[key], sizeMini.width, sizeMini.height, selectors.miniSelector, id); - // - // break; - new Scatterplot().init(data[key], sizeMap.width, sizeMap.height, selectors.diagramSelector, id); - new ScatterplotMini().init(data[key], sizeMini.width, sizeMini.height, selectors.miniSelector, id); - - break; - } - }); - } - } - -})(); \ No newline at end of file diff --git a/clusterix/static/js/render/loading_screen_renderer.js b/clusterix/static/js/render/loading_screen_renderer.js deleted file mode 100644 index 59238b8..0000000 --- a/clusterix/static/js/render/loading_screen_renderer.js +++ /dev/null @@ -1,86 +0,0 @@ -var LoadingScreenRenderer = (function () { - var loadingScreenIntervalID; - var loadingScreenID = '#loading-screen-dimmer'; - - return { - - initLoadingScreen: function() { - $(loadingScreenID) - .on('click', function(){ return false; }) - .dimmer('show'); - LoadingScreenRenderer.render("#clusterix_loader"); - - //var message = ''; - //loadingScreenIntervalID = setInterval(function() { - // $.ajax({ - // type: 'GET', - // url: '/update', - // success: function(msg) { - // if (msg !== message) { - // message = msg; - // - // $('#loading-screen-msg').text(msg); - // console.log('Update: ' + msg); - // } - // } - // }); - //}, 1000); - }, - - removeLoadingScreen: function() { - clearInterval(loadingScreenIntervalID); - $(loadingScreenID).dimmer('hide'); - //$('#loading-screen-msg').text('Loading...'); - }, - - render: function (placement) { - var svg = d3.select(placement + " svg"); - var width = svg.attr('width'), - height = svg.attr('height'); - - var data = []; - - svg.selectAll("circle").each(function (d, i) { - var circle_data = { - 'or': d3.select(this).attr('r'), - 'ocx': d3.select(this).attr('cx'), - 'ocy': d3.select(this).attr('cy'), - 'fill': d3.select(this).style('fill') - }; - d3.select(this).remove(); - data.push(circle_data); - }); - - var circles = svg.selectAll("circle") - .data(data) - .enter() - .append("circle") - .style('fill', '#9eacad') - .attr('cx', function (d, i) { - // we start the X positions either at the beginning of the canvas, or end. - var start_position = Math.round(Math.random()) == 0 ? 0 : width; - return start_position == 0 ? start_position - (20) : start_position + 20; - }) - .attr('cy', function (d, i) { return Math.random() * 3000 % height; }) - .attr('r', 0); - - circles.transition() - .ease('linear') - .duration(function (d, i) { return (Math.random() * i) * 200; }) - .delay(function (d, i) { return i * (Math.random() * 50); }) - .attr('cx', function (d) { return d.ocx; }) - .attr('cy', function (d) { return d.ocy; }) - .attr('r', function (d) { return d.or; }) - - .style('opacity', 1) - .each("end", function (d, i) { - d3.select(this) - .transition('elastic') - .duration(500) - .delay(function (d, i) { return i * (Math.random() * 400); }) - .style("fill", d.fill); - }); - } - } -}) -(); \ No newline at end of file diff --git a/clusterix/static/js/render/preview_renderer.js b/clusterix/static/js/render/preview_renderer.js deleted file mode 100644 index 1bfea6e..0000000 --- a/clusterix/static/js/render/preview_renderer.js +++ /dev/null @@ -1,37 +0,0 @@ -var PreviewRenderer = (function() { - var attr = { - modalContent: '#data-modal-content', - txtTemplate: '#txt-file-preview-template', - csvTemplate: '#csv-file-preview-template' - }; - - function initPreview() { - $('#data-preview').on('click', function() { - $('#preview-dimmer').modal('show'); - }); - } - - return { - - /** - * Parses the CSV file using Papaparse and renders the modal with the preview. - */ - renderCsv: function (csvFile) { - Papa.parse(csvFile, { - dynamicTyping: true, preview: 25, - complete: function (results) { - var headers = results.data.shift(); - var delimiter = results.meta.delimiter; - var data = results.data; - - Utils.compileTemplate(attr.csvTemplate, attr.modalContent, {headers: headers, data: data}, true); - Router.data().csvType.delimiter = delimiter; - initPreview(); - - // Render the csv panel. We do this here because we need the headers (fields). - CsvFieldsInput.init(headers); - } - }); - } - } -})(); \ No newline at end of file diff --git a/clusterix/static/js/router.js b/clusterix/static/js/router.js deleted file mode 100644 index 18f8616..0000000 --- a/clusterix/static/js/router.js +++ /dev/null @@ -1,133 +0,0 @@ -var Router = (function() { - var attr = { - alertTemplate: '#alert-template', - upload: '#upload', - - url: '/upload_and_cluster' - }; - - var newFile; - - var dataFileInfo = { - empty: true, - file: null, - timestamp: '' - }; - - var dataModel = { - - // CSV-specific info - csvType: { - delimiter: ',', - fieldsWithScaling: [] - }, - - // Each algorithm need certain parameters provided here. - algorithms: { - algorithmsToUse: [], - - kmeans: { - kNumber: 1 - }, - - bcluster: { - blockBy: '', - distance: 'single', - affinity: 'euclidean' - }, - - hcluster: { - distance: 'single', - affinity: 'euclidean', - kNumber: 1 - } - }, - - // 3 vectorizer options: Count, Hashing, Tfidf - vectorizer: 'count' - }; - - - /** - * Uploads everything in FormData format. - */ - function upload() { - var data = new FormData(); - data.append('data', JSON.stringify(dataModel)); - - // If newFile == true, send the file - // Else, do not send it and use the route that will get the data without preprocessing - if (newFile) { - data.append('file', dataFileInfo.file); - data.append('type', dataFileInfo.file.type); - data.append('timestamp', dataFileInfo.timestamp); - - newFile = false; - } - - $.ajax({ - type: 'POST', - url: attr.url, - data: data, - cache: false, - contentType: false, - processData: false, - success: function(data){ - Renderer.render(data); - LoadingScreenRenderer.removeLoadingScreen(); - } - }); - LoadingScreenRenderer.initLoadingScreen(); - } - - return { - - /** - * Functionality: - * - Saves file/options. - * - Validates the data. - * - Uploads, and on success it renders the visualizations. - * @constructor - */ - init: function() { - console.log('Router init'); - $(attr.upload).on('click', this.validateAndUpload); - }, - - /** - * Saves the data file. - */ - setFile: function(file) { - dataFileInfo = { - empty: false, - file: file, - fileName: file.name, - timestamp: new Date().getTime() - }; - - // We set the newFile variable. If true, we upload the file, - // else we upload just the data and use the previously created - // json file to produce the results. - newFile = true; - console.log('-- Router received: file: ' + dataFileInfo.fileName); - }, - - /** - * Sets specific options to the data model. - */ - data: function() { return dataModel; }, - - /** - * Self explained. Also notifies Search in order to create index based on csv fields. - */ - validateAndUpload: function() { upload(); }, - - /** - * Checks if the upload button exists, and makes it appear/disappear. - */ - checkUploadButton: function() { - if (!dataModel.algorithms.algorithmsToUse.length) $(attr.upload).fadeOut(200); - else $(attr.upload).fadeIn(200); - } - } -})(); \ No newline at end of file diff --git a/clusterix/static/js/utils.js b/clusterix/static/js/utils.js deleted file mode 100644 index a33f259..0000000 --- a/clusterix/static/js/utils.js +++ /dev/null @@ -1,57 +0,0 @@ -var Utils = (function() { - - return { - - /** - * Compile and render a template using Handlebars. - */ - compileTemplate: function(template, target, data, emptyElement) { - var rendered = Handlebars.compile($(template).html())(data); - - if (emptyElement) - $(target).empty().append(rendered); - else - $(target).append(rendered); - }, - - /** - * Returns true/ false depending on whether an element is found in an array or not. - */ - inArray: function(value, array) { - return array.indexOf(value) > -1; - }, - - /** - * Accepts two arrays, and returns an array containing all the elements that - * exist on the first array but not the second. - */ - arrayDifference: function(first, second) { - return $(first).not(second).get(); - }, - - /** - * Delays the trigger of a function by the provided time. - */ - debounce: function(fn, delay) { - var timer = null; - return function() { - var context = this; - var args = arguments; - - clearTimeout(timer); - timer = setTimeout(function () { - fn.apply(context, args); - }, delay); - }; - }, - - /** - * Attach panel to custom slider. - */ - attachSliderToPanel: function(buttonID, contentID, speed/*, easing, callback*/) { - $(buttonID).click(function() { - $(contentID).fadeThenSlideToggle(buttonID, speed); - }); - } - } -})(); \ No newline at end of file diff --git a/clusterix/static/lib/papaparse.min.js b/clusterix/static/lib/papaparse.min.js deleted file mode 100644 index a62a926..0000000 --- a/clusterix/static/lib/papaparse.min.js +++ /dev/null @@ -1,6 +0,0 @@ -/*! - Papa Parse - v4.1.2 - https://github.com/mholt/PapaParse -*/ -!function(e){"use strict";function t(t,r){if(r=r||{},r.worker&&S.WORKERS_SUPPORTED){var n=f();return n.userStep=r.step,n.userChunk=r.chunk,n.userComplete=r.complete,n.userError=r.error,r.step=m(r.step),r.chunk=m(r.chunk),r.complete=m(r.complete),r.error=m(r.error),delete r.worker,void n.postMessage({input:t,config:r,workerId:n.id})}var o=null;return"string"==typeof t?o=r.download?new i(r):new a(r):(e.File&&t instanceof File||t instanceof Object)&&(o=new s(r)),o.stream(t)}function r(e,t){function r(){"object"==typeof t&&("string"==typeof t.delimiter&&1==t.delimiter.length&&-1==S.BAD_DELIMITERS.indexOf(t.delimiter)&&(u=t.delimiter),("boolean"==typeof t.quotes||t.quotes instanceof Array)&&(o=t.quotes),"string"==typeof t.newline&&(h=t.newline))}function n(e){if("object"!=typeof e)return[];var t=[];for(var r in e)t.push(r);return t}function i(e,t){var r="";"string"==typeof e&&(e=JSON.parse(e)),"string"==typeof t&&(t=JSON.parse(t));var n=e instanceof Array&&e.length>0,i=!(t[0]instanceof Array);if(n){for(var a=0;a0&&(r+=u),r+=s(e[a],a);t.length>0&&(r+=h)}for(var o=0;oc;c++){c>0&&(r+=u);var d=n&&i?e[c]:c;r+=s(t[o][d],c)}o-1||" "==e.charAt(0)||" "==e.charAt(e.length-1);return r?'"'+e+'"':e}function a(e,t){for(var r=0;r-1)return!0;return!1}var o=!1,u=",",h="\r\n";if(r(),"string"==typeof e&&(e=JSON.parse(e)),e instanceof Array){if(!e.length||e[0]instanceof Array)return i(null,e);if("object"==typeof e[0])return i(n(e[0]),e)}else if("object"==typeof e)return"string"==typeof e.data&&(e.data=JSON.parse(e.data)),e.data instanceof Array&&(e.fields||(e.fields=e.data[0]instanceof Array?e.fields:n(e.data[0])),e.data[0]instanceof Array||"object"==typeof e.data[0]||(e.data=[e.data])),i(e.fields||[],e.data||[]);throw"exception: Unable to serialize unrecognized input"}function n(t){function r(e){var t=_(e);t.chunkSize=parseInt(t.chunkSize),e.step||e.chunk||(t.chunkSize=null),this._handle=new o(t),this._handle.streamer=this,this._config=t}this._handle=null,this._paused=!1,this._finished=!1,this._input=null,this._baseIndex=0,this._partialLine="",this._rowCount=0,this._start=0,this._nextChunk=null,this.isFirstChunk=!0,this._completeResults={data:[],errors:[],meta:{}},r.call(this,t),this.parseChunk=function(t){if(this.isFirstChunk&&m(this._config.beforeFirstChunk)){var r=this._config.beforeFirstChunk(t);void 0!==r&&(t=r)}this.isFirstChunk=!1;var n=this._partialLine+t;this._partialLine="";var i=this._handle.parse(n,this._baseIndex,!this._finished);if(!this._handle.paused()&&!this._handle.aborted()){var s=i.meta.cursor;this._finished||(this._partialLine=n.substring(s-this._baseIndex),this._baseIndex=s),i&&i.data&&(this._rowCount+=i.data.length);var a=this._finished||this._config.preview&&this._rowCount>=this._config.preview;if(y)e.postMessage({results:i,workerId:S.WORKER_ID,finished:a});else if(m(this._config.chunk)){if(this._config.chunk(i,this._handle),this._paused)return;i=void 0,this._completeResults=void 0}return this._config.step||this._config.chunk||(this._completeResults.data=this._completeResults.data.concat(i.data),this._completeResults.errors=this._completeResults.errors.concat(i.errors),this._completeResults.meta=i.meta),!a||!m(this._config.complete)||i&&i.meta.aborted||this._config.complete(this._completeResults),a||i&&i.meta.paused||this._nextChunk(),i}},this._sendError=function(t){m(this._config.error)?this._config.error(t):y&&this._config.error&&e.postMessage({workerId:S.WORKER_ID,error:t,finished:!1})}}function i(e){function t(e){var t=e.getResponseHeader("Content-Range");return parseInt(t.substr(t.lastIndexOf("/")+1))}e=e||{},e.chunkSize||(e.chunkSize=S.RemoteChunkSize),n.call(this,e);var r;this._nextChunk=k?function(){this._readChunk(),this._chunkLoaded()}:function(){this._readChunk()},this.stream=function(e){this._input=e,this._nextChunk()},this._readChunk=function(){if(this._finished)return void this._chunkLoaded();if(r=new XMLHttpRequest,k||(r.onload=g(this._chunkLoaded,this),r.onerror=g(this._chunkError,this)),r.open("GET",this._input,!k),this._config.chunkSize){var e=this._start+this._config.chunkSize-1;r.setRequestHeader("Range","bytes="+this._start+"-"+e),r.setRequestHeader("If-None-Match","webkit-no-cache")}try{r.send()}catch(t){this._chunkError(t.message)}k&&0==r.status?this._chunkError():this._start+=this._config.chunkSize},this._chunkLoaded=function(){if(4==r.readyState){if(r.status<200||r.status>=400)return void this._chunkError();this._finished=!this._config.chunkSize||this._start>t(r),this.parseChunk(r.responseText)}},this._chunkError=function(e){var t=r.statusText||e;this._sendError(t)}}function s(e){e=e||{},e.chunkSize||(e.chunkSize=S.LocalChunkSize),n.call(this,e);var t,r,i="undefined"!=typeof FileReader;this.stream=function(e){this._input=e,r=e.slice||e.webkitSlice||e.mozSlice,i?(t=new FileReader,t.onload=g(this._chunkLoaded,this),t.onerror=g(this._chunkError,this)):t=new FileReaderSync,this._nextChunk()},this._nextChunk=function(){this._finished||this._config.preview&&!(this._rowCount=this._input.size,this.parseChunk(e.target.result)},this._chunkError=function(){this._sendError(t.error)}}function a(e){e=e||{},n.call(this,e);var t,r;this.stream=function(e){return t=e,r=e,this._nextChunk()},this._nextChunk=function(){if(!this._finished){var e=this._config.chunkSize,t=e?r.substr(0,e):r;return r=e?r.substr(e):"",this._finished=!r,this.parseChunk(t)}}}function o(e){function t(){if(b&&d&&(h("Delimiter","UndetectableDelimiter","Unable to auto-detect delimiting character; defaulted to '"+S.DefaultDelimiter+"'"),d=!1),e.skipEmptyLines)for(var t=0;t=y.length?(r.__parsed_extra||(r.__parsed_extra=[]),r.__parsed_extra.push(b.data[t][n])):r[y[n]]=b.data[t][n])}e.header&&(b.data[t]=r,n>y.length?h("FieldMismatch","TooManyFields","Too many fields: expected "+y.length+" fields but parsed "+n,t):n1&&(h+=Math.abs(l-i),i=l):i=l}c.data.length>0&&(f/=c.data.length),("undefined"==typeof n||n>h)&&f>1.99&&(n=h,r=o)}return e.delimiter=r,{successful:!!r,bestDelimiter:r}}function a(e){e=e.substr(0,1048576);var t=e.split("\r");if(1==t.length)return"\n";for(var r=0,n=0;n=t.length/2?"\r\n":"\r"}function o(e){var t=l.test(e);return t?parseFloat(e):e}function h(e,t,r,n){b.errors.push({type:e,code:t,message:r,row:n})}var f,c,d,l=/^\s*-?(\d*\.?\d+|\d+\.?\d*)(e[-+]?\d+)?\s*$/i,p=this,g=0,v=!1,k=!1,y=[],b={data:[],errors:[],meta:{}};if(m(e.step)){var R=e.step;e.step=function(n){if(b=n,r())t();else{if(t(),0==b.data.length)return;g+=n.data.length,e.preview&&g>e.preview?c.abort():R(b,p)}}}this.parse=function(r,n,i){if(e.newline||(e.newline=a(r)),d=!1,!e.delimiter){var o=s(r);o.successful?e.delimiter=o.bestDelimiter:(d=!0,e.delimiter=S.DefaultDelimiter),b.meta.delimiter=e.delimiter}var h=_(e);return e.preview&&e.header&&h.preview++,f=r,c=new u(h),b=c.parse(f,n,i),t(),v?{meta:{paused:!0}}:b||{meta:{paused:!1}}},this.paused=function(){return v},this.pause=function(){v=!0,c.abort(),f=f.substr(c.getCharIndex())},this.resume=function(){v=!1,p.streamer.parseChunk(f)},this.aborted=function(){return k},this.abort=function(){k=!0,c.abort(),b.meta.aborted=!0,m(e.complete)&&e.complete(b),f=""}}function u(e){e=e||{};var t=e.delimiter,r=e.newline,n=e.comments,i=e.step,s=e.preview,a=e.fastMode;if(("string"!=typeof t||S.BAD_DELIMITERS.indexOf(t)>-1)&&(t=","),n===t)throw"Comment character same as delimiter";n===!0?n="#":("string"!=typeof n||S.BAD_DELIMITERS.indexOf(n)>-1)&&(n=!1),"\n"!=r&&"\r"!=r&&"\r\n"!=r&&(r="\n");var o=0,u=!1;this.parse=function(e,h,f){function c(e){b.push(e),S=o}function d(t){return f?p():("undefined"==typeof t&&(t=e.substr(o)),w.push(t),o=g,c(w),y&&_(),p())}function l(t){o=t,c(w),w=[],O=e.indexOf(r,o)}function p(e){return{data:b,errors:R,meta:{delimiter:t,linebreak:r,aborted:u,truncated:!!e,cursor:S+(h||0)}}}function _(){i(p()),b=[],R=[]}if("string"!=typeof e)throw"Input must be a string";var g=e.length,m=t.length,v=r.length,k=n.length,y="function"==typeof i;o=0;var b=[],R=[],w=[],S=0;if(!e)return p();if(a||a!==!1&&-1===e.indexOf('"')){for(var C=e.split(r),E=0;E=s)return b=b.slice(0,s),p(!0)}}return p()}for(var x=e.indexOf(t,o),O=e.indexOf(r,o);;)if('"'!=e[o])if(n&&0===w.length&&e.substr(o,k)===n){if(-1==O)return p();o=O+v,O=e.indexOf(r,o),x=e.indexOf(t,o)}else if(-1!==x&&(O>x||-1===O))w.push(e.substring(o,x)),o=x+m,x=e.indexOf(t,o);else{if(-1===O)break;if(w.push(e.substring(o,O)),l(O+v),y&&(_(),u))return p();if(s&&b.length>=s)return p(!0)}else{var I=o;for(o++;;){var I=e.indexOf('"',I+1);if(-1===I)return f||R.push({type:"Quotes",code:"MissingQuotes",message:"Quoted field unterminated",row:b.length,index:o}),d();if(I===g-1){var D=e.substring(o,I).replace(/""/g,'"');return d(D)}if('"'!=e[I+1]){if(e[I+1]==t){w.push(e.substring(o,I).replace(/""/g,'"')),o=I+1+m,x=e.indexOf(t,o),O=e.indexOf(r,o);break}if(e.substr(I+1,v)===r){if(w.push(e.substring(o,I).replace(/""/g,'"')),l(I+1+v),x=e.indexOf(t,o),y&&(_(),u))return p();if(s&&b.length>=s)return p(!0);break}}else I++}}return d()},this.abort=function(){u=!0},this.getCharIndex=function(){return o}}function h(){var e=document.getElementsByTagName("script");return e.length?e[e.length-1].src:""}function f(){if(!S.WORKERS_SUPPORTED)return!1;if(!b&&null===S.SCRIPT_PATH)throw new Error("Script path cannot be determined automatically when Papa Parse is loaded asynchronously. You need to set Papa.SCRIPT_PATH manually.");var t=S.SCRIPT_PATH||v;t+=(-1!==t.indexOf("?")?"&":"?")+"papaworker";var r=new e.Worker(t);return r.onmessage=c,r.id=w++,R[r.id]=r,r}function c(e){var t=e.data,r=R[t.workerId],n=!1;if(t.error)r.userError(t.error,t.file);else if(t.results&&t.results.data){var i=function(){n=!0,d(t.workerId,{data:[],errors:[],meta:{aborted:!0}})},s={abort:i,pause:l,resume:l};if(m(r.userStep)){for(var a=0;a - - - Clusterix - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {% raw %} - - - - - - - - - - - - - - {% endraw %} - - - - - - - - - - - - - - - - -
- - - - -
-

Processing Space

- - - -
-
-

Data Input

- -
-
- - -
-
- - - -
-
-

CSV Fields Options

- -
-
-
- - - - - - - -
-
-
- - - -
-
-

Algorithm Definitions & - Options

- -
- - -
-
- - -
- - -
- - -
- -
- -
-
-
K-Means -
-
-
-
-
- - -
-
-
-
- - -
- -
-
- - -
- - -
- - -
- - -
- -
- - -
-
-
-
-
- -
-
- - - - Upload and Cluster - Data - - -
- - - - - -
-
-

Visualization Swatchboard

-
-
-
-
-
-
- -
-
-
-

TF-IDF by cluster

-
- -
-
- -
-

Cluster Dimension - Comparison

- -
-
- -
-
- -
-
-
-
-
- - - - - - - -
-
-
-
-
-
-
- -{##} - - -
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-

-
Loading...
-
-
-
- - \ No newline at end of file diff --git a/manage.py b/manage.py index cebebdb..6fdf37d 100644 --- a/manage.py +++ b/manage.py @@ -1,9 +1,16 @@ -import sys -from flask.ext.script import Manager, Server -from clusterix import app +from flask_script import Manager, Server +from src import app +from src.config import logger manager = Manager(app) manager.add_command('runserver', Server(use_debugger=True)) if __name__ == '__main__': - sys.setrecursionlimit(10000) # Overflow if we don't do that - manager.run() \ No newline at end of file + logger.info( + """ + ___ _ _ _ ___ _____ ___ ___ _____ __ + / __| | | | | / __|_ _| __| _ \_ _\ \/ / + | (__| |_| |_| \__ \ | | | _|| /| | > < + \___|____\___/|___/ |_| |___|_|_\___/_/\_\\ + + """) + manager.run() diff --git a/requirements.txt b/requirements.txt index f33a714..4864ea1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,11 +1,8 @@ numpy pandas +pandasql scipy sklearn flask flask-script -flask-assets -requests -git+https://github.com/inveniosoftware/beard.git -colorlog whoosh \ No newline at end of file diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..619cfa7 --- /dev/null +++ b/src/__init__.py @@ -0,0 +1,3 @@ +from .utils import save_file, process_and_save_dataframe, \ + save_df, save_cluster_model, load_df, load__cluster_model +from .app import app diff --git a/src/app.py b/src/app.py new file mode 100644 index 0000000..3afd9b4 --- /dev/null +++ b/src/app.py @@ -0,0 +1,8 @@ +from flask import Flask +from .routes import main, results + + +# App init +app = Flask(__name__) +app.register_blueprint(main) +app.register_blueprint(results) diff --git a/src/cluster/__init__.py b/src/cluster/__init__.py new file mode 100644 index 0000000..94cbd16 --- /dev/null +++ b/src/cluster/__init__.py @@ -0,0 +1 @@ +from .process import cluster_data, get_projection, predict_data diff --git a/src/cluster/algorithms.py b/src/cluster/algorithms.py new file mode 100644 index 0000000..3f6818b --- /dev/null +++ b/src/cluster/algorithms.py @@ -0,0 +1,60 @@ +from sklearn.cluster import KMeans +from ..config import log +from ..misc import scatterplot +from ..utils import save_cluster_model + + +@log('K-Means clustering started.') +def kmeans(X, attrs): + """ + K-Means clustering. + Return example: + {'k_num': 2, + 'nodes': [ + {'cluster': 0, 'isCentroid': False, 'id': 2, + 'x': 2.658062848683116e-16, 'y': 1.7320508075688774}, + {'cluster': 0, 'isCentroid': True, + 'x': 2.220446049250313e-16, 'y': 1.7320508075688774}, + {'cluster': 1, 'isCentroid': False, 'id': 1, + 'x': 1.5034503553765397, 'y': 0.0}, + ..............]} + """ + n_clusters = int(attrs['kNumber']) + k_means = KMeans(n_clusters=n_clusters, n_jobs=-1) + k_means.fit(X) + + labels = k_means.labels_ + centroids = k_means.cluster_centers_ + + save_cluster_model(k_means) + return labels, scatterplot(X, labels, n_clusters, centroids=centroids) + + +# def hcluster(X, attrs): +# """ +# Hierarchical Clustering. +# Return Example: +# {'children': [ +# {'children': [], 'name': 2, 'value': 150.0039243544126}, +# {'children': [ +# {'children': [], 'name': 1, 'value': 2.509279181210386}, +# {'children': [ +# {'children': [], 'name': 0, 'value': 2.4987419269136737}, +# {'children': [], 'name': 3, 'value': 2.4987419269136737} +# ], 'name': 4,'value': 4.997483853827347} +# ], 'name': 5, 'value': 5.018558362420772} +# ], 'name': 6, 'value': 300.0078487088252} +# """ +# n_clusters = int(attrs['kNumber']) +# hcluster = ScipyHierarchicalClustering(method=attrs['distance'], +# affinity=attrs['affinity'], +# n_clusters=n_clusters) +# +# hcluster.fit(X) +# labels = hcluster.labels_ +# +# # Z = hcluster.linkage_ +# # return HClusterTree(Z).to_dict() +# +# # save_clusterer(hcluster) +# return scatterplot(X, labels, n_clusters) diff --git a/src/cluster/process.py b/src/cluster/process.py new file mode 100644 index 0000000..d906c86 --- /dev/null +++ b/src/cluster/process.py @@ -0,0 +1,42 @@ +from .algorithms import kmeans +from .utils import get_all_vectors +from ..misc import dim_reduction +from ..config import logger, log +from ..utils import load_df, save_df + + +@log('CLUSTERING PROCESS STARTED.') +def cluster_data(attrs): + """Retrieve items, cluster, and return the result dict.""" + algorithm, decomp, metric = attrs['algorithm'], attrs['decomposition'], attrs['distanceMetric'] + df = load_df() + + X = get_all_vectors(df, attrs) + logger.info('- Data shape original: {}'.format(X.shape)) + + # Dimension reduction + X = dim_reduction(X, decomp) + + # Execute clustering + if algorithm == 'kmeans': + labels, plot = kmeans(X, attrs) + + # Save the dataframe with the clusters for tfidf and similar + df['cluster'] = labels + save_df(df) + return plot + + +@log('CLUSTER PREDICTION PROCESS STARTED.') +def predict_data(attrs): + pass + + +@log('PROJECTION PROCESS STARTED.') +def get_projection(attrs): + """Project the data without clustering.""" + df = load_df() + X = get_all_vectors(df, attrs) + X = dim_reduction(X, attrs['decomposition'], attrs['distanceMetric']) + return [dict(x=x[0], y=x[1]) for x in X] + diff --git a/src/cluster/utils.py b/src/cluster/utils.py new file mode 100644 index 0000000..9337a8b --- /dev/null +++ b/src/cluster/utils.py @@ -0,0 +1,40 @@ +from scipy.sparse import hstack +from ..misc import get_vectorizer, stem_text_input +from ..config import logger, log + + +@log('Vectorization started.') +def get_vectorized_text(dataframe, columns, attrs): + """Vectorize the text columns of a dataframe. Append them in a single matrix""" + vec = get_vectorizer(attrs) + stem = True if attrs['stemming'] == 'true' else False + + X = [] + for column in columns: + logger.info('- Vectorizing field: {}...'.format(column)) + text = dataframe[column].values + + if stem: + text = stem_text_input(text) + + X_new = vec.fit_transform(text) + X = hstack([X, X_new]) if X != [] else X_new + return X + + +def get_dataframe_fields(df, fields): + """Returns the numerical/text field names to be used in this iteration.""" + text_labels = list(df.select_dtypes(include=['object']).columns) + num_columns = [field for field in fields if field not in text_labels] + text_columns = [field for field in fields if field in text_labels] + return num_columns, text_columns + + +def get_all_vectors(df, attrs): + """Returna a matrix of the numerical and text vectors (text after vectorisation).""" + num_columns, text_columns = get_dataframe_fields(df, attrs['fields']) + X = df[num_columns].values # Get the numerical fields (ready to use) + + # Check if there are any text columns, and vectorize them + return hstack([X, get_vectorized_text(df, text_columns, attrs)]) \ + if text_columns else X diff --git a/src/config.py b/src/config.py new file mode 100644 index 0000000..d5234a8 --- /dev/null +++ b/src/config.py @@ -0,0 +1,44 @@ +import logging +import sys + + +# Algorithm templates +algorithms = { + 'kmeans': 'algorithms/kmeans.html', + 'hcluster': 'algorithms/hcluster.html', + 'meanshift': 'algorithms/meanshift.html', + 'spectral': 'algorithms/spectral.html' +} + +# App configuration +TEMP_PATH = 'temp/' +CLUSTERER_PATH = 'temp/clusterer.pkl' +DATAFRAME_PATH = 'temp/dataframe.pkl' +DECOMPOSITION_MODEL_PATH = 'temp/decomposition.pkl' + +stopwords = ['i', 'me', 'my', 'myself', 'we', 'our', 'ours', 'ourselves', 'you', 'your', 'yours', 'yourself', + 'yourselves', 'he', 'him', 'his', 'himself', 'she', 'her', 'hers', 'herself', 'it', 'its', 'itself', + 'they', 'them', 'their', 'theirs', 'themselves', 'what', 'which', 'who', 'whom', 'this', 'that', 'these', + 'those', 'am', 'is', 'are', 'was', 'were', 'be', 'been', 'being', 'have', 'has', 'had', 'having', 'do', + 'does', 'did', 'doing', 'a', 'an', 'the', 'and', 'but', 'if', 'or', 'because', 'as', 'until', 'while', + 'of', 'at', 'by', 'for', 'with', 'about', 'against', 'between', 'into', 'through', 'during', 'before', + 'after', 'above', 'below', 'to', 'from', 'up', 'down', 'in', 'out', 'on', 'off', 'over', 'under', 'again', + 'further', 'then', 'once', 'here', 'there', 'when', 'where', 'why', 'how', 'all', 'any', 'both', 'each', + 'few', 'more', 'most', 'other', 'some', 'such', 'no', 'nor', 'not', 'only', 'own', 'same', 'so', 'than', + 'too', 'very', 's', 't', 'can', 'will', 'just', 'don', 'should', 'now'] + +# Logging +logging.basicConfig(stream=sys.stdout, level=logging.INFO, + format='%(asctime)s - %(levelname)s\t%(message)s', + datefmt='(%Y-%m-%d %H:%M:%S)') +logger = logging + + +def log(msg): + """Logger decorator.""" + def decorator(func): + def wrapper(*args, **kwargs): + logger.info(msg) + return func(*args, **kwargs) + return wrapper + return decorator diff --git a/src/misc/__init__.py b/src/misc/__init__.py new file mode 100644 index 0000000..b82b5b1 --- /dev/null +++ b/src/misc/__init__.py @@ -0,0 +1,4 @@ +from .plots import scatterplot +from .text import stem_text_input, tfidf, get_vectorizer +from .decomposition import dim_reduction +from .search import search diff --git a/src/misc/decomposition.py b/src/misc/decomposition.py new file mode 100644 index 0000000..5d5a4be --- /dev/null +++ b/src/misc/decomposition.py @@ -0,0 +1,44 @@ +from sklearn.decomposition import TruncatedSVD, PCA +from sklearn.manifold import MDS, TSNE +from sklearn.metrics.pairwise import pairwise_distances +from ..config import log + + +@log('Decomposition started.') +def dim_reduction(X, decomp, metric): + """Dimension reduction and saving of the model.""" + if X.shape[1] > 2: + if decomp == 'pca': + return pca(X) + elif decomp == 'mds': + return mds(X, metric) + elif decomp == 'tsne': + return tsne(X, metric) + else: + return svd(X) + return X + + +@log('- Using: PCA.') +def pca(X): + return PCA().fit_transform(X.toarray()) + + +@log('- Using: SVD.') +def svd(X): + return TruncatedSVD().fit_transform(X) + + +@log('- Using: t-SNE') +def tsne(X, metric): + # reduce dimensions first + X = PCA(40).fit_transform(X.toarray()) + # then use tsne + return TSNE(metric=metric).fit_transform(X) + + +@log('- Using: MDS') +def mds(X, metric): + X = PCA(40).fit_transform(X.toarray()) + X = pairwise_distances(X, metric=metric) + return MDS(n_jobs=-1, dissimilarity='precomputed').fit_transform(X) diff --git a/clusterix/clustering/results/plots.py b/src/misc/plots.py similarity index 93% rename from clusterix/clustering/results/plots.py rename to src/misc/plots.py index d80dae3..a3d3c0a 100644 --- a/clusterix/clustering/results/plots.py +++ b/src/misc/plots.py @@ -1,11 +1,13 @@ from scipy.cluster.hierarchy import to_tree +from ..config import log +@log('Plot coordinates created.') def scatterplot(coords, labels, n_clusters, centroids=None): """Get a dictionary that will be converted to json, and contains all the scatterplot visualization data.""" nodes = [] - for cluster in xrange(n_clusters): - for i in xrange(len(labels)): + for cluster in range(n_clusters): + for i in range(len(labels)): # Add nodes if labels[i] == cluster: nodes.append({ @@ -24,11 +26,7 @@ def scatterplot(coords, labels, n_clusters, centroids=None): 'cluster': cluster, 'isCentroid': True }) - - return { - 'k_num': n_clusters, - 'nodes': nodes, - } + return {'k_num': n_clusters, 'nodes': nodes} class HClusterTree(object): diff --git a/src/misc/search.py b/src/misc/search.py new file mode 100644 index 0000000..fac8333 --- /dev/null +++ b/src/misc/search.py @@ -0,0 +1,2 @@ +def search(query): + pass diff --git a/src/misc/text.py b/src/misc/text.py new file mode 100644 index 0000000..a5b810e --- /dev/null +++ b/src/misc/text.py @@ -0,0 +1,87 @@ +from sklearn.feature_extraction.text import HashingVectorizer, TfidfVectorizer, CountVectorizer +from sklearn.externals.joblib import Parallel, delayed +from whoosh.analysis import StemmingAnalyzer, CharsetFilter +from whoosh.support.charset import accent_map +from ..config import logger, log, stopwords +from ..utils import load_df + +TOKEN_PATTERN = r'\b\w+\b' # Keeps single letter attrs +STEM_REGEX = r'\b[^\d\W]+\b' + +stop = lambda attrs: stopwords if attrs['stopwords'] == 'true' else None +norm = lambda attrs: attrs['norm'] if attrs['norm'] != 'none' else None +join_by_cluster = lambda cl, df, tc: ' '.join(df[df.cluster == cl][tc].apply(' '.join, axis=1).values) + +analyzer = StemmingAnalyzer(expression=STEM_REGEX, minsize=1) | CharsetFilter(accent_map) + + +def get_vectorizer(attrs): + """Returna a vectorizer with the user options.""" + stop_ = stop(attrs) + norm_ = norm(attrs) + features = int(attrs['featureNumber']) + vectorizers = { + 'hashing': HashingVectorizer(n_features=features, non_negative=True, norm=norm_, stop_words=stop_), + 'tfidf': TfidfVectorizer(max_features=features, norm=norm_, token_pattern=TOKEN_PATTERN, stop_words=stop_), + 'count': CountVectorizer(max_features=features, token_pattern=TOKEN_PATTERN, stop_words=stop_) + } + + logger.info('- Vectorizer created: {}'.format(attrs['vectorizer'])) + return vectorizers[attrs['vectorizer']] + + +@log('- TF-IDF scoring started.') +def tfidf(attrs, id_list=None): + """ + Returns the TF-IDF scores of words, separated by cluster in the form of: + { + '0':[ + {'term': 'x', 'score': y}, + {'term': 'x', 'score': y}, + ... + ], + '1': [...] + } + """ + df = load_df() + text_columns = list(df.select_dtypes(include=['object']).columns) + + # Only get tfidf in text attributes + if not text_columns: + return + + stop_ = stop(attrs) + norm_ = norm(attrs) + features = attrs['featureNumber'] + + # If an id list is given, get results for these items only + if id_list: + pass + + # Consider each cluster a single document, + # so get the text from each cluster separately + text_by_cluster = [join_by_cluster(cluster, df, text_columns) + for cluster in df.cluster.unique()] + + vec = TfidfVectorizer(max_features=features, norm=norm_, token_pattern=TOKEN_PATTERN, stop_words=stop_) + tfidf_matrix = vec.fit_transform(text_by_cluster).toarray() + feature_names = vec.get_feature_names() + + # Get the top 10 TF-IDF scores for each cluster + tfidf_by_cluster = {} + for cluster, doc in enumerate(tfidf_matrix): + max_10_indices = doc.argsort()[-10:][::-1] + tfidf_by_cluster[str(cluster)] = [{'term': feature_names[i], 'score': doc[i]} + for i in max_10_indices] + return tfidf_by_cluster + + +def stem_text_input(text): + """Stem the text corpus.""" + parallel = Parallel(n_jobs=-1, backend='multiprocessing', verbose=1) + return parallel(delayed(_stem)(row) for row in text) + + +def _stem(row): + """Process a single item, to be used in parallel.""" + return ' '.join([token.text for token in analyzer(row)]) diff --git a/src/routes/__init__.py b/src/routes/__init__.py new file mode 100644 index 0000000..01cc958 --- /dev/null +++ b/src/routes/__init__.py @@ -0,0 +1,2 @@ +from .input import main +from .results import results diff --git a/src/routes/input.py b/src/routes/input.py new file mode 100644 index 0000000..b8a2615 --- /dev/null +++ b/src/routes/input.py @@ -0,0 +1,81 @@ +from flask import Blueprint, render_template, request, jsonify +from .. import save_file, process_and_save_dataframe +from ..cluster import cluster_data, get_projection, predict_data +from ..misc import tfidf +from ..utils import get_attrs +from ..config import algorithms + +main = Blueprint('main', __name__) + + +############ +# TEMPLATING +############ +@main.route('/', methods=['POST', 'GET']) +def index(): + """Main page.""" + return render_template('index.html') + + +@main.route('/loading_screen', methods=['POST']) +def loading_screen(): + """Loading screen of Clusterix.""" + return render_template('loading.html') + + +@main.route('/algorithm_options', methods=['POST']) +def algorithm_options(): + """Render algorithm options.""" + return render_template(algorithms[request.form['algorithm']]) + + +################### +# PROCESSING NEEDED +################### +@main.route('/load_file', methods=['POST']) +def load_file(): + """ + Load and save the file as a pickled dataframe, to be used later. + Returns the rendered fields of the processing space. + """ + file = request.files['file'] + save_file(file) + + fields_with_types, has_text = process_and_save_dataframe(file.filename) + return jsonify(**{ + 'fields': render_template('processing/fields.html', fields=fields_with_types.keys()), + 'algorithms': render_template('processing/algorithms.html'), + 'textOptions': render_template('processing/text.html'), + 'hasText': has_text + }) + + +@main.route('/get_clustering_results', methods=['POST']) +def get_clustering_results(): + """ + Returns a dict with the plot coordinates of the clusted data, + and the TF-IDF scores, if we have text attributes. + """ + attrs = get_attrs(request) + return jsonify(**{ + 'cluster_results': cluster_data(attrs), + 'tfidf': tfidf(attrs) + }) + + +@main.route('/get_projection', methods=['POST']) +def get_projection_results(): + """ + Returns a dict with the plot coordinates of the clusted data, + and the TF-IDF scores, if we have text attributes. + """ + attrs = get_attrs(request) + return jsonify(**{'cluster_results': {'nodes': get_projection(attrs)}}) + + +@main.route('/predict_cluster', methods=['POST']) +def predict_cluster(): + """Predict the cluster of the imput row.""" + # attrs = get_attrs(request) + # return jsonify(**{'cluster_results': {'nodes': predict_data(attrs)}}) + pass diff --git a/src/routes/results.py b/src/routes/results.py new file mode 100644 index 0000000..4ca5b43 --- /dev/null +++ b/src/routes/results.py @@ -0,0 +1,10 @@ +from flask import Blueprint, jsonify +from ..misc import search + +results = Blueprint('results', __name__) + + +@results.route('/search', methods=['POST']) +def search_data(): + """Render algorithm options.""" + return jsonify(**{'nodes': search(query)}) diff --git a/clusterix/static/css/bootstrap.min.css b/src/static/css/bootstrap.min.css similarity index 100% rename from clusterix/static/css/bootstrap.min.css rename to src/static/css/bootstrap.min.css diff --git a/clusterix/static/css/clusterix.css b/src/static/css/clusterix.css similarity index 100% rename from clusterix/static/css/clusterix.css rename to src/static/css/clusterix.css diff --git a/clusterix/static/css/clusterix_colors.css b/src/static/css/clusterix_colors.css similarity index 100% rename from clusterix/static/css/clusterix_colors.css rename to src/static/css/clusterix_colors.css diff --git a/clusterix/static/css/fileinput.css b/src/static/css/fileinput.css similarity index 94% rename from clusterix/static/css/fileinput.css rename to src/static/css/fileinput.css index d91e501..75711cf 100644 --- a/clusterix/static/css/fileinput.css +++ b/src/static/css/fileinput.css @@ -1,253 +1,253 @@ -/*! - * @copyright Copyright © Kartik Visweswaran, Krajee.com, 2014 - 2015 - * @package bootstrap-fileinput - * @version 4.3.0 - * - * File input styling for Bootstrap 3.0 - * Built for Yii Framework 2.0 - * Author: Kartik Visweswaran - * Year: 2015 - * For more Yii related demos visit http://demos.krajee.com - */ -.file-loading { - top: 0; - right: 0; - width: 25px; - height: 25px; - font-size: 999px; - text-align: right; - color: #fff; - background: transparent url('../img/loading.gif') top left no-repeat; - border: none; -} - -.file-object { - margin: 0 0 -5px 0; - padding: 0; -} - -.btn-file { - position: relative; - overflow: hidden; -} - -.btn-file input[type=file] { - position: absolute; - top: 0; - right: 0; - min-width: 100%; - min-height: 100%; - text-align: right; - opacity: 0; - background: none repeat scroll 0 0 transparent; - cursor: inherit; - display: block; -} - -.file-caption-name { - display: inline-block; - overflow: hidden; - height: 20px; - word-break: break-all; -} - -.input-group-lg .file-caption-name { - height: 25px; -} - -.file-preview-detail-modal { - text-align: left; -} - -.file-error-message { - color: #a94442; - background-color: #f2dede; - margin: 5px; - border: 1px solid #ebccd1; - border-radius: 4px; - padding: 15px; -} - -.file-error-message pre, .file-error-message ul { - margin: 0; - text-align: left; -} - -.file-error-message pre { - margin: 5px 0; -} - -.file-caption-disabled { - background-color: #EEEEEE; - cursor: not-allowed; - opacity: 1; -} - -.file-preview { - border-radius: 5px; - border: 1px solid #ddd; - padding: 5px; - width: 100%; - margin-bottom: 5px; -} - -.file-preview-frame { - display: table; - margin: 8px; - height: 160px; - border: 1px solid #ddd; - box-shadow: 1px 1px 5px 0 #a2958a; - padding: 6px; - float: left; - text-align: center; - vertical-align: middle; -} - -.file-preview-frame:not(.file-preview-error):hover { - box-shadow: 3px 3px 5px 0 #333; -} - -.file-preview-image { - height: 160px; - vertical-align: middle; -} - -.file-preview-text { - text-align: left; - width: 160px; - margin-bottom: 2px; - color: #428bca; - background: #fff; - overflow-x: hidden; -} - -.file-preview-other { - display: table-cell; - text-align: center; - vertical-align: middle; - width: 160px; - height: 160px; - border: 2px solid #999; - border-radius: 30px; -} - -.file-preview-other:hover { - opacity: 0.8; -} - -.file-actions, .file-other-error { - text-align: left; -} - -.file-icon-lg { - font-size: 1.2em; -} - -.file-icon-2x { - font-size: 2.4em; -} - -.file-icon-4x { - font-size: 4.8em; -} - -.file-input-new .file-preview, .file-input-new .close, .file-input-new .glyphicon-file, -.file-input-new .fileinput-remove-button, .file-input-new .fileinput-upload-button, -.file-input-ajax-new .fileinput-remove-button, .file-input-ajax-new .fileinput-upload-button { - display: none; -} - -.file-thumb-loading { - background: transparent url('../img/loading.gif') no-repeat scroll center center content-box !important; -} - -.file-actions { - margin-top: 15px; -} - -.file-footer-buttons { - float: right; -} - -.file-upload-indicator { - padding-top: 2px; - cursor: default; - opacity: 0.8; - width: 60%; -} - -.file-upload-indicator:hover { - font-weight: bold; - opacity: 1; -} - -.file-footer-caption { - display: block; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - width: 160px; - text-align: center; - padding-top: 4px; - font-size: 11px; - color: #777; - margin: 5px auto 10px auto; -} - -.file-preview-error { - opacity: 0.65; - box-shadow: none; -} - -.file-preview-frame:not(.file-preview-error) .file-footer-caption:hover { - color: #000; -} - -.file-drop-zone { - border: 1px dashed #aaa; - border-radius: 4px; - height: 100%; - text-align: center; - vertical-align: middle; - margin: 12px 15px 12px 12px; - padding: 5px; -} - -.file-drop-zone-title { - color: #aaa; - font-size: 40px; - padding: 85px 10px; -} - -.file-highlighted { - border: 2px dashed #999 !important; - background-color: #f0f0f0; -} - -.file-uploading { - background: url('../img/loading-sm.gif') no-repeat center bottom 10px; - opacity: 0.65; -} - -.file-thumb-progress .progress, .file-thumb-progress .progress-bar { - height: 10px; - font-size: 9px; - line-height: 10px; -} - -.file-thumbnail-footer { - position: relative; -} - -.file-thumb-progress { - position: absolute; - top: 22px; - left: 0; - right: 0; -} - -/* IE 10 fix */ -.btn-file ::-ms-browse { - width:100%; - height:100%; +/*! + * @copyright Copyright © Kartik Visweswaran, Krajee.com, 2014 - 2015 + * @package bootstrap-fileinput + * @version 4.3.0 + * + * File input styling for Bootstrap 3.0 + * Built for Yii Framework 2.0 + * Author: Kartik Visweswaran + * Year: 2015 + * For more Yii related demos visit http://demos.krajee.com + */ +.file-loading { + top: 0; + right: 0; + width: 25px; + height: 25px; + font-size: 999px; + text-align: right; + color: #fff; + background: transparent url('../img/loading.gif') top left no-repeat; + border: none; +} + +.file-object { + margin: 0 0 -5px 0; + padding: 0; +} + +.btn-file { + position: relative; + overflow: hidden; +} + +.btn-file input[type=file] { + position: absolute; + top: 0; + right: 0; + min-width: 100%; + min-height: 100%; + text-align: right; + opacity: 0; + background: none repeat scroll 0 0 transparent; + cursor: inherit; + display: block; +} + +.file-caption-name { + display: inline-block; + overflow: hidden; + height: 20px; + word-break: break-all; +} + +.input-group-lg .file-caption-name { + height: 25px; +} + +.file-preview-detail-modal { + text-align: left; +} + +.file-error-message { + color: #a94442; + background-color: #f2dede; + margin: 5px; + border: 1px solid #ebccd1; + border-radius: 4px; + padding: 15px; +} + +.file-error-message pre, .file-error-message ul { + margin: 0; + text-align: left; +} + +.file-error-message pre { + margin: 5px 0; +} + +.file-caption-disabled { + background-color: #EEEEEE; + cursor: not-allowed; + opacity: 1; +} + +.file-preview { + border-radius: 5px; + border: 1px solid #ddd; + padding: 5px; + width: 100%; + margin-bottom: 5px; +} + +.file-preview-frame { + display: table; + margin: 8px; + height: 160px; + border: 1px solid #ddd; + box-shadow: 1px 1px 5px 0 #a2958a; + padding: 6px; + float: left; + text-align: center; + vertical-align: middle; +} + +.file-preview-frame:not(.file-preview-error):hover { + box-shadow: 3px 3px 5px 0 #333; +} + +.file-preview-image { + height: 160px; + vertical-align: middle; +} + +.file-preview-text { + text-align: left; + width: 160px; + margin-bottom: 2px; + color: #428bca; + background: #fff; + overflow-x: hidden; +} + +.file-preview-other { + display: table-cell; + text-align: center; + vertical-align: middle; + width: 160px; + height: 160px; + border: 2px solid #999; + border-radius: 30px; +} + +.file-preview-other:hover { + opacity: 0.8; +} + +.file-actions, .file-other-error { + text-align: left; +} + +.file-icon-lg { + font-size: 1.2em; +} + +.file-icon-2x { + font-size: 2.4em; +} + +.file-icon-4x { + font-size: 4.8em; +} + +.file-input-new .file-preview, .file-input-new .close, .file-input-new .glyphicon-file, +.file-input-new .fileinput-remove-button, .file-input-new .fileinput-upload-button, +.file-input-ajax-new .fileinput-remove-button, .file-input-ajax-new .fileinput-upload-button { + display: none; +} + +.file-thumb-loading { + background: transparent url('../img/loading.gif') no-repeat scroll center center content-box !important; +} + +.file-actions { + margin-top: 15px; +} + +.file-footer-buttons { + float: right; +} + +.file-upload-indicator { + padding-top: 2px; + cursor: default; + opacity: 0.8; + width: 60%; +} + +.file-upload-indicator:hover { + font-weight: bold; + opacity: 1; +} + +.file-footer-caption { + display: block; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + width: 160px; + text-align: center; + padding-top: 4px; + font-size: 11px; + color: #777; + margin: 5px auto 10px auto; +} + +.file-preview-error { + opacity: 0.65; + box-shadow: none; +} + +.file-preview-frame:not(.file-preview-error) .file-footer-caption:hover { + color: #000; +} + +.file-drop-zone { + border: 1px dashed #aaa; + border-radius: 4px; + height: 100%; + text-align: center; + vertical-align: middle; + margin: 12px 15px 12px 12px; + padding: 5px; +} + +.file-drop-zone-title { + color: #aaa; + font-size: 40px; + padding: 85px 10px; +} + +.file-highlighted { + border: 2px dashed #999 !important; + background-color: #f0f0f0; +} + +.file-uploading { + background: url('../img/loading-sm.gif') no-repeat center bottom 10px; + opacity: 0.65; +} + +.file-thumb-progress .progress, .file-thumb-progress .progress-bar { + height: 10px; + font-size: 9px; + line-height: 10px; +} + +.file-thumbnail-footer { + position: relative; +} + +.file-thumb-progress { + position: absolute; + top: 22px; + left: 0; + right: 0; +} + +/* IE 10 fix */ +.btn-file ::-ms-browse { + width:100%; + height:100%; } \ No newline at end of file diff --git a/clusterix/static/fonts/glyphicons-halflings-regular.eot b/src/static/fonts/glyphicons-halflings-regular.eot similarity index 100% rename from clusterix/static/fonts/glyphicons-halflings-regular.eot rename to src/static/fonts/glyphicons-halflings-regular.eot diff --git a/clusterix/static/fonts/glyphicons-halflings-regular.svg b/src/static/fonts/glyphicons-halflings-regular.svg similarity index 100% rename from clusterix/static/fonts/glyphicons-halflings-regular.svg rename to src/static/fonts/glyphicons-halflings-regular.svg diff --git a/clusterix/static/fonts/glyphicons-halflings-regular.ttf b/src/static/fonts/glyphicons-halflings-regular.ttf similarity index 100% rename from clusterix/static/fonts/glyphicons-halflings-regular.ttf rename to src/static/fonts/glyphicons-halflings-regular.ttf diff --git a/clusterix/static/fonts/glyphicons-halflings-regular.woff b/src/static/fonts/glyphicons-halflings-regular.woff similarity index 100% rename from clusterix/static/fonts/glyphicons-halflings-regular.woff rename to src/static/fonts/glyphicons-halflings-regular.woff diff --git a/clusterix/static/fonts/glyphicons-halflings-regular.woff2 b/src/static/fonts/glyphicons-halflings-regular.woff2 similarity index 100% rename from clusterix/static/fonts/glyphicons-halflings-regular.woff2 rename to src/static/fonts/glyphicons-halflings-regular.woff2 diff --git a/clusterix/static/img/clear.png b/src/static/img/clear.png similarity index 100% rename from clusterix/static/img/clear.png rename to src/static/img/clear.png diff --git a/clusterix/static/img/clusterix.svg b/src/static/img/clusterix.svg similarity index 98% rename from clusterix/static/img/clusterix.svg rename to src/static/img/clusterix.svg index c9122fa..3050bdb 100644 --- a/clusterix/static/img/clusterix.svg +++ b/src/static/img/clusterix.svg @@ -1,329 +1,329 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/clusterix/static/img/clusterix_bg.svg b/src/static/img/clusterix_bg.svg similarity index 98% rename from clusterix/static/img/clusterix_bg.svg rename to src/static/img/clusterix_bg.svg index f8ced6b..b298338 100644 --- a/clusterix/static/img/clusterix_bg.svg +++ b/src/static/img/clusterix_bg.svg @@ -1,155 +1,155 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/clusterix/static/img/inspirelabs.svg b/src/static/img/inspirelabs.svg similarity index 98% rename from clusterix/static/img/inspirelabs.svg rename to src/static/img/inspirelabs.svg index e8b2e95..57159bc 100644 --- a/clusterix/static/img/inspirelabs.svg +++ b/src/static/img/inspirelabs.svg @@ -1,18 +1,18 @@ - - - - - - - - - - - + + + + + + + + + + + diff --git a/clusterix/static/img/loading.gif b/src/static/img/loading.gif similarity index 100% rename from clusterix/static/img/loading.gif rename to src/static/img/loading.gif diff --git a/clusterix/static/js/diagram/distribution_chart.js b/src/static/js/diagram/distribution_chart.js similarity index 100% rename from clusterix/static/js/diagram/distribution_chart.js rename to src/static/js/diagram/distribution_chart.js diff --git a/clusterix/static/js/diagram/flat_treemap/FlatTreemap.js b/src/static/js/diagram/flat_treemap/FlatTreemap.js similarity index 100% rename from clusterix/static/js/diagram/flat_treemap/FlatTreemap.js rename to src/static/js/diagram/flat_treemap/FlatTreemap.js diff --git a/clusterix/static/js/diagram/scatterplot/cluster_details.js b/src/static/js/diagram/scatterplot/cluster_details.js similarity index 100% rename from clusterix/static/js/diagram/scatterplot/cluster_details.js rename to src/static/js/diagram/scatterplot/cluster_details.js diff --git a/clusterix/static/js/diagram/scatterplot/scatterplot.js b/src/static/js/diagram/scatterplot/scatterplot.js similarity index 93% rename from clusterix/static/js/diagram/scatterplot/scatterplot.js rename to src/static/js/diagram/scatterplot/scatterplot.js index 9d7e4ae..558cb99 100644 --- a/clusterix/static/js/diagram/scatterplot/scatterplot.js +++ b/src/static/js/diagram/scatterplot/scatterplot.js @@ -1,7 +1,6 @@ function Scatterplot() { var attr = { - d3Color: d3.scale.category20(), brushColor: d3.scale.category20(), grey: '#7f8c8d' }; @@ -46,7 +45,7 @@ function Scatterplot() { var selectedItemsList = Array.from(brushSelectedIDs); new ClusterDetailZone().init(selectedItemsList, - '#selections-area ul', brushID, attr.d3Color); + '#selections-area ul', brushID, window.d3Color); } }; @@ -117,9 +116,9 @@ function Scatterplot() { .attr('class', function(d) { return d.isCentroid ? 'centroid centroid-' + d.cluster : 'node node-' + d.id; }) - .attr("fill", function (d) { return attr.d3Color(d.cluster); }) + .attr("fill", function (d) { return window.d3Color(d.cluster); }) .attr('fillbackup', function (d) { - d.fillbackup = attr.d3Color(d.cluster); + d.fillbackup = window.d3Color(d.cluster); return d.fillbackup; }) .attr('transform', function(d) { return translate(xScale(d.x), yScale(d.y)); }); diff --git a/clusterix/static/js/diagram/scatterplot/scatterplot_mini.js b/src/static/js/diagram/scatterplot/scatterplot_mini.js similarity index 100% rename from clusterix/static/js/diagram/scatterplot/scatterplot_mini.js rename to src/static/js/diagram/scatterplot/scatterplot_mini.js diff --git a/clusterix/static/js/diagram/term_frequency_chart.js b/src/static/js/diagram/term_frequency_chart.js similarity index 81% rename from clusterix/static/js/diagram/term_frequency_chart.js rename to src/static/js/diagram/term_frequency_chart.js index 0d1d844..5965b00 100644 --- a/clusterix/static/js/diagram/term_frequency_chart.js +++ b/src/static/js/diagram/term_frequency_chart.js @@ -7,19 +7,14 @@ var TermFrequencyChart = (function () { }; return { - createTFIDFChart: function (clustersWithColors, clustersWithWords/*, tfidf, color*/) { - Utils.compileTemplate('#tf-idf-template', '#tf-idf-results', - {clusters: clustersWithColors}, true); - - - for (var cluster in clustersWithWords) { - var tfidfItems = clustersWithWords[cluster]; - - var selector = '#tf-idf-cluster-' + cluster; + createTFIDFChart: function (tfidfByCluster) { + for (var cluster in tfidfByCluster) { + var tfidfItems = tfidfByCluster[cluster], + selector = '#tf-idf-cluster-' + cluster; var x = d3.scale.linear() .domain([0, d3.max(tfidfItems, function (d) { - return d.tfidf; + return d.score; })]) .range([0, options.block_width]); diff --git a/clusterix/static/js/diagram/treemap/treemap.js b/src/static/js/diagram/treemap/treemap.js similarity index 100% rename from clusterix/static/js/diagram/treemap/treemap.js rename to src/static/js/diagram/treemap/treemap.js diff --git a/clusterix/static/js/diagram/treemap/treemap_mini.js b/src/static/js/diagram/treemap/treemap_mini.js similarity index 100% rename from clusterix/static/js/diagram/treemap/treemap_mini.js rename to src/static/js/diagram/treemap/treemap_mini.js diff --git a/src/static/js/init.js b/src/static/js/init.js new file mode 100644 index 0000000..f113fa2 --- /dev/null +++ b/src/static/js/init.js @@ -0,0 +1,64 @@ +$(function() { + window.d3Color= d3.scale.category20(); + + // Data input + $('#data-input') + .fileinput({ + maxFileCount:1, allowedFileExtensions: ['csv'], showPreview: false, showRemove: false, + uploadClass: 'btn btn-default', uploadLabel: 'Preview', + uploadIcon: ' ', + layoutTemplates: { + main1: "{preview}
" + + "
{browse}" + + "{upload}{remove}
{caption}
" + } + }) + .on('change', function() { + // Checking for stuff and create the data file + if (!this.files[0]) return; + + var formData = new FormData(); + formData.append('file', this.files[0]); + + // Load the file and render the input boxes. + $.ajax({type: 'POST', url: '/load_file', data: formData, + cache: false, contentType: false, processData: false, + success: function(data){ + initFields(data); + Router.init(); + } + }); + }); +}); + + +function initFields(data) { + // FIELDS + $('#fields-panel').html(data['fields']).fadeIn(); + $('#multiple-fields-csv').dropdown(); + $('#decomposition-selection').dropdown(); + $('#decomposition-metric-selection').dropdown(); + + // ALGORITHMS + $('#algorithms-panel').html(data['algorithms']).fadeIn(); + $('#algorithms-selection').dropdown({ + onChange: function(val) { + $.ajax({type: 'POST', url: '/algorithm_options', data: {'algorithm': val}, dataType:'text', + success: function (data) { $('#algorithm-options').html(data); } + }); + } + }); + + // TEXT + if (data['hasText']) { + $('#text-options-panel').html(data['textOptions']).fadeIn(); + $('#vectorizer-selection').dropdown(); + $('#norm-selection').dropdown(); + + $('.checkbox').checkbox(); + } + + // UPLOAD BUTTON + $('#get-results').fadeIn(200); + $('#projection-results').fadeIn(200); +} \ No newline at end of file diff --git a/src/static/js/loading.js b/src/static/js/loading.js new file mode 100644 index 0000000..bfc42fc --- /dev/null +++ b/src/static/js/loading.js @@ -0,0 +1,61 @@ +/** + * Initializes the loading screen. + */ +function initLoadingScreen() { + $('#loading-screen') + .on('click', function(){ return false; }) + .dimmer('show'); + + var svg = d3.select('#clusterix_loader svg'); + var width = svg.attr('width'), + height = svg.attr('height'), + data = []; + + svg.selectAll("circle").each(function (d, i) { + var circle_data = { + 'or': d3.select(this).attr('r'), + 'ocx': d3.select(this).attr('cx'), + 'ocy': d3.select(this).attr('cy'), + 'fill': d3.select(this).style('fill') + }; + d3.select(this).remove(); + data.push(circle_data); + }); + + var circles = svg.selectAll("circle") + .data(data) + .enter() + .append("circle") + .style('fill', '#9eacad') + .attr('cx', function (d, i) { + // we start the X positions either at the beginning of the canvas, or end. + var start_position = Math.round(Math.random()) == 0 ? 0 : width; + return start_position == 0 ? start_position - (20) : start_position + 20; + }) + .attr('cy', function (d, i) { return Math.random() * 3000 % height; }) + .attr('r', 0); + + circles.transition() + .ease('linear') + .duration(function (d, i) { return (Math.random() * i) * 200; }) + .delay(function (d, i) { return i * (Math.random() * 50); }) + .attr('cx', function (d) { return d.ocx; }) + .attr('cy', function (d) { return d.ocy; }) + .attr('r', function (d) { return d.or; }) + + .style('opacity', 1) + .each("end", function (d, i) { + d3.select(this) + .transition('elastic') + .duration(500) + .delay(function (d, i) { return i * (Math.random() * 400); }) + .style("fill", d.fill); + }); +} + +/** + * Hides the loading screen from the DOM. + */ +function removeLoadingScreen() { + $('#loading-screen').dimmer('hide'); +} \ No newline at end of file diff --git a/src/static/js/render/diagram_renderer.js b/src/static/js/render/diagram_renderer.js new file mode 100644 index 0000000..49efb98 --- /dev/null +++ b/src/static/js/render/diagram_renderer.js @@ -0,0 +1,44 @@ +var Renderer = (function() { + var sizeMap = {width: 900, height: 400}; + var sizeMini = {width: 200, height: 100}; + + var selectors = { + diagramSelector: '#vizualization-area', + infoSelector: '#selections-area ul', + miniSelector: '#viz-mini', + resultsSelector: '#brush-results', + distributionSelector: '#cluster-comparison', + tfidfSelector: '#tf-idf-results' + }; + + var idNumber = 0; + + function getNewId() { + idNumber++; + return idNumber; + } + + return { + + get_selector: function(selector) { return selectors[selector]; }, + + /** + * Renders the returned data. + * @param data + */ + render: function(data) { + var id = getNewId(); + + // Empty the div, we only want a single scatterplot + // Also empty the brushed content + $(selectors.diagramSelector).empty(); + $(selectors.infoSelector).empty(); + $(selectors.tfidfSelector).empty(); + $(selectors.distributionSelector).empty(); + + new Scatterplot().init(data['cluster_results'], sizeMap.width, sizeMap.height, selectors.diagramSelector, id); + // new ScatterplotMini().init(data, sizeMini.width, sizeMini.height, selectors.miniSelector, id); + } + } + +})(); \ No newline at end of file diff --git a/src/static/js/router.js b/src/static/js/router.js new file mode 100644 index 0000000..3085ed1 --- /dev/null +++ b/src/static/js/router.js @@ -0,0 +1,82 @@ +var Router = (function() { + var defaultData = { + fields: [], + sampleFraction: '1', + decomposition: 'svd', + distanceMetric: 'euclidean', + vectorizer: 'count', + featureNumber: '500', + stemming: 'true', + stopwords: 'true', + norm: 'none', + algorithm: 'kmeans', + kNumber: 1 + }; + + function getData() { + var data = { + // GENERAL + fields: valOrDefault('#multiple-fields-csv', 'fields'), + sampleFraction: valOrDefault('#sample-fraction', 'sampleFraction'), + + // DECOMPOSITION + decomposition: valOrDefault('#decomposition-selection', 'decomposition'), + distanceMetric: valOrDefault('#decomposition-metric-selection', 'distanceMetric'), + + // TEXT + vectorizer: valOrDefault('#vectorizer-selection', 'vectorizer'), + featureNumber: valOrDefault('#feature-num', 'featureNumber'), + stemming: 'true', + stopwords: 'true', + norm: valOrDefault('#norm-selection', 'norm'), + + // ALGORITHMS + algorithm: valOrDefault('#algorithms-selection', 'algorithm'), + kNumber: valOrDefault('#kmeans-num', 'kNumber') + }; + var formData = new FormData(); + formData.append('data', JSON.stringify(data)); + return formData + } + + function valOrDefault(id, key) { + var value = $(id).val(); + if (value == null || value == "" || value == undefined) { + return defaultData[key]; + } + return value; + } + + return { + + /** + * Uploads the attributes and returns the result. + */ + init: function() { + console.log('Router init'); + $('#get-results').on('click', function () { + var formData = getData(); + $.ajax({type: 'POST', url: '/get_clustering_results', data: formData, + cache: false, contentType: false, processData: false, + success: function(data){ + Renderer.render(data); + removeLoadingScreen(); + } + }); + initLoadingScreen(); + }); + + $('#projection-results').on('click', function () { + var formData = getData(); + $.ajax({type: 'POST', url: '/get_projection', data: formData, + cache: false, contentType: false, processData: false, + success: function(data){ + Renderer.render(data); + removeLoadingScreen(); + } + }); + initLoadingScreen(); + }); + } + } +})(); \ No newline at end of file diff --git a/clusterix/static/js/search.js b/src/static/js/search.js similarity index 100% rename from clusterix/static/js/search.js rename to src/static/js/search.js diff --git a/clusterix/static/lib/bootstrap.js b/src/static/lib/bootstrap.js similarity index 100% rename from clusterix/static/lib/bootstrap.js rename to src/static/lib/bootstrap.js diff --git a/clusterix/static/lib/d3-tip.js b/src/static/lib/d3-tip.js similarity index 100% rename from clusterix/static/lib/d3-tip.js rename to src/static/lib/d3-tip.js diff --git a/clusterix/static/lib/d3.min.js b/src/static/lib/d3.min.js similarity index 100% rename from clusterix/static/lib/d3.min.js rename to src/static/lib/d3.min.js diff --git a/clusterix/static/lib/fileinput.js b/src/static/lib/fileinput.js similarity index 97% rename from clusterix/static/lib/fileinput.js rename to src/static/lib/fileinput.js index 56678a4..902040a 100644 --- a/clusterix/static/lib/fileinput.js +++ b/src/static/lib/fileinput.js @@ -1,2722 +1,2722 @@ -/*! - * @copyright Copyright © Kartik Visweswaran, Krajee.com, 2014 - 2015 - * @version 4.3.0 - * - * File input styled for Bootstrap 3.0 that utilizes HTML5 File Input's advanced features including the FileReader API. - * - * The plugin drastically enhances the HTML file input to preview multiple files on the client before upload. In - * addition it provides the ability to preview content of images, text, videos, audio, html, flash and other objects. - * It also offers the ability to upload and delete files using AJAX, and add files in batches (i.e. preview, append, - * or remove before upload). - * - * Author: Kartik Visweswaran - * Copyright: 2015, Kartik Visweswaran, Krajee.com - * For more JQuery plugins visit http://plugins.krajee.com - * For more Yii related demos visit http://demos.krajee.com - */ -(function (factory) { - "use strict"; - if (typeof define === 'function' && define.amd) { // jshint ignore:line - // AMD. Register as an anonymous module. - define(['jquery'], factory); // jshint ignore:line - } else { // noinspection JSUnresolvedVariable - if (typeof module === 'object' && module.exports) { // jshint ignore:line - // Node/CommonJS - // noinspection JSUnresolvedVariable - module.exports = factory(require('jquery')); // jshint ignore:line - } else { - // Browser globals - factory(window.jQuery); - } - } -}(function ($) { - "use strict"; - - $.fn.fileinputLocales = {}; - - var isIE, isEdge, handler, previewCache, getNum, hasFileAPISupport, hasDragDropSupport, hasFileUploadSupport, addCss, - STYLE_SETTING, OBJECT_PARAMS, DEFAULT_PREVIEW, defaultFileActionSettings, tMain1, tMain2, tPreview, tIcon, tClose, - tCaption, tBtnDefault, tBtnLink, tBtnBrowse, tModal, tProgress, tFooter, tActions, tActionDelete, tActionUpload, - tZoom, tGeneric, tHtml, tImage, tText, tVideo, tAudio, tFlash, tObject, tOther, defaultLayoutTemplates, - defaultPreviewTemplates, defaultPreviewTypes, defaultPreviewSettings, defaultFileTypeSettings, isEmpty, isArray, - isSet, getElement, uniqId, htmlEncode, replaceTags, objUrl, FileInput, NAMESPACE; - - NAMESPACE = '.fileinput'; - isIE = function (ver) { - // check for IE versions < 11 - if (navigator.appName !== 'Microsoft Internet Explorer') { - return false; - } - if (ver === 10) { - return new RegExp('msie\\s' + ver, 'i').test(navigator.userAgent); - } - var div = document.createElement("div"), status; - div.innerHTML = ""; - status = div.getElementsByTagName("i").length; - document.body.appendChild(div); - div.parentNode.removeChild(div); - return status; - }; - isEdge = function () { - return new RegExp('Edge\/[0-9]+', 'i').test(navigator.userAgent); - }; - handler = function ($el, event, callback, skipNS) { - var ev = skipNS ? event : event.split(' ').join(NAMESPACE + ' ') + NAMESPACE; - $el.off(ev).on(ev, callback); - }; - previewCache = { - data: {}, - init: function (obj) { - var content = obj.initialPreview, id = obj.id; - if (content.length > 0 && !isArray(content)) { - content = content.split(obj.initialPreviewDelimiter); - } - previewCache.data[id] = { - content: content, - config: obj.initialPreviewConfig, - tags: obj.initialPreviewThumbTags, - delimiter: obj.initialPreviewDelimiter, - template: obj.previewGenericTemplate, - msg: function (n) { - return obj.getMsgSelected(n); - }, - initId: obj.previewInitId, - footer: obj.getLayoutTemplate('footer').replace(/\{progress}/g, obj.renderThumbProgress()), - isDelete: obj.initialPreviewShowDelete, - caption: obj.initialCaption, - actions: function (showUpload, showDelete, disabled, url, key) { - return obj.renderFileActions(showUpload, showDelete, disabled, url, key); - } - }; - }, - fetch: function (id) { - return previewCache.data[id].content.filter(function (n) { - return n !== null; - }); - }, - count: function (id, all) { - return !!previewCache.data[id] && !!previewCache.data[id].content ? - (all ? previewCache.data[id].content.length : previewCache.fetch(id).length) : 0; - }, - get: function (id, i, isDisabled) { - var ind = 'init_' + i, data = previewCache.data[id], config = data.config[i], - previewId = data.initId + '-' + ind, out, $tmp, frameClass = ' file-preview-initial'; - /** @namespace config.frameClass */ - /** @namespace config.frameAttr */ - isDisabled = isDisabled === undefined ? true : isDisabled; - if (data.content[i] === null) { - return ''; - } - if (!isEmpty(config) && !isEmpty(config.frameClass)) { - frameClass += ' ' + config.frameClass; - } - out = data.template - .replace(/\{previewId}/g, previewId) - .replace(/\{frameClass}/g, frameClass) - .replace(/\{fileindex}/g, ind) - .replace(/\{content}/g, data.content[i]) - .replace(/\{footer}/g, previewCache.footer(id, i, isDisabled)); - if (data.tags.length && data.tags[i]) { - out = replaceTags(out, data.tags[i]); - } - if (!isEmpty(config) && !isEmpty(config.frameAttr)) { - $tmp = $(document.createElement('div')).html(out); - $tmp.find('.file-preview-initial').attr(config.frameAttr); - out = $tmp.html(); - $tmp.remove(); - } - return out; - }, - add: function (id, content, config, tags, append) { - var data = $.extend(true, {}, previewCache.data[id]), index; - if (!isArray(content)) { - content = content.split(data.delimiter); - } - if (append) { - index = data.content.push(content) - 1; - data.config[index] = config; - data.tags[index] = tags; - } else { - index = content.length; - data.content = content; - data.config = config; - data.tags = tags; - } - previewCache.data[id] = data; - return index; - }, - set: function (id, content, config, tags, append) { - var data = $.extend(true, {}, previewCache.data[id]), i, chk; - if (!content || !content.length) { - return; - } - if (!isArray(content)) { - content = content.split(data.delimiter); - } - chk = content.filter(function (n) { - return n !== null; - }); - if (!chk.length) { - return; - } - if (data.content === undefined) { - data.content = []; - } - if (data.config === undefined) { - data.config = []; - } - if (data.tags === undefined) { - data.tags = []; - } - if (append) { - for (i = 0; i < content.length; i++) { - if (content[i]) { - data.content.push(content[i]); - } - } - for (i = 0; i < config.length; i++) { - if (config[i]) { - data.config.push(config[i]); - } - } - for (i = 0; i < tags.length; i++) { - if (tags[i]) { - data.tags.push(tags[i]); - } - } - } else { - data.content = content; - data.config = config; - data.tags = tags; - } - previewCache.data[id] = data; - }, - unset: function (id, index) { - var chk = previewCache.count(id); - if (!chk) { - return; - } - if (chk === 1) { - previewCache.data[id].content = []; - previewCache.data[id].config = []; - return; - } - previewCache.data[id].content[index] = null; - previewCache.data[id].config[index] = null; - }, - out: function (id) { - var html = '', data = previewCache.data[id], caption, len = previewCache.count(id, true); - if (len === 0) { - return {content: '', caption: ''}; - } - for (var i = 0; i < len; i++) { - html += previewCache.get(id, i); - } - caption = data.msg(previewCache.count(id)); - return {content: html, caption: caption}; - }, - footer: function (id, i, isDisabled) { - var data = previewCache.data[id]; - isDisabled = isDisabled === undefined ? true : isDisabled; - if (data.config.length === 0 || isEmpty(data.config[i])) { - return ''; - } - var config = data.config[i], - caption = isSet('caption', config) ? config.caption : '', - width = isSet('width', config) ? config.width : 'auto', - url = isSet('url', config) ? config.url : false, - key = isSet('key', config) ? config.key : null, - disabled = (url === false) && isDisabled, - actions = data.isDelete ? data.actions(false, true, disabled, url, key) : '', - footer = data.footer.replace(/\{actions}/g, actions); - return footer.replace(/\{caption}/g, caption) - .replace(/\{width}/g, width) - .replace(/\{indicator}/g, '') - .replace(/\{indicatorTitle}/g, ''); - } - }; - getNum = function (num, def) { - def = def || 0; - if (typeof num === "number") { - return num; - } - if (typeof num === "string") { - num = parseFloat(num); - } - return isNaN(num) ? def : num; - }; - hasFileAPISupport = function () { - return !!(window.File && window.FileReader); - }; - hasDragDropSupport = function () { - var div = document.createElement('div'); - /** @namespace div.draggable */ - /** @namespace div.ondragstart */ - /** @namespace div.ondrop */ - return !isIE(9) && !isEdge() && // Fix for MS Edge drag & drop support bug - (div.draggable !== undefined || (div.ondragstart !== undefined && div.ondrop !== undefined)); - }; - hasFileUploadSupport = function () { - return hasFileAPISupport() && window.FormData; - }; - addCss = function ($el, css) { - $el.removeClass(css).addClass(css); - }; - STYLE_SETTING = 'style="width:{width};height:{height};"'; - OBJECT_PARAMS = ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n'; - DEFAULT_PREVIEW = '
\n' + - ' {previewFileIcon}\n' + - '
'; - defaultFileActionSettings = { - removeIcon: '', - removeClass: 'btn btn-xs btn-default', - removeTitle: 'Remove file', - uploadIcon: '', - uploadClass: 'btn btn-xs btn-default', - uploadTitle: 'Upload file', - indicatorNew: '', - indicatorSuccess: '', - indicatorError: '', - indicatorLoading: '', - indicatorNewTitle: 'Not uploaded yet', - indicatorSuccessTitle: 'Uploaded', - indicatorErrorTitle: 'Upload Error', - indicatorLoadingTitle: 'Uploading ...' - }; - tMain1 = '{preview}\n' + - '
\n' + - '
\n' + - ' {caption}\n' + - '
\n' + - ' {remove}\n' + - ' {cancel}\n' + - ' {upload}\n' + - ' {browse}\n' + - '
\n' + - '
'; - tMain2 = '{preview}\n
\n{remove}\n{cancel}\n{upload}\n{browse}\n'; - tPreview = '
\n' + - ' {close}' + - '
\n' + - '
\n' + - '
\n' + - '
' + - '
\n' + - '
\n' + - '
\n' + - '
'; - tClose = '
×
\n'; - tIcon = ''; - tCaption = '
\n' + - '
\n' + - '
\n'; - //noinspection HtmlUnknownAttribute - tBtnDefault = ''; - tBtnLink = '{icon}{label}'; - tBtnBrowse = '
{icon}{label}
'; - tModal = ''; - tProgress = '
\n' + - '
\n' + - ' {percent}%\n' + - '
\n' + - '
'; - tFooter = ''; - tActions = '
\n' + - ' \n' + - '
{indicator}
\n' + - '
\n' + - '
'; - tActionDelete = '\n'; - tActionUpload = '\n'; - tZoom = '\n'; - tGeneric = '
\n' + - ' {content}\n' + - ' {footer}\n' + - '
\n'; - tHtml = '
\n' + - ' \n' + - ' ' + DEFAULT_PREVIEW + '\n' + - ' \n' + - ' {footer}\n' + - '
'; - tImage = '
\n' + - ' {caption}\n' + - ' {footer}\n' + - '
\n'; - tText = '
\n' + - '
{data}
\n' + - ' {zoom}\n' + - ' {footer}\n' + - '
'; - tVideo = '
\n' + - ' \n' + - ' {footer}\n' + - '
\n'; - tAudio = '
\n' + - ' \n' + - ' {footer}\n' + - '
'; - tFlash = '
\n' + - ' \n' + - OBJECT_PARAMS + ' ' + DEFAULT_PREVIEW + '\n' + - ' \n' + - ' {footer}\n' + - '
\n'; - tObject = '
\n' + - ' \n' + - ' \n' + - OBJECT_PARAMS + ' ' + DEFAULT_PREVIEW + '\n' + - ' \n' + - ' {footer}\n' + - '
'; - tOther = '
\n' + - '
\n' + - ' ' + DEFAULT_PREVIEW + '\n' + - '
\n' + - ' \n' + - '
'; - defaultLayoutTemplates = { - main1: tMain1, - main2: tMain2, - preview: tPreview, - close: tClose, - zoom: tZoom, - icon: tIcon, - caption: tCaption, - modal: tModal, - progress: tProgress, - footer: tFooter, - actions: tActions, - actionDelete: tActionDelete, - actionUpload: tActionUpload, - btnDefault: tBtnDefault, - btnLink: tBtnLink, - btnBrowse: tBtnBrowse - }; - defaultPreviewTemplates = { - generic: tGeneric, - html: tHtml, - image: tImage, - text: tText, - video: tVideo, - audio: tAudio, - flash: tFlash, - object: tObject, - other: tOther - }; - defaultPreviewTypes = ['image', 'html', 'text', 'video', 'audio', 'flash', 'object']; - defaultPreviewSettings = { - image: {width: "auto", height: "160px"}, - html: {width: "213px", height: "160px"}, - text: {width: "160px", height: "136px"}, - video: {width: "213px", height: "160px"}, - audio: {width: "213px", height: "80px"}, - flash: {width: "213px", height: "160px"}, - object: {width: "160px", height: "160px"}, - other: {width: "160px", height: "160px"} - }; - defaultFileTypeSettings = { - image: function (vType, vName) { - return (vType !== undefined) ? vType.match('image.*') : vName.match(/\.(gif|png|jpe?g)$/i); - }, - html: function (vType, vName) { - return (vType !== undefined) ? vType === 'text/html' : vName.match(/\.(htm|html)$/i); - }, - text: function (vType, vName) { - return (vType !== undefined && vType.match('text.*')) || vName.match( - /\.(txt|md|csv|nfo|ini|json|php|js|css)$/i); - }, - video: function (vType, vName) { - return (vType !== undefined && vType.match(/\.video\/(ogg|mp4|webm|3gp)$/i)) || vName.match( - /\.(og?|mp4|webm|3gp)$/i); - }, - audio: function (vType, vName) { - return (vType !== undefined && vType.match(/\.audio\/(ogg|mp3|wav)$/i)) || vName.match(/\.(ogg|mp3|wav)$/i); - }, - flash: function (vType, vName) { - return (vType !== undefined && vType === 'application/x-shockwave-flash') || vName.match(/\.(swf)$/i); - }, - object: function () { - return true; - }, - other: function () { - return true; - } - }; - isEmpty = function (value, trim) { - return value === undefined || value === null || value.length === 0 || (trim && $.trim(value) === ''); - }; - isArray = function (a) { - return Array.isArray(a) || Object.prototype.toString.call(a) === '[object Array]'; - }; - isSet = function (needle, haystack) { - return (typeof haystack === 'object' && needle in haystack); - }; - getElement = function (options, param, value) { - return (isEmpty(options) || isEmpty(options[param])) ? value : $(options[param]); - }; - uniqId = function () { - return Math.round(new Date().getTime() + (Math.random() * 100)); - }; - htmlEncode = function (str) { - return str.replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); - }; - replaceTags = function (str, tags) { - var out = str; - if (!tags) { - return out; - } - $.each(tags, function (key, value) { - if (typeof value === "function") { - value = value(); - } - out = out.split(key).join(value); - }); - return out; - }; - //noinspection JSUnresolvedVariable - objUrl = window.URL || window.webkitURL; - FileInput = function (element, options) { - var self = this; - self.$element = $(element); - if (!self.validate()) { - return; - } - self.isPreviewable = hasFileAPISupport(); - self.isIE9 = isIE(9); - self.isIE10 = isIE(10); - if (self.isPreviewable || self.isIE9) { - self.init(options); - self.listen(); - } else { - self.$element.removeClass('file-loading'); - } - }; - - FileInput.prototype = { - constructor: FileInput, - validate: function () { - var self = this, $exception; - if (self.$element.attr('type') === 'file') { - return true; - } - $exception = '
' + - '

Invalid Input Type

' + - 'You must set an input type = file for bootstrap-fileinput plugin to initialize.' + - '
'; - self.$element.after($exception); - return false; - }, - init: function (options) { - var self = this, $el = self.$element, t; - $.each(options, function (key, value) { - switch (key) { - case 'minFileCount': - case 'maxFileCount': - case 'maxFileSize': - self[key] = getNum(value); - break; - default: - self[key] = value; - break; - } - }); - self.fileInputCleared = false; - self.fileBatchCompleted = true; - if (!self.isPreviewable) { - self.showPreview = false; - } - self.uploadFileAttr = !isEmpty($el.attr('name')) ? $el.attr('name') : 'file_data'; - self.reader = null; - self.formdata = {}; - self.clearStack(); - self.uploadCount = 0; - self.uploadStatus = {}; - self.uploadLog = []; - self.uploadAsyncCount = 0; - self.loadedImages = []; - self.totalImagesCount = 0; - self.ajaxRequests = []; - self.isError = false; - self.ajaxAborted = false; - self.cancelling = false; - t = self.getLayoutTemplate('progress'); - self.progressTemplate = t.replace('{class}', self.progressClass); - self.progressCompleteTemplate = t.replace('{class}', self.progressCompleteClass); - self.dropZoneEnabled = hasDragDropSupport() && self.dropZoneEnabled; - self.isDisabled = self.$element.attr('disabled') || self.$element.attr('readonly'); - self.isUploadable = hasFileUploadSupport() && !isEmpty(self.uploadUrl); - self.slug = typeof options.slugCallback === "function" ? options.slugCallback : self.slugDefault; - self.mainTemplate = self.showCaption ? self.getLayoutTemplate('main1') : self.getLayoutTemplate('main2'); - self.captionTemplate = self.getLayoutTemplate('caption'); - self.previewGenericTemplate = self.getPreviewTemplate('generic'); - if (self.resizeImage && (self.maxImageWidth || self.maxImageHeight)) { - self.imageCanvas = document.createElement('canvas'); - self.imageCanvasContext = self.imageCanvas.getContext('2d'); - } - if (isEmpty(self.$element.attr('id'))) { - self.$element.attr('id', uniqId()); - } - if (self.$container === undefined) { - self.$container = self.createContainer(); - } else { - self.refreshContainer(); - } - self.$dropZone = self.$container.find('.file-drop-zone'); - self.$progress = self.$container.find('.kv-upload-progress'); - self.$btnUpload = self.$container.find('.fileinput-upload'); - self.$captionContainer = getElement(options, 'elCaptionContainer', self.$container.find('.file-caption')); - self.$caption = getElement(options, 'elCaptionText', self.$container.find('.file-caption-name')); - self.$previewContainer = getElement(options, 'elPreviewContainer', self.$container.find('.file-preview')); - self.$preview = getElement(options, 'elPreviewImage', self.$container.find('.file-preview-thumbnails')); - self.$previewStatus = getElement(options, 'elPreviewStatus', self.$container.find('.file-preview-status')); - self.$errorContainer = getElement(options, 'elErrorContainer', - self.$previewContainer.find('.kv-fileinput-error')); - if (!isEmpty(self.msgErrorClass)) { - addCss(self.$errorContainer, self.msgErrorClass); - } - self.$errorContainer.hide(); - self.fileActionSettings = $.extend(true, defaultFileActionSettings, options.fileActionSettings); - self.previewInitId = "preview-" + uniqId(); - self.id = self.$element.attr('id'); - previewCache.init(self); - self.initPreview(true); - self.initPreviewDeletes(); - self.options = options; - self.setFileDropZoneTitle(); - self.$element.removeClass('file-loading'); - if (self.$element.attr('disabled')) { - self.disable(); - } - }, - parseError: function (jqXHR, errorThrown, fileName) { - /** @namespace jqXHR.responseJSON */ - var self = this, errMsg = $.trim(errorThrown + ''), - dot = errMsg.slice(-1) === '.' ? '' : '.', - text = jqXHR.responseJSON !== undefined && jqXHR.responseJSON.error !== undefined ? - jqXHR.responseJSON.error : jqXHR.responseText; - if (self.cancelling && self.msgUploadAborted) { - errMsg = self.msgUploadAborted; - } - if (self.showAjaxErrorDetails && text) { - text = $.trim(text.replace(/\n\s*\n/g, '\n')); - text = text.length > 0 ? '
' + text + '
' : ''; - errMsg += dot + text; - } else { - errMsg += dot; - } - self.cancelling = false; - return fileName ? '' + fileName + ': ' + errMsg : errMsg; - }, - raise: function (event, params) { - var self = this, e = $.Event(event); - if (params !== undefined) { - self.$element.trigger(e, params); - } else { - self.$element.trigger(e); - } - if (e.isDefaultPrevented()) { - return false; - } - if (!e.result) { - return e.result; - } - switch (event) { - // ignore these events - case 'filebatchuploadcomplete': - case 'filebatchuploadsuccess': - case 'fileuploaded': - case 'fileclear': - case 'filecleared': - case 'filereset': - case 'fileerror': - case 'filefoldererror': - case 'fileuploaderror': - case 'filebatchuploaderror': - case 'filedeleteerror': - case 'filecustomerror': - case 'filesuccessremove': - break; - // receive data response via `filecustomerror` event` - default: - self.ajaxAborted = e.result; - break; - } - return true; - }, - getLayoutTemplate: function (t) { - var self = this, - template = isSet(t, self.layoutTemplates) ? self.layoutTemplates[t] : defaultLayoutTemplates[t]; - if (isEmpty(self.customLayoutTags)) { - return template; - } - return replaceTags(template, self.customLayoutTags); - }, - getPreviewTemplate: function (t) { - var self = this, - template = isSet(t, self.previewTemplates) ? self.previewTemplates[t] : defaultPreviewTemplates[t]; - if (isEmpty(self.customPreviewTags)) { - return template; - } - return replaceTags(template, self.customPreviewTags); - }, - parseFilePreviewIcon: function (content, fname) { - var self = this, ext, icn = self.previewFileIcon; - if (fname && fname.indexOf('.') > -1) { - ext = fname.split('.').pop(); - if (self.previewFileIconSettings && self.previewFileIconSettings[ext]) { - icn = self.previewFileIconSettings[ext]; - } - if (self.previewFileExtSettings) { - $.each(self.previewFileExtSettings, function (key, func) { - if (self.previewFileIconSettings[key] && func(ext)) { - icn = self.previewFileIconSettings[key]; - } - }); - } - } - if (content.indexOf('{previewFileIcon}') > -1) { - return content.replace(/\{previewFileIconClass}/g, self.previewFileIconClass).replace( - /\{previewFileIcon}/g, icn); - } - return content; - }, - getOutData: function (jqXHR, responseData, filesData) { - var self = this; - jqXHR = jqXHR || {}; - responseData = responseData || {}; - filesData = filesData || self.filestack.slice(0) || {}; - return { - form: self.formdata, - files: filesData, - filenames: self.filenames, - extra: self.getExtraData(), - response: responseData, - reader: self.reader, - jqXHR: jqXHR - }; - }, - listen: function () { - var self = this, $el = self.$element, $form = $el.closest('form'), $cont = self.$container; - handler($el, 'change', $.proxy(self.change, self)); - handler(self.$btnFile, 'click', $.proxy(self.browse, self)); - handler($form, 'reset', $.proxy(self.reset, self)); - handler($cont.find('.fileinput-remove:not([disabled])'), 'click', $.proxy(self.clear, self)); - handler($cont.find('.fileinput-cancel'), 'click', $.proxy(self.cancel, self)); - self.initDragDrop(); - if (!self.isUploadable) { - handler($form, 'submit', $.proxy(self.submitForm, self)); - } - handler(self.$container.find('.fileinput-upload'), 'click', $.proxy(self.uploadClick, self)); - }, - zoneDragDropInit: function (e) { - e.stopPropagation(); - e.preventDefault(); - }, - zoneDragEnter: function (e) { - var self = this, hasFiles = $.inArray('Files', e.originalEvent.dataTransfer.types) > -1; - self.zoneDragDropInit(e); - if (self.isDisabled || !hasFiles) { - e.originalEvent.dataTransfer.effectAllowed = 'none'; - e.originalEvent.dataTransfer.dropEffect = 'none'; - return; - } - addCss(self.$dropZone, 'file-highlighted'); - }, - zoneDragLeave: function (e) { - var self = this; - self.zoneDragDropInit(e); - if (self.isDisabled) { - return; - } - self.$dropZone.removeClass('file-highlighted'); - }, - zoneDrop: function (e) { - var self = this; - e.preventDefault(); - /** @namespace e.originalEvent.dataTransfer */ - if (self.isDisabled || isEmpty(e.originalEvent.dataTransfer.files)) { - return; - } - self.change(e, 'dragdrop'); - self.$dropZone.removeClass('file-highlighted'); - }, - initDragDrop: function () { - var self = this, $zone = self.$dropZone; - if (self.isUploadable && self.dropZoneEnabled && self.showPreview) { - handler($zone, 'dragenter dragover', $.proxy(self.zoneDragEnter, self)); - handler($zone, 'dragleave', $.proxy(self.zoneDragLeave, self)); - handler($zone, 'drop', $.proxy(self.zoneDrop, self)); - handler($(document), 'dragenter dragover drop', self.zoneDragDropInit); - } - }, - browse: function (e) { - var self = this; - self.raise('filebrowse'); - if (e && e.isDefaultPrevented()) { - return; - } - if (self.isError && !self.isUploadable) { - self.clear(); - } - self.$captionContainer.focus(); - }, - uploadClick: function (e) { - var self = this, $btn = self.$container.find('.fileinput-upload'), $form, - isEnabled = !$btn.hasClass('disabled') && isEmpty($btn.attr('disabled')); - if (e && e.isDefaultPrevented()) { - return; - } - if (!self.isUploadable) { - if (isEnabled && $btn.attr('type') !== 'submit') { - $form = $btn.closest('form'); - // downgrade to normal form submit if possible - if ($form.length) { - $form.trigger('submit'); - } - e.preventDefault(); - } - return; - } - e.preventDefault(); - if (isEnabled) { - self.upload(); - } - }, - submitForm: function () { - var self = this, $el = self.$element, files = $el.get(0).files; - if (files && self.minFileCount > 0 && self.getFileCount(files.length) < self.minFileCount) { - self.noFilesError({}); - return false; - } - return !self.abort({}); - }, - abort: function (params) { - var self = this, data; - if (self.ajaxAborted && typeof self.ajaxAborted === "object" && self.ajaxAborted.message !== undefined) { - data = $.extend(true, {}, self.getOutData(), params); - data.abortData = self.ajaxAborted.data || {}; - data.abortMessage = self.ajaxAborted.message; - self.cancel(); - self.setProgress(100); - self.showUploadError(self.ajaxAborted.message, data, 'filecustomerror'); - return true; - } - return false; - }, - noFilesError: function (params) { - var self = this, label = self.minFileCount > 1 ? self.filePlural : self.fileSingle, - msg = self.msgFilesTooLess.replace('{n}', self.minFileCount).replace('{files}', label), - $error = self.$errorContainer; - self.addError(msg); - self.isError = true; - self.updateFileDetails(0); - $error.fadeIn(800); - self.raise('fileerror', [params]); - self.clearFileInput(); - addCss(self.$container, 'has-error'); - }, - setProgress: function (p, $el) { - var self = this, pct = Math.min(p, 100), - template = pct < 100 ? self.progressTemplate : self.progressCompleteTemplate; - $el = $el || self.$progress; - if (!isEmpty(template)) { - $el.html(template.replace(/\{percent}/g, pct)); - } - }, - lock: function () { - var self = this; - self.resetErrors(); - self.disable(); - if (self.showRemove) { - addCss(self.$container.find('.fileinput-remove'), 'hide'); - } - if (self.showCancel) { - self.$container.find('.fileinput-cancel').removeClass('hide'); - } - self.raise('filelock', [self.filestack, self.getExtraData()]); - }, - unlock: function (reset) { - var self = this; - if (reset === undefined) { - reset = true; - } - self.enable(); - if (self.showCancel) { - addCss(self.$container.find('.fileinput-cancel'), 'hide'); - } - if (self.showRemove) { - self.$container.find('.fileinput-remove').removeClass('hide'); - } - if (reset) { - self.resetFileStack(); - } - self.raise('fileunlock', [self.filestack, self.getExtraData()]); - }, - resetFileStack: function () { - var self = this, i = 0, newstack = [], newnames = []; - self.getThumbs().each(function () { - var $thumb = $(this), ind = $thumb.attr('data-fileindex'), - file = self.filestack[ind]; - if (ind === -1) { - return; - } - if (file !== undefined) { - newstack[i] = file; - newnames[i] = self.getFileName(file); - $thumb.attr({ - 'id': self.previewInitId + '-' + i, - 'data-fileindex': i - }); - i++; - } else { - $thumb.attr({ - 'id': 'uploaded-' + uniqId(), - 'data-fileindex': '-1' - }); - } - }); - self.filestack = newstack; - self.filenames = newnames; - }, - destroy: function () { - var self = this, $cont = self.$container; - $cont.find('.file-drop-zone').off(); - self.$element.insertBefore($cont).off(NAMESPACE).removeData(); - $cont.off().remove(); - }, - refresh: function (options) { - var self = this, $el = self.$element; - options = options ? $.extend(true, {}, self.options, options) : self.options; - self.destroy(); - $el.fileinput(options); - if ($el.val()) { - $el.trigger('change.fileinput'); - } - }, - setFileDropZoneTitle: function () { - var self = this, $zone = self.$container.find('.file-drop-zone'); - $zone.find('.' + self.dropZoneTitleClass).remove(); - if (!self.isUploadable || !self.showPreview || $zone.length === 0 || self.getFileStack().length > 0 || !self.dropZoneEnabled) { - return; - } - if ($zone.find('.file-preview-frame').length === 0 && isEmpty(self.defaultPreviewContent)) { - $zone.prepend('
' + self.dropZoneTitle + '
'); - } - self.$container.removeClass('file-input-new'); - addCss(self.$container, 'file-input-ajax-new'); - }, - errorsExist: function () { - var self = this, $err; - if (self.$errorContainer.find('li').length) { - return true; - } - $err = $(document.createElement('div')).html(self.$errorContainer.html()); - $err.find('span.kv-error-close').remove(); - $err.find('ul').remove(); - return $.trim($err.text()).length ? true : false; - }, - getMsgSelected: function (n) { - var self = this, strFiles = n === 1 ? self.fileSingle : self.filePlural; - return self.msgSelected.replace('{n}', n).replace('{files}', strFiles); - }, - renderThumbProgress: function () { - return '
' + this.progressTemplate.replace(/\{percent}/g, - '0') + '
'; - }, - renderFileFooter: function (caption, width) { - var self = this, config = self.fileActionSettings, footer, out, template = self.getLayoutTemplate('footer'); - if (self.isUploadable) { - footer = template.replace(/\{actions}/g, self.renderFileActions(true, true, false, false, false)); - out = footer.replace(/\{caption}/g, caption) - .replace(/\{width}/g, width) - .replace(/\{progress}/g, self.renderThumbProgress()) - .replace(/\{indicator}/g, config.indicatorNew) - .replace(/\{indicatorTitle}/g, config.indicatorNewTitle); - } else { - out = template.replace(/\{actions}/g, '') - .replace(/\{caption}/g, caption) - .replace(/\{progress}/g, '') - .replace(/\{width}/g, width) - .replace(/\{indicator}/g, '') - .replace(/\{indicatorTitle}/g, ''); - } - out = replaceTags(out, self.previewThumbTags); - return out; - }, - renderFileActions: function (showUpload, showDelete, disabled, url, key) { - if (!showUpload && !showDelete) { - return ''; - } - var self = this, - vUrl = url === false ? '' : ' data-url="' + url + '"', - vKey = key === false ? '' : ' data-key="' + key + '"', - btnDelete = self.getLayoutTemplate('actionDelete'), - btnUpload = '', - template = self.getLayoutTemplate('actions'), - otherButtons = self.otherActionButtons.replace(/\{dataKey}/g, vKey), - config = self.fileActionSettings, - removeClass = disabled ? config.removeClass + ' disabled' : config.removeClass; - btnDelete = btnDelete - .replace(/\{removeClass}/g, removeClass) - .replace(/\{removeIcon}/g, config.removeIcon) - .replace(/\{removeTitle}/g, config.removeTitle) - .replace(/\{dataUrl}/g, vUrl) - .replace(/\{dataKey}/g, vKey); - if (showUpload) { - btnUpload = self.getLayoutTemplate('actionUpload') - .replace(/\{uploadClass}/g, config.uploadClass) - .replace(/\{uploadIcon}/g, config.uploadIcon) - .replace(/\{uploadTitle}/g, config.uploadTitle); - } - return template - .replace(/\{delete}/g, btnDelete) - .replace(/\{upload}/g, btnUpload) - .replace(/\{other}/g, otherButtons); - }, - setThumbStatus: function ($thumb, status) { - var self = this; - if (!self.showPreview) { - return; - } - var icon = 'indicator' + status, msg = icon + 'Title', - css = 'file-preview-' + status.toLowerCase(), - $indicator = $thumb.find('.file-upload-indicator'), - config = self.fileActionSettings; - $thumb.removeClass('file-preview-success file-preview-error file-preview-loading'); - if (status === 'Error') { - $thumb.find('.kv-file-upload').attr('disabled', true); - } - $indicator.html(config[icon]); - $indicator.attr('title', config[msg]); - $thumb.addClass(css); - }, - clearPreview: function () { - var self = this, $thumbs = !self.showUploadedThumbs ? self.$preview.find('.file-preview-frame') : - self.$preview.find('.file-preview-frame:not(.file-preview-success)'); - $thumbs.remove(); - if (!self.$preview.find('.file-preview-frame').length || !self.showPreview) { - self.resetUpload(); - } - self.validateDefaultPreview(); - }, - initPreview: function (isInit) { - var self = this, cap = self.initialCaption || '', out; - if (!previewCache.count(self.id)) { - self.clearPreview(); - if (isInit) { - self.setCaption(cap); - } else { - self.initCaption(); - } - return; - } - out = previewCache.out(self.id); - cap = isInit && self.initialCaption ? self.initialCaption : out.caption; - self.$preview.html(out.content); - self.setCaption(cap); - if (!isEmpty(out.content)) { - self.$container.removeClass('file-input-new'); - } - }, - initPreviewDeletes: function () { - var self = this, deleteExtraData = self.deleteExtraData || {}, - resetProgress = function () { - var hasFiles = self.isUploadable ? previewCache.count(self.id) : self.$element.get(0).files.length; - if (self.$preview.find('.kv-file-remove').length === 0 && !hasFiles) { - self.reset(); - self.initialCaption = ''; - } - }; - - self.$preview.find('.kv-file-remove').each(function () { - var $el = $(this), vUrl = $el.data('url') || self.deleteUrl, vKey = $el.data('key'); - if (isEmpty(vUrl) || vKey === undefined) { - return; - } - var $frame = $el.closest('.file-preview-frame'), cache = previewCache.data[self.id], - settings, params, index = $frame.data('fileindex'), config, extraData; - index = parseInt(index.replace('init_', '')); - config = isEmpty(cache.config) && isEmpty(cache.config[index]) ? null : cache.config[index]; - extraData = isEmpty(config) || isEmpty(config.extra) ? deleteExtraData : config.extra; - if (typeof extraData === "function") { - extraData = extraData(); - } - params = {id: $el.attr('id'), key: vKey, extra: extraData}; - settings = $.extend(true, {}, { - url: vUrl, - type: 'POST', - dataType: 'json', - data: $.extend(true, {}, {key: vKey}, extraData), - beforeSend: function (jqXHR) { - self.ajaxAborted = false; - self.raise('filepredelete', [vKey, jqXHR, extraData]); - if (self.ajaxAborted) { - jqXHR.abort(); - } else { - addCss($frame, 'file-uploading'); - addCss($el, 'disabled'); - } - }, - success: function (data, textStatus, jqXHR) { - var n, cap; - if (isEmpty(data) || isEmpty(data.error)) { - previewCache.unset(self.id, index); - n = previewCache.count(self.id); - cap = n > 0 ? self.getMsgSelected(n) : ''; - self.raise('filedeleted', [vKey, jqXHR, extraData]); - self.setCaption(cap); - } else { - params.jqXHR = jqXHR; - params.response = data; - self.showError(data.error, params, 'filedeleteerror'); - $frame.removeClass('file-uploading'); - $el.removeClass('disabled'); - resetProgress(); - return; - } - $frame.removeClass('file-uploading').addClass('file-deleted'); - $frame.fadeOut('slow', function () { - self.clearObjects($frame); - $frame.remove(); - resetProgress(); - if (!n && self.getFileStack().length === 0) { - self.setCaption(''); - self.reset(); - } - }); - }, - error: function (jqXHR, textStatus, errorThrown) { - var errMsg = self.parseError(jqXHR, errorThrown); - params.jqXHR = jqXHR; - params.response = {}; - self.showError(errMsg, params, 'filedeleteerror'); - $frame.removeClass('file-uploading'); - resetProgress(); - } - }, self.ajaxDeleteSettings); - handler($el, 'click', function () { - if (!self.validateMinCount()) { - return false; - } - $.ajax(settings); - }); - }); - }, - clearObjects: function ($el) { - $el.find('video audio').each(function () { - this.pause(); - $(this).remove(); - }); - $el.find('img object div').each(function () { - $(this).remove(); - }); - }, - clearFileInput: function () { - var self = this, $el = self.$element, $srcFrm, $tmpFrm, $tmpEl; - if (isEmpty($el.val())) { - return; - } - // Fix for IE ver < 11, that does not clear file input - // Requires a sequence of steps to prevent IE crashing but - // still allow clearing of the file input. - if (self.isIE9 || self.isIE10) { - $srcFrm = $el.closest('form'); - $tmpFrm = $(document.createElement('form')); - $tmpEl = $(document.createElement('div')); - $el.before($tmpEl); - if ($srcFrm.length) { - $srcFrm.after($tmpFrm); - } else { - $tmpEl.after($tmpFrm); - } - $tmpFrm.append($el).trigger('reset'); - $tmpEl.before($el).remove(); - $tmpFrm.remove(); - } else { // normal input clear behavior for other sane browsers - $el.val(''); - } - self.fileInputCleared = true; - }, - resetUpload: function () { - var self = this; - self.uploadCache = {content: [], config: [], tags: [], append: true}; - self.uploadCount = 0; - self.uploadStatus = {}; - self.uploadLog = []; - self.uploadAsyncCount = 0; - self.loadedImages = []; - self.totalImagesCount = 0; - self.$btnUpload.removeAttr('disabled'); - self.setProgress(0); - addCss(self.$progress, 'hide'); - self.resetErrors(false); - self.ajaxAborted = false; - self.ajaxRequests = []; - self.resetCanvas(); - }, - resetCanvas: function () { - var self = this; - if (self.canvas && self.imageCanvasContext) { - self.imageCanvasContext.clearRect(0, 0, self.canvas.width, self.canvas.height); - } - }, - cancel: function () { - var self = this, xhr = self.ajaxRequests, len = xhr.length, i; - if (len > 0) { - for (i = 0; i < len; i += 1) { - self.cancelling = true; - xhr[i].abort(); - } - } - self.getThumbs().each(function () { - var $thumb = $(this), ind = $thumb.attr('data-fileindex'); - $thumb.removeClass('file-uploading'); - if (self.filestack[ind] !== undefined) { - $thumb.find('.kv-file-upload').removeClass('disabled').removeAttr('disabled'); - $thumb.find('.kv-file-remove').removeClass('disabled').removeAttr('disabled'); - } - self.unlock(); - }); - }, - cleanMemory: function ($thumb) { - var data = $thumb.is('img') ? $thumb.attr('src') : $thumb.find('source').attr('src'); - /** @namespace objUrl.revokeObjectURL */ - objUrl.revokeObjectURL(data); - }, - hasInitialPreview: function () { - var self = this; - return !self.overwriteInitial && previewCache.count(self.id); - }, - clear: function () { - var self = this, cap; - self.$btnUpload.removeAttr('disabled'); - self.getThumbs().find('video,audio,img').each(function () { - self.cleanMemory($(this)); - }); - self.resetUpload(); - self.clearStack(); - self.clearFileInput(); - self.resetErrors(true); - self.raise('fileclear'); - if (self.hasInitialPreview()) { - self.showFileIcon(); - self.resetPreview(); - self.initPreviewDeletes(); - self.$container.removeClass('file-input-new'); - } else { - self.getThumbs().each(function () { - self.clearObjects($(this)); - }); - if (self.isUploadable) { - previewCache.data[self.id] = {}; - } - self.$preview.html(''); - cap = (!self.overwriteInitial && self.initialCaption.length > 0) ? self.initialCaption : ''; - self.setCaption(cap); - self.$caption.attr('title', ''); - addCss(self.$container, 'file-input-new'); - self.validateDefaultPreview(); - } - if (self.$container.find('.file-preview-frame').length === 0) { - if (!self.initCaption()) { - self.$captionContainer.find('.kv-caption-icon').hide(); - } - } - self.hideFileIcon(); - self.raise('filecleared'); - self.$captionContainer.focus(); - self.setFileDropZoneTitle(); - }, - resetPreview: function () { - var self = this, out, cap; - if (previewCache.count(self.id)) { - out = previewCache.out(self.id); - self.$preview.html(out.content); - cap = self.initialCaption ? self.initialCaption : out.caption; - self.setCaption(cap); - } else { - self.clearPreview(); - self.initCaption(); - } - }, - clearDefaultPreview: function () { - var self = this; - self.$preview.find('.file-default-preview').remove(); - }, - validateDefaultPreview: function () { - var self = this; - if (!self.showPreview || isEmpty(self.defaultPreviewContent)) { - return; - } - self.$preview.html('
' + self.defaultPreviewContent + '
'); - self.$container.removeClass('file-input-new'); - }, - resetPreviewThumbs: function (isAjax) { - var self = this, out; - if (isAjax) { - self.clearPreview(); - self.clearStack(); - return; - } - if (self.hasInitialPreview()) { - out = previewCache.out(self.id); - self.$preview.html(out.content); - self.setCaption(out.caption); - self.initPreviewDeletes(); - } else { - self.clearPreview(); - } - }, - reset: function () { - var self = this; - self.resetPreview(); - self.$container.find('.fileinput-filename').text(''); - self.raise('filereset'); - addCss(self.$container, 'file-input-new'); - if (self.$preview.find('.file-preview-frame').length || self.isUploadable && self.dropZoneEnabled) { - self.$container.removeClass('file-input-new'); - } - self.setFileDropZoneTitle(); - self.clearStack(); - self.formdata = {}; - }, - disable: function () { - var self = this; - self.isDisabled = true; - self.raise('filedisabled'); - self.$element.attr('disabled', 'disabled'); - self.$container.find(".kv-fileinput-caption").addClass("file-caption-disabled"); - self.$container.find(".btn-file, .fileinput-remove, .fileinput-upload, .file-preview-frame button").attr( - "disabled", - true); - self.initDragDrop(); - }, - enable: function () { - var self = this; - self.isDisabled = false; - self.raise('fileenabled'); - self.$element.removeAttr('disabled'); - self.$container.find(".kv-fileinput-caption").removeClass("file-caption-disabled"); - self.$container.find( - ".btn-file, .fileinput-remove, .fileinput-upload, .file-preview-frame button").removeAttr("disabled"); - self.initDragDrop(); - }, - getThumbs: function (css) { - css = css || ''; - return this.$preview.find('.file-preview-frame:not(.file-preview-initial)' + css); - }, - getExtraData: function (previewId, index) { - var self = this, data = self.uploadExtraData; - if (typeof self.uploadExtraData === "function") { - data = self.uploadExtraData(previewId, index); - } - return data; - }, - uploadExtra: function (previewId, index) { - var self = this, data = self.getExtraData(previewId, index); - if (data.length === 0) { - return; - } - $.each(data, function (key, value) { - self.formdata.append(key, value); - }); - }, - setAsyncUploadStatus: function (previewId, pct, total) { - var self = this, sum = 0; - self.setProgress(pct, $('#' + previewId).find('.file-thumb-progress')); - self.uploadStatus[previewId] = pct; - $.each(self.uploadStatus, function (key, value) { - sum += value; - }); - self.setProgress(Math.ceil(sum / total)); - - }, - initXhr: function (xhrobj, previewId, fileCount) { - var self = this; - if (xhrobj.upload) { - xhrobj.upload.addEventListener('progress', function (event) { - var pct = 0, position = event.loaded || event.position, total = event.total; - /** @namespace event.lengthComputable */ - if (event.lengthComputable) { - pct = Math.ceil(position / total * 100); - } - if (previewId) { - self.setAsyncUploadStatus(previewId, pct, fileCount); - } else { - self.setProgress(Math.ceil(pct)); - } - }, false); - } - return xhrobj; - }, - ajaxSubmit: function (fnBefore, fnSuccess, fnComplete, fnError, previewId, index) { - var self = this, settings; - self.raise('filepreajax', [previewId, index]); - self.uploadExtra(previewId, index); - settings = $.extend(true, {}, { - xhr: function () { - var xhrobj = $.ajaxSettings.xhr(); - return self.initXhr(xhrobj, previewId, self.getFileStack().length); - }, - url: self.uploadUrl, - type: 'POST', - dataType: 'json', - data: self.formdata, - cache: false, - processData: false, - contentType: false, - beforeSend: fnBefore, - success: fnSuccess, - complete: fnComplete, - error: fnError - }, self.ajaxSettings); - self.ajaxRequests.push($.ajax(settings)); - }, - initUploadSuccess: function (out, $thumb, allFiles) { - var self = this, append, data, index, $newThumb, content, config, tags, i; - if (!self.showPreview || typeof out !== 'object' || $.isEmptyObject(out)) { - return; - } - if (out.initialPreview !== undefined && out.initialPreview.length > 0) { - self.hasInitData = true; - content = out.initialPreview || []; - config = out.initialPreviewConfig || []; - tags = out.initialPreviewThumbTags || []; - append = out.append === undefined || out.append ? true : false; - self.overwriteInitial = false; - if ($thumb !== undefined) { - if (!allFiles) { - index = previewCache.add(self.id, content, config[0], tags[0], append); - data = previewCache.get(self.id, index, false); - $newThumb = $(data).hide(); - $thumb.after($newThumb).fadeOut('slow', function () { - $newThumb.fadeIn('slow').css('display:inline-block'); - self.initPreviewDeletes(); - self.clearFileInput(); - $thumb.remove(); - }); - } else { - i = $thumb.attr('data-fileindex'); - self.uploadCache.content[i] = content[0]; - self.uploadCache.config[i] = config[0]; - self.uploadCache.tags[i] = tags[0]; - self.uploadCache.append = append; - } - } else { - previewCache.set(self.id, content, config, tags, append); - self.initPreview(); - self.initPreviewDeletes(); - } - } - }, - initSuccessThumbs: function () { - var self = this; - if (!self.showPreview) { - return; - } - self.getThumbs('.file-preview-success').each(function () { - var $thumb = $(this), $remove = $thumb.find('.kv-file-remove'); - $remove.removeAttr('disabled'); - handler($remove, 'click', function () { - var out = self.raise('filesuccessremove', [$thumb.attr('id'), $thumb.data('fileindex')]); - self.cleanMemory($thumb); - if (out === false) { - return; - } - $thumb.fadeOut('slow', function () { - $thumb.remove(); - if (!self.$preview.find('.file-preview-frame').length) { - self.reset(); - } - }); - }); - }); - }, - checkAsyncComplete: function () { - var self = this, previewId, i; - for (i = 0; i < self.filestack.length; i++) { - if (self.filestack[i]) { - previewId = self.previewInitId + "-" + i; - if ($.inArray(previewId, self.uploadLog) === -1) { - return false; - } - } - } - return (self.uploadAsyncCount === self.uploadLog.length); - }, - uploadSingle: function (i, files, allFiles) { - var self = this, total = self.getFileStack().length, formdata = new FormData(), outData, - previewId = self.previewInitId + "-" + i, $thumb, chkComplete, $btnUpload, $btnDelete, - hasPostData = self.filestack.length > 0 || !$.isEmptyObject(self.uploadExtraData), - fnBefore, fnSuccess, fnComplete, fnError, updateUploadLog, params = {id: previewId, index: i}; - self.formdata = formdata; - if (self.showPreview) { - $thumb = $('#' + previewId + ':not(.file-preview-initial)'); - $btnUpload = $thumb.find('.kv-file-upload'); - $btnDelete = $thumb.find('.kv-file-remove'); - $('#' + previewId).find('.file-thumb-progress').removeClass('hide'); - } - if (total === 0 || !hasPostData || ($btnUpload && $btnUpload.hasClass('disabled')) || self.abort(params)) { - return; - } - updateUploadLog = function (i, previewId) { - self.updateStack(i, undefined); - self.uploadLog.push(previewId); - if (self.checkAsyncComplete()) { - self.fileBatchCompleted = true; - } - }; - chkComplete = function () { - if (!self.fileBatchCompleted) { - return; - } - setTimeout(function () { - if (self.showPreview) { - previewCache.set( - self.id, - self.uploadCache.content, - self.uploadCache.config, - self.uploadCache.tags, - self.uploadCache.append - ); - if (self.hasInitData) { - self.initPreview(); - self.initPreviewDeletes(); - } - } - self.unlock(); - self.clearFileInput(); - self.raise('filebatchuploadcomplete', [self.filestack, self.getExtraData()]); - self.uploadCount = 0; - self.uploadStatus = {}; - self.uploadLog = []; - self.setProgress(100); - }, 100); - }; - fnBefore = function (jqXHR) { - outData = self.getOutData(jqXHR); - self.fileBatchCompleted = false; - if (self.showPreview) { - if (!$thumb.hasClass('file-preview-success')) { - self.setThumbStatus($thumb, 'Loading'); - addCss($thumb, 'file-uploading'); - } - $btnUpload.attr('disabled', true); - $btnDelete.attr('disabled', true); - } - if (!allFiles) { - self.lock(); - } - self.raise('filepreupload', [outData, previewId, i]); - $.extend(true, params, outData); - if (self.abort(params)) { - jqXHR.abort(); - self.setProgress(100); - } - }; - fnSuccess = function (data, textStatus, jqXHR) { - outData = self.getOutData(jqXHR, data); - $.extend(true, params, outData); - setTimeout(function () { - if (isEmpty(data) || isEmpty(data.error)) { - if (self.showPreview) { - self.setThumbStatus($thumb, 'Success'); - $btnUpload.hide(); - self.initUploadSuccess(data, $thumb, allFiles); - } - self.raise('fileuploaded', [outData, previewId, i]); - if (!allFiles) { - self.updateStack(i, undefined); - } else { - updateUploadLog(i, previewId); - } - } else { - self.showUploadError(data.error, params); - self.setPreviewError($thumb, i); - if (allFiles) { - updateUploadLog(i, previewId); - } - } - }, 100); - }; - fnComplete = function () { - setTimeout(function () { - if (self.showPreview) { - $btnUpload.removeAttr('disabled'); - $btnDelete.removeAttr('disabled'); - $thumb.removeClass('file-uploading'); - } - if (!allFiles) { - self.unlock(false); - self.clearFileInput(); - } else { - chkComplete(); - } - self.initSuccessThumbs(); - }, 100); - }; - fnError = function (jqXHR, textStatus, errorThrown) { - var errMsg = self.parseError(jqXHR, errorThrown, (allFiles ? files[i].name : null)); - setTimeout(function () { - if (allFiles) { - updateUploadLog(i, previewId); - } - self.uploadStatus[previewId] = 100; - self.setPreviewError($thumb, i); - $.extend(true, params, self.getOutData(jqXHR)); - self.showUploadError(errMsg, params); - }, 100); - }; - formdata.append(self.uploadFileAttr, files[i], self.filenames[i]); - formdata.append('file_id', i); - self.ajaxSubmit(fnBefore, fnSuccess, fnComplete, fnError, previewId, i); - }, - uploadBatch: function () { - var self = this, files = self.filestack, total = files.length, params = {}, fnBefore, fnSuccess, fnError, - fnComplete, hasPostData = self.filestack.length > 0 || !$.isEmptyObject(self.uploadExtraData), - setAllUploaded; - self.formdata = new FormData(); - if (total === 0 || !hasPostData || self.abort(params)) { - return; - } - setAllUploaded = function () { - $.each(files, function (key) { - self.updateStack(key, undefined); - }); - self.clearFileInput(); - }; - fnBefore = function (jqXHR) { - self.lock(); - var outData = self.getOutData(jqXHR); - if (self.showPreview) { - self.getThumbs().each(function () { - var $thumb = $(this), $btnUpload = $thumb.find('.kv-file-upload'), - $btnDelete = $thumb.find('.kv-file-remove'); - if (!$thumb.hasClass('file-preview-success')) { - self.setThumbStatus($thumb, 'Loading'); - addCss($thumb, 'file-uploading'); - } - $btnUpload.attr('disabled', true); - $btnDelete.attr('disabled', true); - }); - } - self.raise('filebatchpreupload', [outData]); - if (self.abort(outData)) { - jqXHR.abort(); - self.setProgress(100); - } - }; - fnSuccess = function (data, textStatus, jqXHR) { - /** @namespace data.errorkeys */ - var outData = self.getOutData(jqXHR, data), $thumbs = self.getThumbs(), key = 0, - keys = isEmpty(data) || isEmpty(data.errorkeys) ? [] : data.errorkeys; - if (isEmpty(data) || isEmpty(data.error)) { - self.raise('filebatchuploadsuccess', [outData]); - setAllUploaded(); - if (self.showPreview) { - $thumbs.each(function () { - var $thumb = $(this), $btnUpload = $thumb.find('.kv-file-upload'); - $thumb.find('.kv-file-upload').hide(); - self.setThumbStatus($thumb, 'Success'); - $thumb.removeClass('file-uploading'); - $btnUpload.removeAttr('disabled'); - }); - self.initUploadSuccess(data); - } else { - self.reset(); - } - } else { - if (self.showPreview) { - $thumbs.each(function () { - var $thumb = $(this), $btnDelete = $thumb.find('.kv-file-remove'), - $btnUpload = $thumb.find('.kv-file-upload'); - $thumb.removeClass('file-uploading'); - $btnUpload.removeAttr('disabled'); - $btnDelete.removeAttr('disabled'); - if (keys.length === 0) { - self.setPreviewError($thumb); - return; - } - if ($.inArray(key, keys) !== -1) { - self.setPreviewError($thumb); - } else { - $thumb.find('.kv-file-upload').hide(); - self.setThumbStatus($thumb, 'Success'); - self.updateStack(key, undefined); - } - key++; - }); - self.initUploadSuccess(data); - } - self.showUploadError(data.error, outData, 'filebatchuploaderror'); - } - }; - fnComplete = function () { - self.setProgress(100); - self.unlock(); - self.initSuccessThumbs(); - self.clearFileInput(); - self.raise('filebatchuploadcomplete', [self.filestack, self.getExtraData()]); - }; - fnError = function (jqXHR, textStatus, errorThrown) { - var outData = self.getOutData(jqXHR), errMsg = self.parseError(jqXHR, errorThrown); - self.showUploadError(errMsg, outData, 'filebatchuploaderror'); - self.uploadFileCount = total - 1; - if (!self.showPreview) { - return; - } - self.getThumbs().each(function () { - var $thumb = $(this), key = $thumb.attr('data-fileindex'); - $thumb.removeClass('file-uploading'); - if (self.filestack[key] !== undefined) { - self.setPreviewError($thumb); - } - }); - self.getThumbs().removeClass('file-uploading'); - self.getThumbs(' .kv-file-upload').removeAttr('disabled'); - self.getThumbs(' .kv-file-delete').removeAttr('disabled'); - }; - $.each(files, function (key, data) { - if (!isEmpty(files[key])) { - self.formdata.append(self.uploadFileAttr, data, self.filenames[key]); - } - }); - self.ajaxSubmit(fnBefore, fnSuccess, fnComplete, fnError); - }, - uploadExtraOnly: function () { - var self = this, params = {}, fnBefore, fnSuccess, fnComplete, fnError; - self.formdata = new FormData(); - if (self.abort(params)) { - return; - } - fnBefore = function (jqXHR) { - self.lock(); - var outData = self.getOutData(jqXHR); - self.raise('filebatchpreupload', [outData]); - self.setProgress(50); - params.data = outData; - params.xhr = jqXHR; - if (self.abort(params)) { - jqXHR.abort(); - self.setProgress(100); - } - }; - fnSuccess = function (data, textStatus, jqXHR) { - var outData = self.getOutData(jqXHR, data); - if (isEmpty(data) || isEmpty(data.error)) { - self.raise('filebatchuploadsuccess', [outData]); - self.clearFileInput(); - self.initUploadSuccess(data); - } else { - self.showUploadError(data.error, outData, 'filebatchuploaderror'); - } - }; - fnComplete = function () { - self.setProgress(100); - self.unlock(); - self.clearFileInput(); - self.raise('filebatchuploadcomplete', [self.filestack, self.getExtraData()]); - }; - fnError = function (jqXHR, textStatus, errorThrown) { - var outData = self.getOutData(jqXHR), errMsg = self.parseError(jqXHR, errorThrown); - params.data = outData; - self.showUploadError(errMsg, outData, 'filebatchuploaderror'); - }; - self.ajaxSubmit(fnBefore, fnSuccess, fnComplete, fnError); - }, - upload: function () { - var self = this, totLen = self.getFileStack().length, params = {}, - i, outData, len, hasExtraData = !$.isEmptyObject(self.getExtraData()); - if (self.minFileCount > 0 && self.getFileCount(totLen) < self.minFileCount) { - self.noFilesError(params); - return; - } - if (!self.isUploadable || self.isDisabled || (totLen === 0 && !hasExtraData)) { - return; - } - self.resetUpload(); - self.$progress.removeClass('hide'); - self.uploadCount = 0; - self.uploadStatus = {}; - self.uploadLog = []; - self.lock(); - self.setProgress(2); - if (totLen === 0 && hasExtraData) { - self.uploadExtraOnly(); - return; - } - len = self.filestack.length; - self.hasInitData = false; - if (self.uploadAsync) { - outData = self.getOutData(); - self.raise('filebatchpreupload', [outData]); - self.fileBatchCompleted = false; - self.uploadCache = {content: [], config: [], tags: [], append: true}; - self.uploadAsyncCount = self.getFileStack().length; - for (i = 0; i < len; i++) { - self.uploadCache.content[i] = null; - self.uploadCache.config[i] = null; - self.uploadCache.tags[i] = null; - } - for (i = 0; i < len; i++) { - if (self.filestack[i] !== undefined) { - self.uploadSingle(i, self.filestack, true); - } - } - return; - } - self.uploadBatch(); - }, - initFileActions: function () { - var self = this; - if (!self.showPreview) { - return; - } - self.$preview.find('.kv-file-remove').each(function () { - var $el = $(this), $frame = $el.closest('.file-preview-frame'), hasError, - id = $frame.attr('id'), ind = $frame.attr('data-fileindex'), n, cap, status; - handler($el, 'click', function () { - status = self.raise('filepreremove', [id, ind]); - if (status === false || !self.validateMinCount()) { - return false; - } - hasError = $frame.hasClass('file-preview-error'); - self.cleanMemory($frame); - $frame.fadeOut('slow', function () { - self.updateStack(ind, undefined); - self.clearObjects($frame); - $frame.remove(); - if (id && hasError) { - self.$errorContainer.find('li[data-file-id="' + id + '"]').fadeOut('fast', function () { - $(this).remove(); - if (!self.errorsExist()) { - self.resetErrors(); - } - }); - } - var filestack = self.getFileStack(true), len = filestack.length, chk = previewCache.count( - self.id), - hasThumb = self.showPreview && self.$preview.find('.file-preview-frame').length; - self.clearFileInput(); - if (len === 0 && chk === 0 && !hasThumb) { - self.reset(); - } else { - n = chk + len; - cap = n > 1 ? self.getMsgSelected(n) : (filestack[0] ? self.getFileNames()[0] : ''); - self.setCaption(cap); - } - self.raise('fileremoved', [id, ind]); - }); - }); - }); - self.$preview.find('.kv-file-upload').each(function () { - var $el = $(this); - handler($el, 'click', function () { - var $frame = $el.closest('.file-preview-frame'), - ind = $frame.attr('data-fileindex'); - if (!$frame.hasClass('file-preview-error')) { - self.uploadSingle(ind, self.filestack, false); - } - }); - }); - }, - hideFileIcon: function () { - if (this.overwriteInitial) { - this.$captionContainer.find('.kv-caption-icon').hide(); - } - }, - showFileIcon: function () { - this.$captionContainer.find('.kv-caption-icon').show(); - }, - addError: function (msg) { - var self = this, $error = self.$errorContainer; - if (msg && $error.length) { - $error.html(self.errorCloseButton + msg); - handler($error.find('.kv-error-close'), 'click', function () { - $error.fadeOut('slow'); - }); - } - }, - resetErrors: function (fade) { - var self = this, $error = self.$errorContainer; - self.isError = false; - self.$container.removeClass('has-error'); - $error.html(''); - if (fade) { - $error.fadeOut('slow'); - } else { - $error.hide(); - } - }, - showFolderError: function (folders) { - var self = this, $error = self.$errorContainer; - if (!folders) { - return; - } - self.addError(self.msgFoldersNotAllowed.replace(/\{n}/g, folders)); - $error.fadeIn(800); - addCss(self.$container, 'has-error'); - self.raise('filefoldererror', [folders]); - }, - showUploadError: function (msg, params, event) { - var self = this, $error = self.$errorContainer, ev = event || 'fileuploaderror', - e = params && params.id ? '
  • ' + msg + '
  • ' : '
  • ' + msg + '
  • '; - if ($error.find('ul').length === 0) { - self.addError('
      ' + e + '
    '); - } else { - $error.find('ul').append(e); - } - $error.fadeIn(800); - self.raise(ev, [params]); - self.$container.removeClass('file-input-new'); - addCss(self.$container, 'has-error'); - return true; - }, - showError: function (msg, params, event) { - var self = this, $error = self.$errorContainer, ev = event || 'fileerror'; - params = params || {}; - params.reader = self.reader; - self.addError(msg); - $error.fadeIn(800); - self.raise(ev, [params]); - if (!self.isUploadable) { - self.clearFileInput(); - } - self.$container.removeClass('file-input-new'); - addCss(self.$container, 'has-error'); - self.$btnUpload.attr('disabled', true); - return true; - }, - errorHandler: function (evt, caption) { - var self = this, err = evt.target.error; - /** @namespace err.NOT_FOUND_ERR */ - /** @namespace err.SECURITY_ERR */ - /** @namespace err.NOT_READABLE_ERR */ - if (err.code === err.NOT_FOUND_ERR) { - self.showError(self.msgFileNotFound.replace('{name}', caption)); - } else if (err.code === err.SECURITY_ERR) { - self.showError(self.msgFileSecured.replace('{name}', caption)); - } else if (err.code === err.NOT_READABLE_ERR) { - self.showError(self.msgFileNotReadable.replace('{name}', caption)); - } else if (err.code === err.ABORT_ERR) { - self.showError(self.msgFilePreviewAborted.replace('{name}', caption)); - } else { - self.showError(self.msgFilePreviewError.replace('{name}', caption)); - } - }, - parseFileType: function (file) { - var self = this, isValid, vType, cat, i; - for (i = 0; i < defaultPreviewTypes.length; i += 1) { - cat = defaultPreviewTypes[i]; - isValid = isSet(cat, self.fileTypeSettings) ? self.fileTypeSettings[cat] : defaultFileTypeSettings[cat]; - vType = isValid(file.type, file.name) ? cat : ''; - if (!isEmpty(vType)) { - return vType; - } - } - return 'other'; - }, - previewDefault: function (file, previewId, isDisabled) { - if (!this.showPreview) { - return; - } - var self = this, frameClass = '', fname = file ? file.name : '', - /** @namespace objUrl.createObjectURL */ - data = objUrl.createObjectURL(file), ind = previewId.slice(previewId.lastIndexOf('-') + 1), - config = self.previewSettings.other || defaultPreviewSettings.other, - footer = self.renderFileFooter(file.name, config.width), - previewOtherTemplate = self.parseFilePreviewIcon(self.getPreviewTemplate('other'), fname); - if (isDisabled === true) { - if (!self.isUploadable) { - footer += '
    ' + self.fileActionSettings.indicatorError + '
    '; - } - } - self.clearDefaultPreview(); - self.$preview.append("\n" + previewOtherTemplate - .replace(/\{previewId}/g, previewId) - .replace(/\{frameClass}/g, frameClass) - .replace(/\{fileindex}/g, ind) - .replace(/\{caption}/g, self.slug(file.name)) - .replace(/\{width}/g, config.width) - .replace(/\{height}/g, config.height) - .replace(/\{type}/g, file.type) - .replace(/\{data}/g, data) - .replace(/\{footer}/g, footer)); - if (isDisabled === true && self.isUploadable) { - self.setThumbStatus($('#' + previewId), 'Error'); - } - }, - previewFile: function (i, file, theFile, previewId, data) { - if (!this.showPreview) { - return; - } - var self = this, cat = self.parseFileType(file), fname = file ? file.name : '', caption = self.slug(fname), - content, strText, types = self.allowedPreviewTypes, mimes = self.allowedPreviewMimeTypes, - tmplt = self.getPreviewTemplate(cat), chkTypes = types && types.indexOf(cat) >= 0, id, - config = isSet(cat, self.previewSettings) ? self.previewSettings[cat] : defaultPreviewSettings[cat], - chkMimes = mimes && mimes.indexOf(file.type) !== -1, - footer = self.renderFileFooter(caption, config.width), modal = '', - ind = previewId.slice(previewId.lastIndexOf('-') + 1); - if (chkTypes || chkMimes) { - tmplt = self.parseFilePreviewIcon(tmplt, fname.split('.').pop()); - if (cat === 'text') { - strText = htmlEncode(theFile.target.result); - id = 'text-' + uniqId(); - content = tmplt.replace(/\{zoom}/g, self.getLayoutTemplate('zoom')); - modal = self.getLayoutTemplate('modal').replace('{id}', id) - .replace(/\{title}/g, caption) - .replace(/\{body}/g, strText).replace(/\{heading}/g, self.msgZoomModalHeading); - content = content.replace(/\{previewId}/g, previewId).replace(/\{caption}/g, caption) - .replace(/\{width}/g, config.width).replace(/\{height}/g, config.height) - .replace(/\{frameClass}/g, '').replace(/\{zoomInd}/g, self.zoomIndicator) - .replace(/\{footer}/g, footer).replace(/\{fileindex}/g, ind) - .replace(/\{type}/g, file.type).replace(/\{zoomTitle}/g, self.msgZoomTitle) - .replace(/\{dialog}/g, "$('#" + id + "').modal('show')") - .replace(/\{data}/g, strText) + modal; - } else { - content = tmplt.replace(/\{previewId}/g, previewId).replace(/\{caption}/g, caption) - .replace(/\{frameClass}/g, '').replace(/\{type}/g, file.type).replace(/\{fileindex}/g, ind) - .replace(/\{width}/g, config.width).replace(/\{height}/g, config.height) - .replace(/\{footer}/g, footer).replace(/\{data}/g, data); - } - self.clearDefaultPreview(); - self.$preview.append("\n" + content); - self.validateImage(i, previewId, caption, file.type); - } else { - self.previewDefault(file, previewId); - } - }, - slugDefault: function (text) { - return isEmpty(text) ? '' : String(text).replace(/[\-\[\]\/\{}:;#%=\(\)\*\+\?\\\^\$\|<>&"']/g, '_'); - }, - readFiles: function (files) { - this.reader = new FileReader(); - var self = this, $el = self.$element, $preview = self.$preview, reader = self.reader, - $container = self.$previewContainer, $status = self.$previewStatus, msgLoading = self.msgLoading, - msgProgress = self.msgProgress, previewInitId = self.previewInitId, numFiles = files.length, - settings = self.fileTypeSettings, ctr = self.filestack.length, readFile, - throwError = function (msg, file, previewId, index) { - var p1 = $.extend(true, {}, self.getOutData({}, {}, files), {id: previewId, index: index}), - p2 = {id: previewId, index: index, file: file, files: files}; - self.previewDefault(file, previewId, true); - if (self.isUploadable) { - self.addToStack(undefined); - } - setTimeout(readFile(index + 1), 100); - self.initFileActions(); - if (self.removeFromPreviewOnError) { - $('#' + previewId).remove(); - } - return self.isUploadable ? self.showUploadError(msg, p1) : self.showError(msg, p2); - }; - - self.loadedImages = []; - self.totalImagesCount = 0; - - $.each(files, function (key, file) { - var func = self.fileTypeSettings.image || defaultFileTypeSettings.image; - if (func && func(file.type)) { - self.totalImagesCount++; - } - }); - - readFile = function (i) { - if (isEmpty($el.attr('multiple'))) { - numFiles = 1; - } - if (i >= numFiles) { - if (self.isUploadable && self.filestack.length > 0) { - self.raise('filebatchselected', [self.getFileStack()]); - } else { - self.raise('filebatchselected', [files]); - } - $container.removeClass('file-thumb-loading'); - $status.html(''); - return; - } - var node = ctr + i, previewId = previewInitId + "-" + node, isText, file = files[i], - caption = self.slug(file.name), fileSize = (file.size || 0) / 1000, checkFile, fileExtExpr = '', - previewData = objUrl.createObjectURL(file), fileCount = 0, j, msg, typ, chk, - fileTypes = self.allowedFileTypes, strTypes = isEmpty(fileTypes) ? '' : fileTypes.join(', '), - fileExt = self.allowedFileExtensions, strExt = isEmpty(fileExt) ? '' : fileExt.join(', '); - if (!isEmpty(fileExt)) { - fileExtExpr = new RegExp('\\.(' + fileExt.join('|') + ')$', 'i'); - } - fileSize = fileSize.toFixed(2); - if (self.maxFileSize > 0 && fileSize > self.maxFileSize) { - msg = self.msgSizeTooLarge.replace('{name}', caption) - .replace('{size}', fileSize) - .replace('{maxSize}', self.maxFileSize); - self.isError = throwError(msg, file, previewId, i); - return; - } - if (!isEmpty(fileTypes) && isArray(fileTypes)) { - for (j = 0; j < fileTypes.length; j += 1) { - typ = fileTypes[j]; - checkFile = settings[typ]; - chk = (checkFile !== undefined && checkFile(file.type, caption)); - fileCount += isEmpty(chk) ? 0 : chk.length; - } - if (fileCount === 0) { - msg = self.msgInvalidFileType.replace('{name}', caption).replace('{types}', strTypes); - self.isError = throwError(msg, file, previewId, i); - return; - } - } - if (fileCount === 0 && !isEmpty(fileExt) && isArray(fileExt) && !isEmpty(fileExtExpr)) { - chk = caption.match(fileExtExpr); - fileCount += isEmpty(chk) ? 0 : chk.length; - if (fileCount === 0) { - msg = self.msgInvalidFileExtension.replace('{name}', caption).replace('{extensions}', - strExt); - self.isError = throwError(msg, file, previewId, i); - return; - } - } - if (!self.showPreview) { - self.addToStack(file); - setTimeout(readFile(i + 1), 100); - self.raise('fileloaded', [file, previewId, i, reader]); - return; - } - if ($preview.length > 0 && FileReader !== undefined) { - $status.html(msgLoading.replace('{index}', i + 1).replace('{files}', numFiles)); - $container.addClass('file-thumb-loading'); - reader.onerror = function (evt) { - self.errorHandler(evt, caption); - }; - reader.onload = function (theFile) { - self.previewFile(i, file, theFile, previewId, previewData); - self.initFileActions(); - }; - reader.onloadend = function () { - msg = msgProgress - .replace('{index}', i + 1).replace('{files}', numFiles) - .replace('{percent}', 50).replace('{name}', caption); - setTimeout(function () { - $status.html(msg); - self.updateFileDetails(numFiles); - readFile(i + 1); - }, 100); - self.raise('fileloaded', [file, previewId, i, reader]); - }; - reader.onprogress = function (data) { - if (data.lengthComputable) { - var fact = (data.loaded / data.total) * 100, progress = Math.ceil(fact); - msg = msgProgress.replace('{index}', i + 1).replace('{files}', numFiles) - .replace('{percent}', progress).replace('{name}', caption); - setTimeout(function () { - $status.html(msg); - }, 100); - } - }; - isText = isSet('text', settings) ? settings.text : defaultFileTypeSettings.text; - if (isText(file.type, caption)) { - reader.readAsText(file, self.textEncoding); - } else { - reader.readAsArrayBuffer(file); - } - } else { - self.previewDefault(file, previewId); - setTimeout(function () { - readFile(i + 1); - self.updateFileDetails(numFiles); - }, 100); - self.raise('fileloaded', [file, previewId, i, reader]); - } - self.addToStack(file); - }; - - readFile(0); - self.updateFileDetails(numFiles, false); - }, - updateFileDetails: function (numFiles) { - var self = this, $el = self.$element, fileStack = self.getFileStack(), - name = ($el[0].files[0] && $el[0].files[0].name) || (fileStack.length && fileStack[0].name) || '', - label = self.slug(name), n = self.isUploadable ? fileStack.length : numFiles, - nFiles = previewCache.count(self.id) + n, log = n > 1 ? self.getMsgSelected(nFiles) : label; - if (self.isError) { - self.$previewContainer.removeClass('file-thumb-loading'); - self.$previewStatus.html(''); - self.$captionContainer.find('.kv-caption-icon').hide(); - } else { - self.showFileIcon(); - } - self.setCaption(log, self.isError); - self.$container.removeClass('file-input-new file-input-ajax-new'); - if (arguments.length === 1) { - self.raise('fileselect', [numFiles, label]); - } - if (previewCache.count(self.id)) { - self.initPreviewDeletes(); - } - }, - validateMinCount: function () { - var self = this, len = self.isUploadable ? self.getFileStack().length : self.$element.get(0).files.length; - if (self.validateInitialCount && self.minFileCount > 0 && self.getFileCount(len - 1) < self.minFileCount) { - self.noFilesError({}); - return false; - } - return true; - }, - getFileCount: function (fileCount) { - var self = this, addCount = 0; - if (self.validateInitialCount && !self.overwriteInitial) { - addCount = previewCache.count(self.id); - fileCount += addCount; - } - return fileCount; - }, - change: function (e) { - var self = this, $el = self.$element; - if (!self.isUploadable && isEmpty($el.val()) && self.fileInputCleared) { // IE 11 fix - self.fileInputCleared = false; - return; - } - self.fileInputCleared = false; - var tfiles, msg, total, isDragDrop = arguments.length > 1, isAjaxUpload = self.isUploadable, i = 0, f, n, len, - files = isDragDrop ? e.originalEvent.dataTransfer.files : $el.get(0).files, ctr = self.filestack.length, - isSingleUpload = isEmpty($el.attr('multiple')), flagSingle = (isSingleUpload && ctr > 0), folders = 0, - throwError = function (mesg, file, previewId, index) { - var p1 = $.extend(true, {}, self.getOutData({}, {}, files), {id: previewId, index: index}), - p2 = {id: previewId, index: index, file: file, files: files}; - return self.isUploadable ? self.showUploadError(mesg, p1) : self.showError(mesg, p2); - }; - self.reader = null; - self.resetUpload(); - self.hideFileIcon(); - if (self.isUploadable) { - self.$container.find('.file-drop-zone .' + self.dropZoneTitleClass).remove(); - } - if (isDragDrop) { - tfiles = []; - while (files[i]) { - f = files[i]; - if (!f.type && f.size % 4096 === 0) { - folders++; - } else { - tfiles.push(f); - } - i++; - } - } else { - if (e.target.files === undefined) { - tfiles = e.target && e.target.value ? [ - {name: e.target.value.replace(/^.+\\/, '')} - ] : []; - } else { - tfiles = e.target.files; - } - } - if (isEmpty(tfiles) || tfiles.length === 0) { - if (!isAjaxUpload) { - self.clear(); - } - self.showFolderError(folders); - self.raise('fileselectnone'); - return; - } - self.resetErrors(); - len = tfiles.length; - total = self.isUploadable ? self.getFileStack().length + len : len; - total = self.getFileCount(total); - if (self.maxFileCount > 0 && total > self.maxFileCount) { - if (!self.autoReplace || len > self.maxFileCount) { - n = (self.autoReplace && len > self.maxFileCount) ? len : total; - msg = self.msgFilesTooMany.replace('{m}', self.maxFileCount).replace('{n}', n); - self.isError = throwError(msg, null, null, null); - self.$captionContainer.find('.kv-caption-icon').hide(); - self.setCaption('', true); - self.$container.removeClass('file-input-new file-input-ajax-new'); - return; - } - if (total > self.maxFileCount) { - self.resetPreviewThumbs(isAjaxUpload); - } - } else { - if (!isAjaxUpload || flagSingle) { - self.resetPreviewThumbs(false); - if (flagSingle) { - self.clearStack(); - } - } else { - if (isAjaxUpload && ctr === 0 && (!previewCache.count(self.id) || self.overwriteInitial)) { - self.resetPreviewThumbs(true); - } - } - } - if (self.isPreviewable) { - self.readFiles(tfiles); - } else { - self.updateFileDetails(1); - } - self.showFolderError(folders); - }, - getFileName: function (file) { - return file && file.name ? this.slug(file.name) : undefined; - }, - getFileNames: function (skipNull) { - var self = this; - return self.filenames.filter(function (n) { - return (skipNull ? n !== undefined : n !== undefined && n !== null); - }); - }, - getFileStack: function (skipNull) { - var self = this; - return self.filestack.filter(function (n) { - return (skipNull ? n !== undefined : n !== undefined && n !== null); - }); - }, - clearStack: function () { - var self = this; - self.filestack = []; - self.filenames = []; - }, - updateStack: function (i, file) { - var self = this; - self.filestack[i] = file; - self.filenames[i] = self.getFileName(file); - }, - addToStack: function (file) { - var self = this; - self.filestack.push(file); - self.filenames.push(self.getFileName(file)); - }, - setPreviewError: function ($thumb, i, val) { - var self = this; - if (i) { - self.updateStack(i, val); - } - if (self.removeFromPreviewOnError) { - $thumb.remove(); - } else { - self.setThumbStatus($thumb, 'Error'); - } - }, - checkDimensions: function (i, chk, $img, $thumb, fname, type, params) { - var self = this, msg, dim, tag = chk === 'Small' ? 'min' : 'max', - limit = self[tag + 'Image' + type], $imgEl, isValid; - if (isEmpty(limit) || !$img.length) { - return; - } - $imgEl = $img[0]; - dim = (type === 'Width') ? $imgEl.naturalWidth || $imgEl.width : $imgEl.naturalHeight || $imgEl.height; - isValid = chk === 'Small' ? dim >= limit : dim <= limit; - if (isValid) { - return; - } - msg = self['msgImage' + type + chk].replace('{name}', fname).replace('{size}', limit); - self.showUploadError(msg, params); - self.setPreviewError($thumb, i, null); - }, - validateImage: function (i, previewId, fname, ftype) { - var self = this, $preview = self.$preview, params, w1, w2, - $thumb = $preview.find("#" + previewId), $img = $thumb.find('img'); - fname = fname || 'Untitled'; - if (!$img.length) { - return; - } - handler($img, 'load', function () { - w1 = $thumb.width(); - w2 = $preview.width(); - if (w1 > w2) { - $img.css('width', '100%'); - $thumb.css('width', '97%'); - } - params = {ind: i, id: previewId}; - self.checkDimensions(i, 'Small', $img, $thumb, fname, 'Width', params); - self.checkDimensions(i, 'Small', $img, $thumb, fname, 'Height', params); - if (!self.resizeImage) { - self.checkDimensions(i, 'Large', $img, $thumb, fname, 'Width', params); - self.checkDimensions(i, 'Large', $img, $thumb, fname, 'Height', params); - } - self.raise('fileimageloaded', [previewId]); - self.loadedImages.push({ind: i, img: $img, thumb: $thumb, pid: previewId, typ: ftype}); - self.validateAllImages(); - objUrl.revokeObjectURL($img.attr('src')); - }); - }, - validateAllImages: function () { - var self = this, i, config, $img, $thumb, pid, ind, params = {}, errFunc; - if (self.loadedImages.length !== self.totalImagesCount) { - return; - } - self.raise('fileimagesloaded'); - if (!self.resizeImage) { - return; - } - errFunc = self.isUploadable ? self.showUploadError : self.showError; - for (i = 0; i < self.loadedImages.length; i++) { - config = self.loadedImages[i]; - $img = config.img; - $thumb = config.thumb; - pid = config.pid; - ind = config.ind; - params = {id: pid, 'index': ind}; - if (!self.getResizedImage($img[0], config.typ, pid, ind)) { - errFunc(self.msgImageResizeError, params, 'fileimageresizeerror'); - self.setPreviewError($thumb, ind); - } - } - self.raise('fileimagesresized'); - }, - getResizedImage: function (image, type, pid, ind) { - var self = this, width = image.naturalWidth, height = image.naturalHeight, ratio = 1, - maxWidth = self.maxImageWidth || width, maxHeight = self.maxImageHeight || height, - isValidImage = (width && height), chkWidth, chkHeight, - canvas = self.imageCanvas, context = self.imageCanvasContext; - if (!isValidImage) { - return false; - } - if (width === maxWidth && height === maxHeight) { - return true; - } - type = type || self.resizeDefaultImageType; - chkWidth = width > maxWidth; - chkHeight = height > maxHeight; - if (self.resizePreference === 'width') { - ratio = chkWidth ? maxWidth / width : (chkHeight ? maxHeight / height : 1); - } else { - ratio = chkHeight ? maxHeight / height : (chkWidth ? maxWidth / width : 1); - } - self.resetCanvas(); - width *= ratio; - height *= ratio; - canvas.width = width; - canvas.height = height; - try { - context.drawImage(image, 0, 0, width, height); - canvas.toBlob(function (blob) { - self.raise('fileimageresized', [pid, ind]); - self.filestack[ind] = blob; - }, type, self.resizeQuality); - return true; - } - catch (err) { - return false; - } - }, - initCaption: function () { - var self = this, cap = self.initialCaption || ''; - if (self.overwriteInitial || isEmpty(cap)) { - self.$caption.html(''); - return false; - } - self.setCaption(cap); - return true; - }, - setCaption: function (content, isError) { - var self = this, title, out; - if (isError) { - title = $('
    ' + self.msgValidationError + '
    ').text(); - out = '' + - self.msgValidationErrorIcon + title + ''; - } else { - if (isEmpty(content) || self.$caption.length === 0) { - return; - } - title = $('
    ' + content + '
    ').text(); - out = self.getLayoutTemplate('icon') + title; - } - self.$caption.html(out); - self.$caption.attr('title', title); - self.$captionContainer.find('.file-caption-ellipsis').attr('title', title); - }, - initBrowse: function ($container) { - var self = this; - self.$btnFile = $container.find('.btn-file'); - self.$btnFile.append(self.$element); - }, - createContainer: function () { - var self = this, - $container = $(document.createElement("div")) - .attr({"class": 'file-input file-input-new'}) - .html(self.renderMain()); - self.$element.before($container); - self.initBrowse($container); - return $container; - }, - refreshContainer: function () { - var self = this, $container = self.$container; - $container.before(self.$element); - $container.html(self.renderMain()); - self.initBrowse($container); - }, - renderMain: function () { - var self = this, dropCss = (self.isUploadable && self.dropZoneEnabled) ? ' file-drop-zone' : 'file-drop-disabled', - close = !self.showClose ? '' : self.getLayoutTemplate('close'), - preview = !self.showPreview ? '' : self.getLayoutTemplate('preview') - .replace(/\{class}/g, self.previewClass) - .replace(/\{dropClass}/g, dropCss), - css = self.isDisabled ? self.captionClass + ' file-caption-disabled' : self.captionClass, - caption = self.captionTemplate.replace(/\{class}/g, css + ' kv-fileinput-caption'); - return self.mainTemplate.replace(/\{class}/g, self.mainClass) - .replace(/\{preview}/g, preview) - .replace(/\{close}/g, close) - .replace(/\{caption}/g, caption) - .replace(/\{upload}/g, self.renderButton('upload')) - .replace(/\{remove}/g, self.renderButton('remove')) - .replace(/\{cancel}/g, self.renderButton('cancel')) - .replace(/\{browse}/g, self.renderButton('browse')); - }, - renderButton: function (type) { - var self = this, tmplt = self.getLayoutTemplate('btnDefault'), css = self[type + 'Class'], - title = self[type + 'Title'], icon = self[type + 'Icon'], label = self[type + 'Label'], - status = self.isDisabled ? ' disabled' : '', btnType = 'button'; - switch (type) { - case 'remove': - if (!self.showRemove) { - return ''; - } - break; - case 'cancel': - if (!self.showCancel) { - return ''; - } - css += ' hide'; - break; - case 'upload': - if (!self.showUpload) { - return ''; - } - if (self.isUploadable && !self.isDisabled) { - tmplt = self.getLayoutTemplate('btnLink').replace('{href}', self.uploadUrl); - } else { - btnType = 'submit'; - } - break; - case 'browse': - tmplt = self.getLayoutTemplate('btnBrowse'); - break; - default: - return ''; - } - css += type === 'browse' ? ' btn-file' : ' fileinput-' + type + ' fileinput-' + type + '-button'; - if (!isEmpty(label)) { - label = ' ' + label + ''; - } - return tmplt.replace('{type}', btnType) - .replace('{css}', css) - .replace('{title}', title) - .replace('{status}', status) - .replace('{icon}', icon) - .replace('{label}', label); - } - }; - - $.fn.fileinput = function (option) { - if (!hasFileAPISupport() && !isIE(9)) { - return; - } - var args = Array.apply(null, arguments), retvals = []; - args.shift(); - this.each(function () { - var self = $(this), data = self.data('fileinput'), options = typeof option === 'object' && option, - lang = options.language || self.data('language') || 'en', loc = {}, opts; - - if (!data) { - if (lang !== 'en' && !isEmpty($.fn.fileinputLocales[lang])) { - loc = $.fn.fileinputLocales[lang]; - } - opts = $.extend(true, {}, $.fn.fileinput.defaults, $.fn.fileinputLocales.en, loc, options, self.data()); - data = new FileInput(this, opts); - self.data('fileinput', data); - } - - if (typeof option === 'string') { - retvals.push(data[option].apply(data, args)); - } - }); - switch (retvals.length) { - case 0: - return this; - case 1: - return retvals[0]; - default: - return retvals; - } - }; - - $.fn.fileinput.defaults = { - language: 'en', - showCaption: true, - showPreview: true, - showRemove: true, - showUpload: true, - showCancel: true, - showClose: true, - showUploadedThumbs: true, - autoReplace: false, - mainClass: '', - previewClass: '', - captionClass: '', - mainTemplate: null, - initialCaption: '', - initialPreview: [], - initialPreviewDelimiter: '*$$*', - initialPreviewConfig: [], - initialPreviewThumbTags: [], - previewThumbTags: {}, - initialPreviewShowDelete: true, - removeFromPreviewOnError: true, - deleteUrl: '', - deleteExtraData: {}, - overwriteInitial: true, - layoutTemplates: defaultLayoutTemplates, - previewTemplates: defaultPreviewTemplates, - allowedPreviewTypes: defaultPreviewTypes, - allowedPreviewMimeTypes: null, - allowedFileTypes: null, - allowedFileExtensions: null, - defaultPreviewContent: null, - customLayoutTags: {}, - customPreviewTags: {}, - previewSettings: defaultPreviewSettings, - fileTypeSettings: defaultFileTypeSettings, - previewFileIcon: '', - previewFileIconClass: 'file-icon-4x', - previewFileIconSettings: {}, - previewFileExtSettings: {}, - buttonLabelClass: 'hidden-xs', - browseIcon: '', - browseClass: 'btn btn-primary', - removeIcon: '', - removeClass: 'btn btn-default', - cancelIcon: '', - cancelClass: 'btn btn-default', - uploadIcon: '', - uploadClass: 'btn btn-default', - uploadUrl: null, - uploadAsync: true, - uploadExtraData: {}, - minImageWidth: null, - minImageHeight: null, - maxImageWidth: null, - maxImageHeight: null, - resizeImage: false, - resizePreference: 'width', - resizeQuality: 0.92, - resizeDefaultImageType: 'image/jpeg', - maxFileSize: 0, - minFileCount: 0, - maxFileCount: 0, - validateInitialCount: false, - msgValidationErrorClass: 'text-danger', - msgValidationErrorIcon: ' ', - msgErrorClass: 'file-error-message', - progressThumbClass: "progress-bar progress-bar-success progress-bar-striped active", - progressClass: "progress-bar progress-bar-success progress-bar-striped active", - progressCompleteClass: "progress-bar progress-bar-success", - previewFileType: 'image', - zoomIndicator: '', - elCaptionContainer: null, - elCaptionText: null, - elPreviewContainer: null, - elPreviewImage: null, - elPreviewStatus: null, - elErrorContainer: null, - errorCloseButton: '×', - slugCallback: null, - dropZoneEnabled: true, - dropZoneTitleClass: 'file-drop-zone-title', - fileActionSettings: {}, - otherActionButtons: '', - textEncoding: 'UTF-8', - ajaxSettings: {}, - ajaxDeleteSettings: {}, - showAjaxErrorDetails: true - }; - - $.fn.fileinputLocales.en = { - fileSingle: 'file', - filePlural: 'files', - browseLabel: 'Browse …', - removeLabel: 'Remove', - removeTitle: 'Clear selected files', - cancelLabel: 'Cancel', - cancelTitle: 'Abort ongoing upload', - uploadLabel: 'Upload', - uploadTitle: 'Upload selected files', - msgZoomTitle: 'View details', - msgZoomModalHeading: 'Detailed Preview', - msgSizeTooLarge: 'File "{name}" ({size} KB) exceeds maximum allowed upload size of {maxSize} KB.', - msgFilesTooLess: 'You must select at least {n} {files} to upload.', - msgFilesTooMany: 'Number of files selected for upload ({n}) exceeds maximum allowed limit of {m}.', - msgFileNotFound: 'File "{name}" not found!', - msgFileSecured: 'Security restrictions prevent reading the file "{name}".', - msgFileNotReadable: 'File "{name}" is not readable.', - msgFilePreviewAborted: 'File preview aborted for "{name}".', - msgFilePreviewError: 'An error occurred while reading the file "{name}".', - msgInvalidFileType: 'Invalid type for file "{name}". Only "{types}" files are supported.', - msgInvalidFileExtension: 'Invalid extension for file "{name}". Only "{extensions}" files are supported.', - msgUploadAborted: 'The file upload was aborted', - msgValidationError: 'File Upload Error', - msgLoading: 'Loading file {index} of {files} …', - msgProgress: 'Loading file {index} of {files} - {name} - {percent}% completed.', - msgSelected: '{n} {files} selected', - msgFoldersNotAllowed: 'Drag & drop files only! {n} folder(s) dropped were skipped.', - msgImageWidthSmall: 'Width of image file "{name}" must be at least {size} px.', - msgImageHeightSmall: 'Height of image file "{name}" must be at least {size} px.', - msgImageWidthLarge: 'Width of image file "{name}" cannot exceed {size} px.', - msgImageHeightLarge: 'Height of image file "{name}" cannot exceed {size} px.', - msgImageResizeError: 'Could not get the image dimensions to resize.', - msgImageResizeException: 'Error while resizing the image.
    {errors}
    ', - dropZoneTitle: 'Drag & drop files here …' - }; - - $.fn.fileinput.Constructor = FileInput; - - /** - * Convert automatically file input with class 'file' into a bootstrap fileinput control. - */ - $(document).ready(function () { - var $input = $('input.file[type=file]'); - if ($input.length) { - $input.fileinput(); - } - }); +/*! + * @copyright Copyright © Kartik Visweswaran, Krajee.com, 2014 - 2015 + * @version 4.3.0 + * + * File input styled for Bootstrap 3.0 that utilizes HTML5 File Input's advanced features including the FileReader API. + * + * The plugin drastically enhances the HTML file input to preview multiple files on the client before upload. In + * addition it provides the ability to preview content of images, text, videos, audio, html, flash and other objects. + * It also offers the ability to upload and delete files using AJAX, and add files in batches (i.e. preview, append, + * or remove before upload). + * + * Author: Kartik Visweswaran + * Copyright: 2015, Kartik Visweswaran, Krajee.com + * For more JQuery plugins visit http://plugins.krajee.com + * For more Yii related demos visit http://demos.krajee.com + */ +(function (factory) { + "use strict"; + if (typeof define === 'function' && define.amd) { // jshint ignore:line + // AMD. Register as an anonymous module. + define(['jquery'], factory); // jshint ignore:line + } else { // noinspection JSUnresolvedVariable + if (typeof module === 'object' && module.exports) { // jshint ignore:line + // Node/CommonJS + // noinspection JSUnresolvedVariable + module.exports = factory(require('jquery')); // jshint ignore:line + } else { + // Browser globals + factory(window.jQuery); + } + } +}(function ($) { + "use strict"; + + $.fn.fileinputLocales = {}; + + var isIE, isEdge, handler, previewCache, getNum, hasFileAPISupport, hasDragDropSupport, hasFileUploadSupport, addCss, + STYLE_SETTING, OBJECT_PARAMS, DEFAULT_PREVIEW, defaultFileActionSettings, tMain1, tMain2, tPreview, tIcon, tClose, + tCaption, tBtnDefault, tBtnLink, tBtnBrowse, tModal, tProgress, tFooter, tActions, tActionDelete, tActionUpload, + tZoom, tGeneric, tHtml, tImage, tText, tVideo, tAudio, tFlash, tObject, tOther, defaultLayoutTemplates, + defaultPreviewTemplates, defaultPreviewTypes, defaultPreviewSettings, defaultFileTypeSettings, isEmpty, isArray, + isSet, getElement, uniqId, htmlEncode, replaceTags, objUrl, FileInput, NAMESPACE; + + NAMESPACE = '.fileinput'; + isIE = function (ver) { + // check for IE versions < 11 + if (navigator.appName !== 'Microsoft Internet Explorer') { + return false; + } + if (ver === 10) { + return new RegExp('msie\\s' + ver, 'i').test(navigator.userAgent); + } + var div = document.createElement("div"), status; + div.innerHTML = ""; + status = div.getElementsByTagName("i").length; + document.body.appendChild(div); + div.parentNode.removeChild(div); + return status; + }; + isEdge = function () { + return new RegExp('Edge\/[0-9]+', 'i').test(navigator.userAgent); + }; + handler = function ($el, event, callback, skipNS) { + var ev = skipNS ? event : event.split(' ').join(NAMESPACE + ' ') + NAMESPACE; + $el.off(ev).on(ev, callback); + }; + previewCache = { + data: {}, + init: function (obj) { + var content = obj.initialPreview, id = obj.id; + if (content.length > 0 && !isArray(content)) { + content = content.split(obj.initialPreviewDelimiter); + } + previewCache.data[id] = { + content: content, + config: obj.initialPreviewConfig, + tags: obj.initialPreviewThumbTags, + delimiter: obj.initialPreviewDelimiter, + template: obj.previewGenericTemplate, + msg: function (n) { + return obj.getMsgSelected(n); + }, + initId: obj.previewInitId, + footer: obj.getLayoutTemplate('footer').replace(/\{progress}/g, obj.renderThumbProgress()), + isDelete: obj.initialPreviewShowDelete, + caption: obj.initialCaption, + actions: function (showUpload, showDelete, disabled, url, key) { + return obj.renderFileActions(showUpload, showDelete, disabled, url, key); + } + }; + }, + fetch: function (id) { + return previewCache.data[id].content.filter(function (n) { + return n !== null; + }); + }, + count: function (id, all) { + return !!previewCache.data[id] && !!previewCache.data[id].content ? + (all ? previewCache.data[id].content.length : previewCache.fetch(id).length) : 0; + }, + get: function (id, i, isDisabled) { + var ind = 'init_' + i, data = previewCache.data[id], config = data.config[i], + previewId = data.initId + '-' + ind, out, $tmp, frameClass = ' file-preview-initial'; + /** @namespace config.frameClass */ + /** @namespace config.frameAttr */ + isDisabled = isDisabled === undefined ? true : isDisabled; + if (data.content[i] === null) { + return ''; + } + if (!isEmpty(config) && !isEmpty(config.frameClass)) { + frameClass += ' ' + config.frameClass; + } + out = data.template + .replace(/\{previewId}/g, previewId) + .replace(/\{frameClass}/g, frameClass) + .replace(/\{fileindex}/g, ind) + .replace(/\{content}/g, data.content[i]) + .replace(/\{footer}/g, previewCache.footer(id, i, isDisabled)); + if (data.tags.length && data.tags[i]) { + out = replaceTags(out, data.tags[i]); + } + if (!isEmpty(config) && !isEmpty(config.frameAttr)) { + $tmp = $(document.createElement('div')).html(out); + $tmp.find('.file-preview-initial').attr(config.frameAttr); + out = $tmp.html(); + $tmp.remove(); + } + return out; + }, + add: function (id, content, config, tags, append) { + var data = $.extend(true, {}, previewCache.data[id]), index; + if (!isArray(content)) { + content = content.split(data.delimiter); + } + if (append) { + index = data.content.push(content) - 1; + data.config[index] = config; + data.tags[index] = tags; + } else { + index = content.length; + data.content = content; + data.config = config; + data.tags = tags; + } + previewCache.data[id] = data; + return index; + }, + set: function (id, content, config, tags, append) { + var data = $.extend(true, {}, previewCache.data[id]), i, chk; + if (!content || !content.length) { + return; + } + if (!isArray(content)) { + content = content.split(data.delimiter); + } + chk = content.filter(function (n) { + return n !== null; + }); + if (!chk.length) { + return; + } + if (data.content === undefined) { + data.content = []; + } + if (data.config === undefined) { + data.config = []; + } + if (data.tags === undefined) { + data.tags = []; + } + if (append) { + for (i = 0; i < content.length; i++) { + if (content[i]) { + data.content.push(content[i]); + } + } + for (i = 0; i < config.length; i++) { + if (config[i]) { + data.config.push(config[i]); + } + } + for (i = 0; i < tags.length; i++) { + if (tags[i]) { + data.tags.push(tags[i]); + } + } + } else { + data.content = content; + data.config = config; + data.tags = tags; + } + previewCache.data[id] = data; + }, + unset: function (id, index) { + var chk = previewCache.count(id); + if (!chk) { + return; + } + if (chk === 1) { + previewCache.data[id].content = []; + previewCache.data[id].config = []; + return; + } + previewCache.data[id].content[index] = null; + previewCache.data[id].config[index] = null; + }, + out: function (id) { + var html = '', data = previewCache.data[id], caption, len = previewCache.count(id, true); + if (len === 0) { + return {content: '', caption: ''}; + } + for (var i = 0; i < len; i++) { + html += previewCache.get(id, i); + } + caption = data.msg(previewCache.count(id)); + return {content: html, caption: caption}; + }, + footer: function (id, i, isDisabled) { + var data = previewCache.data[id]; + isDisabled = isDisabled === undefined ? true : isDisabled; + if (data.config.length === 0 || isEmpty(data.config[i])) { + return ''; + } + var config = data.config[i], + caption = isSet('caption', config) ? config.caption : '', + width = isSet('width', config) ? config.width : 'auto', + url = isSet('url', config) ? config.url : false, + key = isSet('key', config) ? config.key : null, + disabled = (url === false) && isDisabled, + actions = data.isDelete ? data.actions(false, true, disabled, url, key) : '', + footer = data.footer.replace(/\{actions}/g, actions); + return footer.replace(/\{caption}/g, caption) + .replace(/\{width}/g, width) + .replace(/\{indicator}/g, '') + .replace(/\{indicatorTitle}/g, ''); + } + }; + getNum = function (num, def) { + def = def || 0; + if (typeof num === "number") { + return num; + } + if (typeof num === "string") { + num = parseFloat(num); + } + return isNaN(num) ? def : num; + }; + hasFileAPISupport = function () { + return !!(window.File && window.FileReader); + }; + hasDragDropSupport = function () { + var div = document.createElement('div'); + /** @namespace div.draggable */ + /** @namespace div.ondragstart */ + /** @namespace div.ondrop */ + return !isIE(9) && !isEdge() && // Fix for MS Edge drag & drop support bug + (div.draggable !== undefined || (div.ondragstart !== undefined && div.ondrop !== undefined)); + }; + hasFileUploadSupport = function () { + return hasFileAPISupport() && window.FormData; + }; + addCss = function ($el, css) { + $el.removeClass(css).addClass(css); + }; + STYLE_SETTING = 'style="width:{width};height:{height};"'; + OBJECT_PARAMS = ' \n' + + ' \n' + + ' \n' + + ' \n' + + ' \n' + + ' \n'; + DEFAULT_PREVIEW = '
    \n' + + ' {previewFileIcon}\n' + + '
    '; + defaultFileActionSettings = { + removeIcon: '', + removeClass: 'btn btn-xs btn-default', + removeTitle: 'Remove file', + uploadIcon: '', + uploadClass: 'btn btn-xs btn-default', + uploadTitle: 'Upload file', + indicatorNew: '', + indicatorSuccess: '', + indicatorError: '', + indicatorLoading: '', + indicatorNewTitle: 'Not uploaded yet', + indicatorSuccessTitle: 'Uploaded', + indicatorErrorTitle: 'Upload Error', + indicatorLoadingTitle: 'Uploading ...' + }; + tMain1 = '{preview}\n' + + '
    \n' + + '
    \n' + + ' {caption}\n' + + '
    \n' + + ' {remove}\n' + + ' {cancel}\n' + + ' {upload}\n' + + ' {browse}\n' + + '
    \n' + + '
    '; + tMain2 = '{preview}\n
    \n{remove}\n{cancel}\n{upload}\n{browse}\n'; + tPreview = '
    \n' + + ' {close}' + + '
    \n' + + '
    \n' + + '
    \n' + + '
    ' + + '
    \n' + + '
    \n' + + '
    \n' + + '
    '; + tClose = '
    ×
    \n'; + tIcon = ''; + tCaption = '
    \n' + + '
    \n' + + '
    \n'; + //noinspection HtmlUnknownAttribute + tBtnDefault = ''; + tBtnLink = '{icon}{label}'; + tBtnBrowse = '
    {icon}{label}
    '; + tModal = ''; + tProgress = '
    \n' + + '
    \n' + + ' {percent}%\n' + + '
    \n' + + '
    '; + tFooter = ''; + tActions = '
    \n' + + ' \n' + + '
    {indicator}
    \n' + + '
    \n' + + '
    '; + tActionDelete = '\n'; + tActionUpload = '\n'; + tZoom = '\n'; + tGeneric = '
    \n' + + ' {content}\n' + + ' {footer}\n' + + '
    \n'; + tHtml = '
    \n' + + ' \n' + + ' ' + DEFAULT_PREVIEW + '\n' + + ' \n' + + ' {footer}\n' + + '
    '; + tImage = '
    \n' + + ' {caption}\n' + + ' {footer}\n' + + '
    \n'; + tText = '
    \n' + + '
    {data}
    \n' + + ' {zoom}\n' + + ' {footer}\n' + + '
    '; + tVideo = '
    \n' + + ' \n' + + ' {footer}\n' + + '
    \n'; + tAudio = '
    \n' + + ' \n' + + ' {footer}\n' + + '
    '; + tFlash = '
    \n' + + ' \n' + + OBJECT_PARAMS + ' ' + DEFAULT_PREVIEW + '\n' + + ' \n' + + ' {footer}\n' + + '
    \n'; + tObject = '
    \n' + + ' \n' + + ' \n' + + OBJECT_PARAMS + ' ' + DEFAULT_PREVIEW + '\n' + + ' \n' + + ' {footer}\n' + + '
    '; + tOther = '
    \n' + + '
    \n' + + ' ' + DEFAULT_PREVIEW + '\n' + + '
    \n' + + ' \n' + + '
    '; + defaultLayoutTemplates = { + main1: tMain1, + main2: tMain2, + preview: tPreview, + close: tClose, + zoom: tZoom, + icon: tIcon, + caption: tCaption, + modal: tModal, + progress: tProgress, + footer: tFooter, + actions: tActions, + actionDelete: tActionDelete, + actionUpload: tActionUpload, + btnDefault: tBtnDefault, + btnLink: tBtnLink, + btnBrowse: tBtnBrowse + }; + defaultPreviewTemplates = { + generic: tGeneric, + html: tHtml, + image: tImage, + text: tText, + video: tVideo, + audio: tAudio, + flash: tFlash, + object: tObject, + other: tOther + }; + defaultPreviewTypes = ['image', 'html', 'text', 'video', 'audio', 'flash', 'object']; + defaultPreviewSettings = { + image: {width: "auto", height: "160px"}, + html: {width: "213px", height: "160px"}, + text: {width: "160px", height: "136px"}, + video: {width: "213px", height: "160px"}, + audio: {width: "213px", height: "80px"}, + flash: {width: "213px", height: "160px"}, + object: {width: "160px", height: "160px"}, + other: {width: "160px", height: "160px"} + }; + defaultFileTypeSettings = { + image: function (vType, vName) { + return (vType !== undefined) ? vType.match('image.*') : vName.match(/\.(gif|png|jpe?g)$/i); + }, + html: function (vType, vName) { + return (vType !== undefined) ? vType === 'text/html' : vName.match(/\.(htm|html)$/i); + }, + text: function (vType, vName) { + return (vType !== undefined && vType.match('text.*')) || vName.match( + /\.(txt|md|csv|nfo|ini|json|php|js|css)$/i); + }, + video: function (vType, vName) { + return (vType !== undefined && vType.match(/\.video\/(ogg|mp4|webm|3gp)$/i)) || vName.match( + /\.(og?|mp4|webm|3gp)$/i); + }, + audio: function (vType, vName) { + return (vType !== undefined && vType.match(/\.audio\/(ogg|mp3|wav)$/i)) || vName.match(/\.(ogg|mp3|wav)$/i); + }, + flash: function (vType, vName) { + return (vType !== undefined && vType === 'application/x-shockwave-flash') || vName.match(/\.(swf)$/i); + }, + object: function () { + return true; + }, + other: function () { + return true; + } + }; + isEmpty = function (value, trim) { + return value === undefined || value === null || value.length === 0 || (trim && $.trim(value) === ''); + }; + isArray = function (a) { + return Array.isArray(a) || Object.prototype.toString.call(a) === '[object Array]'; + }; + isSet = function (needle, haystack) { + return (typeof haystack === 'object' && needle in haystack); + }; + getElement = function (options, param, value) { + return (isEmpty(options) || isEmpty(options[param])) ? value : $(options[param]); + }; + uniqId = function () { + return Math.round(new Date().getTime() + (Math.random() * 100)); + }; + htmlEncode = function (str) { + return str.replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + }; + replaceTags = function (str, tags) { + var out = str; + if (!tags) { + return out; + } + $.each(tags, function (key, value) { + if (typeof value === "function") { + value = value(); + } + out = out.split(key).join(value); + }); + return out; + }; + //noinspection JSUnresolvedVariable + objUrl = window.URL || window.webkitURL; + FileInput = function (element, options) { + var self = this; + self.$element = $(element); + if (!self.validate()) { + return; + } + self.isPreviewable = hasFileAPISupport(); + self.isIE9 = isIE(9); + self.isIE10 = isIE(10); + if (self.isPreviewable || self.isIE9) { + self.init(options); + self.listen(); + } else { + self.$element.removeClass('file-loading'); + } + }; + + FileInput.prototype = { + constructor: FileInput, + validate: function () { + var self = this, $exception; + if (self.$element.attr('type') === 'file') { + return true; + } + $exception = '
    ' + + '

    Invalid Input Type

    ' + + 'You must set an input type = file for bootstrap-fileinput plugin to initialize.' + + '
    '; + self.$element.after($exception); + return false; + }, + init: function (options) { + var self = this, $el = self.$element, t; + $.each(options, function (key, value) { + switch (key) { + case 'minFileCount': + case 'maxFileCount': + case 'maxFileSize': + self[key] = getNum(value); + break; + default: + self[key] = value; + break; + } + }); + self.fileInputCleared = false; + self.fileBatchCompleted = true; + if (!self.isPreviewable) { + self.showPreview = false; + } + self.uploadFileAttr = !isEmpty($el.attr('name')) ? $el.attr('name') : 'file_data'; + self.reader = null; + self.formdata = {}; + self.clearStack(); + self.uploadCount = 0; + self.uploadStatus = {}; + self.uploadLog = []; + self.uploadAsyncCount = 0; + self.loadedImages = []; + self.totalImagesCount = 0; + self.ajaxRequests = []; + self.isError = false; + self.ajaxAborted = false; + self.cancelling = false; + t = self.getLayoutTemplate('progress'); + self.progressTemplate = t.replace('{class}', self.progressClass); + self.progressCompleteTemplate = t.replace('{class}', self.progressCompleteClass); + self.dropZoneEnabled = hasDragDropSupport() && self.dropZoneEnabled; + self.isDisabled = self.$element.attr('disabled') || self.$element.attr('readonly'); + self.isUploadable = hasFileUploadSupport() && !isEmpty(self.uploadUrl); + self.slug = typeof options.slugCallback === "function" ? options.slugCallback : self.slugDefault; + self.mainTemplate = self.showCaption ? self.getLayoutTemplate('main1') : self.getLayoutTemplate('main2'); + self.captionTemplate = self.getLayoutTemplate('caption'); + self.previewGenericTemplate = self.getPreviewTemplate('generic'); + if (self.resizeImage && (self.maxImageWidth || self.maxImageHeight)) { + self.imageCanvas = document.createElement('canvas'); + self.imageCanvasContext = self.imageCanvas.getContext('2d'); + } + if (isEmpty(self.$element.attr('id'))) { + self.$element.attr('id', uniqId()); + } + if (self.$container === undefined) { + self.$container = self.createContainer(); + } else { + self.refreshContainer(); + } + self.$dropZone = self.$container.find('.file-drop-zone'); + self.$progress = self.$container.find('.kv-upload-progress'); + self.$btnUpload = self.$container.find('.fileinput-upload'); + self.$captionContainer = getElement(options, 'elCaptionContainer', self.$container.find('.file-caption')); + self.$caption = getElement(options, 'elCaptionText', self.$container.find('.file-caption-name')); + self.$previewContainer = getElement(options, 'elPreviewContainer', self.$container.find('.file-preview')); + self.$preview = getElement(options, 'elPreviewImage', self.$container.find('.file-preview-thumbnails')); + self.$previewStatus = getElement(options, 'elPreviewStatus', self.$container.find('.file-preview-status')); + self.$errorContainer = getElement(options, 'elErrorContainer', + self.$previewContainer.find('.kv-fileinput-error')); + if (!isEmpty(self.msgErrorClass)) { + addCss(self.$errorContainer, self.msgErrorClass); + } + self.$errorContainer.hide(); + self.fileActionSettings = $.extend(true, defaultFileActionSettings, options.fileActionSettings); + self.previewInitId = "preview-" + uniqId(); + self.id = self.$element.attr('id'); + previewCache.init(self); + self.initPreview(true); + self.initPreviewDeletes(); + self.options = options; + self.setFileDropZoneTitle(); + self.$element.removeClass('file-loading'); + if (self.$element.attr('disabled')) { + self.disable(); + } + }, + parseError: function (jqXHR, errorThrown, fileName) { + /** @namespace jqXHR.responseJSON */ + var self = this, errMsg = $.trim(errorThrown + ''), + dot = errMsg.slice(-1) === '.' ? '' : '.', + text = jqXHR.responseJSON !== undefined && jqXHR.responseJSON.error !== undefined ? + jqXHR.responseJSON.error : jqXHR.responseText; + if (self.cancelling && self.msgUploadAborted) { + errMsg = self.msgUploadAborted; + } + if (self.showAjaxErrorDetails && text) { + text = $.trim(text.replace(/\n\s*\n/g, '\n')); + text = text.length > 0 ? '
    ' + text + '
    ' : ''; + errMsg += dot + text; + } else { + errMsg += dot; + } + self.cancelling = false; + return fileName ? '' + fileName + ': ' + errMsg : errMsg; + }, + raise: function (event, params) { + var self = this, e = $.Event(event); + if (params !== undefined) { + self.$element.trigger(e, params); + } else { + self.$element.trigger(e); + } + if (e.isDefaultPrevented()) { + return false; + } + if (!e.result) { + return e.result; + } + switch (event) { + // ignore these events + case 'filebatchuploadcomplete': + case 'filebatchuploadsuccess': + case 'fileuploaded': + case 'fileclear': + case 'filecleared': + case 'filereset': + case 'fileerror': + case 'filefoldererror': + case 'fileuploaderror': + case 'filebatchuploaderror': + case 'filedeleteerror': + case 'filecustomerror': + case 'filesuccessremove': + break; + // receive data response via `filecustomerror` event` + default: + self.ajaxAborted = e.result; + break; + } + return true; + }, + getLayoutTemplate: function (t) { + var self = this, + template = isSet(t, self.layoutTemplates) ? self.layoutTemplates[t] : defaultLayoutTemplates[t]; + if (isEmpty(self.customLayoutTags)) { + return template; + } + return replaceTags(template, self.customLayoutTags); + }, + getPreviewTemplate: function (t) { + var self = this, + template = isSet(t, self.previewTemplates) ? self.previewTemplates[t] : defaultPreviewTemplates[t]; + if (isEmpty(self.customPreviewTags)) { + return template; + } + return replaceTags(template, self.customPreviewTags); + }, + parseFilePreviewIcon: function (content, fname) { + var self = this, ext, icn = self.previewFileIcon; + if (fname && fname.indexOf('.') > -1) { + ext = fname.split('.').pop(); + if (self.previewFileIconSettings && self.previewFileIconSettings[ext]) { + icn = self.previewFileIconSettings[ext]; + } + if (self.previewFileExtSettings) { + $.each(self.previewFileExtSettings, function (key, func) { + if (self.previewFileIconSettings[key] && func(ext)) { + icn = self.previewFileIconSettings[key]; + } + }); + } + } + if (content.indexOf('{previewFileIcon}') > -1) { + return content.replace(/\{previewFileIconClass}/g, self.previewFileIconClass).replace( + /\{previewFileIcon}/g, icn); + } + return content; + }, + getOutData: function (jqXHR, responseData, filesData) { + var self = this; + jqXHR = jqXHR || {}; + responseData = responseData || {}; + filesData = filesData || self.filestack.slice(0) || {}; + return { + form: self.formdata, + files: filesData, + filenames: self.filenames, + extra: self.getExtraData(), + response: responseData, + reader: self.reader, + jqXHR: jqXHR + }; + }, + listen: function () { + var self = this, $el = self.$element, $form = $el.closest('form'), $cont = self.$container; + handler($el, 'change', $.proxy(self.change, self)); + handler(self.$btnFile, 'click', $.proxy(self.browse, self)); + handler($form, 'reset', $.proxy(self.reset, self)); + handler($cont.find('.fileinput-remove:not([disabled])'), 'click', $.proxy(self.clear, self)); + handler($cont.find('.fileinput-cancel'), 'click', $.proxy(self.cancel, self)); + self.initDragDrop(); + if (!self.isUploadable) { + handler($form, 'submit', $.proxy(self.submitForm, self)); + } + handler(self.$container.find('.fileinput-upload'), 'click', $.proxy(self.uploadClick, self)); + }, + zoneDragDropInit: function (e) { + e.stopPropagation(); + e.preventDefault(); + }, + zoneDragEnter: function (e) { + var self = this, hasFiles = $.inArray('Files', e.originalEvent.dataTransfer.types) > -1; + self.zoneDragDropInit(e); + if (self.isDisabled || !hasFiles) { + e.originalEvent.dataTransfer.effectAllowed = 'none'; + e.originalEvent.dataTransfer.dropEffect = 'none'; + return; + } + addCss(self.$dropZone, 'file-highlighted'); + }, + zoneDragLeave: function (e) { + var self = this; + self.zoneDragDropInit(e); + if (self.isDisabled) { + return; + } + self.$dropZone.removeClass('file-highlighted'); + }, + zoneDrop: function (e) { + var self = this; + e.preventDefault(); + /** @namespace e.originalEvent.dataTransfer */ + if (self.isDisabled || isEmpty(e.originalEvent.dataTransfer.files)) { + return; + } + self.change(e, 'dragdrop'); + self.$dropZone.removeClass('file-highlighted'); + }, + initDragDrop: function () { + var self = this, $zone = self.$dropZone; + if (self.isUploadable && self.dropZoneEnabled && self.showPreview) { + handler($zone, 'dragenter dragover', $.proxy(self.zoneDragEnter, self)); + handler($zone, 'dragleave', $.proxy(self.zoneDragLeave, self)); + handler($zone, 'drop', $.proxy(self.zoneDrop, self)); + handler($(document), 'dragenter dragover drop', self.zoneDragDropInit); + } + }, + browse: function (e) { + var self = this; + self.raise('filebrowse'); + if (e && e.isDefaultPrevented()) { + return; + } + if (self.isError && !self.isUploadable) { + self.clear(); + } + self.$captionContainer.focus(); + }, + uploadClick: function (e) { + var self = this, $btn = self.$container.find('.fileinput-upload'), $form, + isEnabled = !$btn.hasClass('disabled') && isEmpty($btn.attr('disabled')); + if (e && e.isDefaultPrevented()) { + return; + } + if (!self.isUploadable) { + if (isEnabled && $btn.attr('type') !== 'submit') { + $form = $btn.closest('form'); + // downgrade to normal form submit if possible + if ($form.length) { + $form.trigger('submit'); + } + e.preventDefault(); + } + return; + } + e.preventDefault(); + if (isEnabled) { + self.upload(); + } + }, + submitForm: function () { + var self = this, $el = self.$element, files = $el.get(0).files; + if (files && self.minFileCount > 0 && self.getFileCount(files.length) < self.minFileCount) { + self.noFilesError({}); + return false; + } + return !self.abort({}); + }, + abort: function (params) { + var self = this, data; + if (self.ajaxAborted && typeof self.ajaxAborted === "object" && self.ajaxAborted.message !== undefined) { + data = $.extend(true, {}, self.getOutData(), params); + data.abortData = self.ajaxAborted.data || {}; + data.abortMessage = self.ajaxAborted.message; + self.cancel(); + self.setProgress(100); + self.showUploadError(self.ajaxAborted.message, data, 'filecustomerror'); + return true; + } + return false; + }, + noFilesError: function (params) { + var self = this, label = self.minFileCount > 1 ? self.filePlural : self.fileSingle, + msg = self.msgFilesTooLess.replace('{n}', self.minFileCount).replace('{files}', label), + $error = self.$errorContainer; + self.addError(msg); + self.isError = true; + self.updateFileDetails(0); + $error.fadeIn(800); + self.raise('fileerror', [params]); + self.clearFileInput(); + addCss(self.$container, 'has-error'); + }, + setProgress: function (p, $el) { + var self = this, pct = Math.min(p, 100), + template = pct < 100 ? self.progressTemplate : self.progressCompleteTemplate; + $el = $el || self.$progress; + if (!isEmpty(template)) { + $el.html(template.replace(/\{percent}/g, pct)); + } + }, + lock: function () { + var self = this; + self.resetErrors(); + self.disable(); + if (self.showRemove) { + addCss(self.$container.find('.fileinput-remove'), 'hide'); + } + if (self.showCancel) { + self.$container.find('.fileinput-cancel').removeClass('hide'); + } + self.raise('filelock', [self.filestack, self.getExtraData()]); + }, + unlock: function (reset) { + var self = this; + if (reset === undefined) { + reset = true; + } + self.enable(); + if (self.showCancel) { + addCss(self.$container.find('.fileinput-cancel'), 'hide'); + } + if (self.showRemove) { + self.$container.find('.fileinput-remove').removeClass('hide'); + } + if (reset) { + self.resetFileStack(); + } + self.raise('fileunlock', [self.filestack, self.getExtraData()]); + }, + resetFileStack: function () { + var self = this, i = 0, newstack = [], newnames = []; + self.getThumbs().each(function () { + var $thumb = $(this), ind = $thumb.attr('data-fileindex'), + file = self.filestack[ind]; + if (ind === -1) { + return; + } + if (file !== undefined) { + newstack[i] = file; + newnames[i] = self.getFileName(file); + $thumb.attr({ + 'id': self.previewInitId + '-' + i, + 'data-fileindex': i + }); + i++; + } else { + $thumb.attr({ + 'id': 'uploaded-' + uniqId(), + 'data-fileindex': '-1' + }); + } + }); + self.filestack = newstack; + self.filenames = newnames; + }, + destroy: function () { + var self = this, $cont = self.$container; + $cont.find('.file-drop-zone').off(); + self.$element.insertBefore($cont).off(NAMESPACE).removeData(); + $cont.off().remove(); + }, + refresh: function (options) { + var self = this, $el = self.$element; + options = options ? $.extend(true, {}, self.options, options) : self.options; + self.destroy(); + $el.fileinput(options); + if ($el.val()) { + $el.trigger('change.fileinput'); + } + }, + setFileDropZoneTitle: function () { + var self = this, $zone = self.$container.find('.file-drop-zone'); + $zone.find('.' + self.dropZoneTitleClass).remove(); + if (!self.isUploadable || !self.showPreview || $zone.length === 0 || self.getFileStack().length > 0 || !self.dropZoneEnabled) { + return; + } + if ($zone.find('.file-preview-frame').length === 0 && isEmpty(self.defaultPreviewContent)) { + $zone.prepend('
    ' + self.dropZoneTitle + '
    '); + } + self.$container.removeClass('file-input-new'); + addCss(self.$container, 'file-input-ajax-new'); + }, + errorsExist: function () { + var self = this, $err; + if (self.$errorContainer.find('li').length) { + return true; + } + $err = $(document.createElement('div')).html(self.$errorContainer.html()); + $err.find('span.kv-error-close').remove(); + $err.find('ul').remove(); + return $.trim($err.text()).length ? true : false; + }, + getMsgSelected: function (n) { + var self = this, strFiles = n === 1 ? self.fileSingle : self.filePlural; + return self.msgSelected.replace('{n}', n).replace('{files}', strFiles); + }, + renderThumbProgress: function () { + return '
    ' + this.progressTemplate.replace(/\{percent}/g, + '0') + '
    '; + }, + renderFileFooter: function (caption, width) { + var self = this, config = self.fileActionSettings, footer, out, template = self.getLayoutTemplate('footer'); + if (self.isUploadable) { + footer = template.replace(/\{actions}/g, self.renderFileActions(true, true, false, false, false)); + out = footer.replace(/\{caption}/g, caption) + .replace(/\{width}/g, width) + .replace(/\{progress}/g, self.renderThumbProgress()) + .replace(/\{indicator}/g, config.indicatorNew) + .replace(/\{indicatorTitle}/g, config.indicatorNewTitle); + } else { + out = template.replace(/\{actions}/g, '') + .replace(/\{caption}/g, caption) + .replace(/\{progress}/g, '') + .replace(/\{width}/g, width) + .replace(/\{indicator}/g, '') + .replace(/\{indicatorTitle}/g, ''); + } + out = replaceTags(out, self.previewThumbTags); + return out; + }, + renderFileActions: function (showUpload, showDelete, disabled, url, key) { + if (!showUpload && !showDelete) { + return ''; + } + var self = this, + vUrl = url === false ? '' : ' data-url="' + url + '"', + vKey = key === false ? '' : ' data-key="' + key + '"', + btnDelete = self.getLayoutTemplate('actionDelete'), + btnUpload = '', + template = self.getLayoutTemplate('actions'), + otherButtons = self.otherActionButtons.replace(/\{dataKey}/g, vKey), + config = self.fileActionSettings, + removeClass = disabled ? config.removeClass + ' disabled' : config.removeClass; + btnDelete = btnDelete + .replace(/\{removeClass}/g, removeClass) + .replace(/\{removeIcon}/g, config.removeIcon) + .replace(/\{removeTitle}/g, config.removeTitle) + .replace(/\{dataUrl}/g, vUrl) + .replace(/\{dataKey}/g, vKey); + if (showUpload) { + btnUpload = self.getLayoutTemplate('actionUpload') + .replace(/\{uploadClass}/g, config.uploadClass) + .replace(/\{uploadIcon}/g, config.uploadIcon) + .replace(/\{uploadTitle}/g, config.uploadTitle); + } + return template + .replace(/\{delete}/g, btnDelete) + .replace(/\{upload}/g, btnUpload) + .replace(/\{other}/g, otherButtons); + }, + setThumbStatus: function ($thumb, status) { + var self = this; + if (!self.showPreview) { + return; + } + var icon = 'indicator' + status, msg = icon + 'Title', + css = 'file-preview-' + status.toLowerCase(), + $indicator = $thumb.find('.file-upload-indicator'), + config = self.fileActionSettings; + $thumb.removeClass('file-preview-success file-preview-error file-preview-loading'); + if (status === 'Error') { + $thumb.find('.kv-file-upload').attr('disabled', true); + } + $indicator.html(config[icon]); + $indicator.attr('title', config[msg]); + $thumb.addClass(css); + }, + clearPreview: function () { + var self = this, $thumbs = !self.showUploadedThumbs ? self.$preview.find('.file-preview-frame') : + self.$preview.find('.file-preview-frame:not(.file-preview-success)'); + $thumbs.remove(); + if (!self.$preview.find('.file-preview-frame').length || !self.showPreview) { + self.resetUpload(); + } + self.validateDefaultPreview(); + }, + initPreview: function (isInit) { + var self = this, cap = self.initialCaption || '', out; + if (!previewCache.count(self.id)) { + self.clearPreview(); + if (isInit) { + self.setCaption(cap); + } else { + self.initCaption(); + } + return; + } + out = previewCache.out(self.id); + cap = isInit && self.initialCaption ? self.initialCaption : out.caption; + self.$preview.html(out.content); + self.setCaption(cap); + if (!isEmpty(out.content)) { + self.$container.removeClass('file-input-new'); + } + }, + initPreviewDeletes: function () { + var self = this, deleteExtraData = self.deleteExtraData || {}, + resetProgress = function () { + var hasFiles = self.isUploadable ? previewCache.count(self.id) : self.$element.get(0).files.length; + if (self.$preview.find('.kv-file-remove').length === 0 && !hasFiles) { + self.reset(); + self.initialCaption = ''; + } + }; + + self.$preview.find('.kv-file-remove').each(function () { + var $el = $(this), vUrl = $el.data('url') || self.deleteUrl, vKey = $el.data('key'); + if (isEmpty(vUrl) || vKey === undefined) { + return; + } + var $frame = $el.closest('.file-preview-frame'), cache = previewCache.data[self.id], + settings, params, index = $frame.data('fileindex'), config, extraData; + index = parseInt(index.replace('init_', '')); + config = isEmpty(cache.config) && isEmpty(cache.config[index]) ? null : cache.config[index]; + extraData = isEmpty(config) || isEmpty(config.extra) ? deleteExtraData : config.extra; + if (typeof extraData === "function") { + extraData = extraData(); + } + params = {id: $el.attr('id'), key: vKey, extra: extraData}; + settings = $.extend(true, {}, { + url: vUrl, + type: 'POST', + dataType: 'json', + data: $.extend(true, {}, {key: vKey}, extraData), + beforeSend: function (jqXHR) { + self.ajaxAborted = false; + self.raise('filepredelete', [vKey, jqXHR, extraData]); + if (self.ajaxAborted) { + jqXHR.abort(); + } else { + addCss($frame, 'file-uploading'); + addCss($el, 'disabled'); + } + }, + success: function (data, textStatus, jqXHR) { + var n, cap; + if (isEmpty(data) || isEmpty(data.error)) { + previewCache.unset(self.id, index); + n = previewCache.count(self.id); + cap = n > 0 ? self.getMsgSelected(n) : ''; + self.raise('filedeleted', [vKey, jqXHR, extraData]); + self.setCaption(cap); + } else { + params.jqXHR = jqXHR; + params.response = data; + self.showError(data.error, params, 'filedeleteerror'); + $frame.removeClass('file-uploading'); + $el.removeClass('disabled'); + resetProgress(); + return; + } + $frame.removeClass('file-uploading').addClass('file-deleted'); + $frame.fadeOut('slow', function () { + self.clearObjects($frame); + $frame.remove(); + resetProgress(); + if (!n && self.getFileStack().length === 0) { + self.setCaption(''); + self.reset(); + } + }); + }, + error: function (jqXHR, textStatus, errorThrown) { + var errMsg = self.parseError(jqXHR, errorThrown); + params.jqXHR = jqXHR; + params.response = {}; + self.showError(errMsg, params, 'filedeleteerror'); + $frame.removeClass('file-uploading'); + resetProgress(); + } + }, self.ajaxDeleteSettings); + handler($el, 'click', function () { + if (!self.validateMinCount()) { + return false; + } + $.ajax(settings); + }); + }); + }, + clearObjects: function ($el) { + $el.find('video audio').each(function () { + this.pause(); + $(this).remove(); + }); + $el.find('img object div').each(function () { + $(this).remove(); + }); + }, + clearFileInput: function () { + var self = this, $el = self.$element, $srcFrm, $tmpFrm, $tmpEl; + if (isEmpty($el.val())) { + return; + } + // Fix for IE ver < 11, that does not clear file input + // Requires a sequence of steps to prevent IE crashing but + // still allow clearing of the file input. + if (self.isIE9 || self.isIE10) { + $srcFrm = $el.closest('form'); + $tmpFrm = $(document.createElement('form')); + $tmpEl = $(document.createElement('div')); + $el.before($tmpEl); + if ($srcFrm.length) { + $srcFrm.after($tmpFrm); + } else { + $tmpEl.after($tmpFrm); + } + $tmpFrm.append($el).trigger('reset'); + $tmpEl.before($el).remove(); + $tmpFrm.remove(); + } else { // normal input clear behavior for other sane browsers + $el.val(''); + } + self.fileInputCleared = true; + }, + resetUpload: function () { + var self = this; + self.uploadCache = {content: [], config: [], tags: [], append: true}; + self.uploadCount = 0; + self.uploadStatus = {}; + self.uploadLog = []; + self.uploadAsyncCount = 0; + self.loadedImages = []; + self.totalImagesCount = 0; + self.$btnUpload.removeAttr('disabled'); + self.setProgress(0); + addCss(self.$progress, 'hide'); + self.resetErrors(false); + self.ajaxAborted = false; + self.ajaxRequests = []; + self.resetCanvas(); + }, + resetCanvas: function () { + var self = this; + if (self.canvas && self.imageCanvasContext) { + self.imageCanvasContext.clearRect(0, 0, self.canvas.width, self.canvas.height); + } + }, + cancel: function () { + var self = this, xhr = self.ajaxRequests, len = xhr.length, i; + if (len > 0) { + for (i = 0; i < len; i += 1) { + self.cancelling = true; + xhr[i].abort(); + } + } + self.getThumbs().each(function () { + var $thumb = $(this), ind = $thumb.attr('data-fileindex'); + $thumb.removeClass('file-uploading'); + if (self.filestack[ind] !== undefined) { + $thumb.find('.kv-file-upload').removeClass('disabled').removeAttr('disabled'); + $thumb.find('.kv-file-remove').removeClass('disabled').removeAttr('disabled'); + } + self.unlock(); + }); + }, + cleanMemory: function ($thumb) { + var data = $thumb.is('img') ? $thumb.attr('src') : $thumb.find('source').attr('src'); + /** @namespace objUrl.revokeObjectURL */ + objUrl.revokeObjectURL(data); + }, + hasInitialPreview: function () { + var self = this; + return !self.overwriteInitial && previewCache.count(self.id); + }, + clear: function () { + var self = this, cap; + self.$btnUpload.removeAttr('disabled'); + self.getThumbs().find('video,audio,img').each(function () { + self.cleanMemory($(this)); + }); + self.resetUpload(); + self.clearStack(); + self.clearFileInput(); + self.resetErrors(true); + self.raise('fileclear'); + if (self.hasInitialPreview()) { + self.showFileIcon(); + self.resetPreview(); + self.initPreviewDeletes(); + self.$container.removeClass('file-input-new'); + } else { + self.getThumbs().each(function () { + self.clearObjects($(this)); + }); + if (self.isUploadable) { + previewCache.data[self.id] = {}; + } + self.$preview.html(''); + cap = (!self.overwriteInitial && self.initialCaption.length > 0) ? self.initialCaption : ''; + self.setCaption(cap); + self.$caption.attr('title', ''); + addCss(self.$container, 'file-input-new'); + self.validateDefaultPreview(); + } + if (self.$container.find('.file-preview-frame').length === 0) { + if (!self.initCaption()) { + self.$captionContainer.find('.kv-caption-icon').hide(); + } + } + self.hideFileIcon(); + self.raise('filecleared'); + self.$captionContainer.focus(); + self.setFileDropZoneTitle(); + }, + resetPreview: function () { + var self = this, out, cap; + if (previewCache.count(self.id)) { + out = previewCache.out(self.id); + self.$preview.html(out.content); + cap = self.initialCaption ? self.initialCaption : out.caption; + self.setCaption(cap); + } else { + self.clearPreview(); + self.initCaption(); + } + }, + clearDefaultPreview: function () { + var self = this; + self.$preview.find('.file-default-preview').remove(); + }, + validateDefaultPreview: function () { + var self = this; + if (!self.showPreview || isEmpty(self.defaultPreviewContent)) { + return; + } + self.$preview.html('
    ' + self.defaultPreviewContent + '
    '); + self.$container.removeClass('file-input-new'); + }, + resetPreviewThumbs: function (isAjax) { + var self = this, out; + if (isAjax) { + self.clearPreview(); + self.clearStack(); + return; + } + if (self.hasInitialPreview()) { + out = previewCache.out(self.id); + self.$preview.html(out.content); + self.setCaption(out.caption); + self.initPreviewDeletes(); + } else { + self.clearPreview(); + } + }, + reset: function () { + var self = this; + self.resetPreview(); + self.$container.find('.fileinput-filename').text(''); + self.raise('filereset'); + addCss(self.$container, 'file-input-new'); + if (self.$preview.find('.file-preview-frame').length || self.isUploadable && self.dropZoneEnabled) { + self.$container.removeClass('file-input-new'); + } + self.setFileDropZoneTitle(); + self.clearStack(); + self.formdata = {}; + }, + disable: function () { + var self = this; + self.isDisabled = true; + self.raise('filedisabled'); + self.$element.attr('disabled', 'disabled'); + self.$container.find(".kv-fileinput-caption").addClass("file-caption-disabled"); + self.$container.find(".btn-file, .fileinput-remove, .fileinput-upload, .file-preview-frame button").attr( + "disabled", + true); + self.initDragDrop(); + }, + enable: function () { + var self = this; + self.isDisabled = false; + self.raise('fileenabled'); + self.$element.removeAttr('disabled'); + self.$container.find(".kv-fileinput-caption").removeClass("file-caption-disabled"); + self.$container.find( + ".btn-file, .fileinput-remove, .fileinput-upload, .file-preview-frame button").removeAttr("disabled"); + self.initDragDrop(); + }, + getThumbs: function (css) { + css = css || ''; + return this.$preview.find('.file-preview-frame:not(.file-preview-initial)' + css); + }, + getExtraData: function (previewId, index) { + var self = this, data = self.uploadExtraData; + if (typeof self.uploadExtraData === "function") { + data = self.uploadExtraData(previewId, index); + } + return data; + }, + uploadExtra: function (previewId, index) { + var self = this, data = self.getExtraData(previewId, index); + if (data.length === 0) { + return; + } + $.each(data, function (key, value) { + self.formdata.append(key, value); + }); + }, + setAsyncUploadStatus: function (previewId, pct, total) { + var self = this, sum = 0; + self.setProgress(pct, $('#' + previewId).find('.file-thumb-progress')); + self.uploadStatus[previewId] = pct; + $.each(self.uploadStatus, function (key, value) { + sum += value; + }); + self.setProgress(Math.ceil(sum / total)); + + }, + initXhr: function (xhrobj, previewId, fileCount) { + var self = this; + if (xhrobj.upload) { + xhrobj.upload.addEventListener('progress', function (event) { + var pct = 0, position = event.loaded || event.position, total = event.total; + /** @namespace event.lengthComputable */ + if (event.lengthComputable) { + pct = Math.ceil(position / total * 100); + } + if (previewId) { + self.setAsyncUploadStatus(previewId, pct, fileCount); + } else { + self.setProgress(Math.ceil(pct)); + } + }, false); + } + return xhrobj; + }, + ajaxSubmit: function (fnBefore, fnSuccess, fnComplete, fnError, previewId, index) { + var self = this, settings; + self.raise('filepreajax', [previewId, index]); + self.uploadExtra(previewId, index); + settings = $.extend(true, {}, { + xhr: function () { + var xhrobj = $.ajaxSettings.xhr(); + return self.initXhr(xhrobj, previewId, self.getFileStack().length); + }, + url: self.uploadUrl, + type: 'POST', + dataType: 'json', + data: self.formdata, + cache: false, + processData: false, + contentType: false, + beforeSend: fnBefore, + success: fnSuccess, + complete: fnComplete, + error: fnError + }, self.ajaxSettings); + self.ajaxRequests.push($.ajax(settings)); + }, + initUploadSuccess: function (out, $thumb, allFiles) { + var self = this, append, data, index, $newThumb, content, config, tags, i; + if (!self.showPreview || typeof out !== 'object' || $.isEmptyObject(out)) { + return; + } + if (out.initialPreview !== undefined && out.initialPreview.length > 0) { + self.hasInitData = true; + content = out.initialPreview || []; + config = out.initialPreviewConfig || []; + tags = out.initialPreviewThumbTags || []; + append = out.append === undefined || out.append ? true : false; + self.overwriteInitial = false; + if ($thumb !== undefined) { + if (!allFiles) { + index = previewCache.add(self.id, content, config[0], tags[0], append); + data = previewCache.get(self.id, index, false); + $newThumb = $(data).hide(); + $thumb.after($newThumb).fadeOut('slow', function () { + $newThumb.fadeIn('slow').css('display:inline-block'); + self.initPreviewDeletes(); + self.clearFileInput(); + $thumb.remove(); + }); + } else { + i = $thumb.attr('data-fileindex'); + self.uploadCache.content[i] = content[0]; + self.uploadCache.config[i] = config[0]; + self.uploadCache.tags[i] = tags[0]; + self.uploadCache.append = append; + } + } else { + previewCache.set(self.id, content, config, tags, append); + self.initPreview(); + self.initPreviewDeletes(); + } + } + }, + initSuccessThumbs: function () { + var self = this; + if (!self.showPreview) { + return; + } + self.getThumbs('.file-preview-success').each(function () { + var $thumb = $(this), $remove = $thumb.find('.kv-file-remove'); + $remove.removeAttr('disabled'); + handler($remove, 'click', function () { + var out = self.raise('filesuccessremove', [$thumb.attr('id'), $thumb.data('fileindex')]); + self.cleanMemory($thumb); + if (out === false) { + return; + } + $thumb.fadeOut('slow', function () { + $thumb.remove(); + if (!self.$preview.find('.file-preview-frame').length) { + self.reset(); + } + }); + }); + }); + }, + checkAsyncComplete: function () { + var self = this, previewId, i; + for (i = 0; i < self.filestack.length; i++) { + if (self.filestack[i]) { + previewId = self.previewInitId + "-" + i; + if ($.inArray(previewId, self.uploadLog) === -1) { + return false; + } + } + } + return (self.uploadAsyncCount === self.uploadLog.length); + }, + uploadSingle: function (i, files, allFiles) { + var self = this, total = self.getFileStack().length, formdata = new FormData(), outData, + previewId = self.previewInitId + "-" + i, $thumb, chkComplete, $btnUpload, $btnDelete, + hasPostData = self.filestack.length > 0 || !$.isEmptyObject(self.uploadExtraData), + fnBefore, fnSuccess, fnComplete, fnError, updateUploadLog, params = {id: previewId, index: i}; + self.formdata = formdata; + if (self.showPreview) { + $thumb = $('#' + previewId + ':not(.file-preview-initial)'); + $btnUpload = $thumb.find('.kv-file-upload'); + $btnDelete = $thumb.find('.kv-file-remove'); + $('#' + previewId).find('.file-thumb-progress').removeClass('hide'); + } + if (total === 0 || !hasPostData || ($btnUpload && $btnUpload.hasClass('disabled')) || self.abort(params)) { + return; + } + updateUploadLog = function (i, previewId) { + self.updateStack(i, undefined); + self.uploadLog.push(previewId); + if (self.checkAsyncComplete()) { + self.fileBatchCompleted = true; + } + }; + chkComplete = function () { + if (!self.fileBatchCompleted) { + return; + } + setTimeout(function () { + if (self.showPreview) { + previewCache.set( + self.id, + self.uploadCache.content, + self.uploadCache.config, + self.uploadCache.tags, + self.uploadCache.append + ); + if (self.hasInitData) { + self.initPreview(); + self.initPreviewDeletes(); + } + } + self.unlock(); + self.clearFileInput(); + self.raise('filebatchuploadcomplete', [self.filestack, self.getExtraData()]); + self.uploadCount = 0; + self.uploadStatus = {}; + self.uploadLog = []; + self.setProgress(100); + }, 100); + }; + fnBefore = function (jqXHR) { + outData = self.getOutData(jqXHR); + self.fileBatchCompleted = false; + if (self.showPreview) { + if (!$thumb.hasClass('file-preview-success')) { + self.setThumbStatus($thumb, 'Loading'); + addCss($thumb, 'file-uploading'); + } + $btnUpload.attr('disabled', true); + $btnDelete.attr('disabled', true); + } + if (!allFiles) { + self.lock(); + } + self.raise('filepreupload', [outData, previewId, i]); + $.extend(true, params, outData); + if (self.abort(params)) { + jqXHR.abort(); + self.setProgress(100); + } + }; + fnSuccess = function (data, textStatus, jqXHR) { + outData = self.getOutData(jqXHR, data); + $.extend(true, params, outData); + setTimeout(function () { + if (isEmpty(data) || isEmpty(data.error)) { + if (self.showPreview) { + self.setThumbStatus($thumb, 'Success'); + $btnUpload.hide(); + self.initUploadSuccess(data, $thumb, allFiles); + } + self.raise('fileuploaded', [outData, previewId, i]); + if (!allFiles) { + self.updateStack(i, undefined); + } else { + updateUploadLog(i, previewId); + } + } else { + self.showUploadError(data.error, params); + self.setPreviewError($thumb, i); + if (allFiles) { + updateUploadLog(i, previewId); + } + } + }, 100); + }; + fnComplete = function () { + setTimeout(function () { + if (self.showPreview) { + $btnUpload.removeAttr('disabled'); + $btnDelete.removeAttr('disabled'); + $thumb.removeClass('file-uploading'); + } + if (!allFiles) { + self.unlock(false); + self.clearFileInput(); + } else { + chkComplete(); + } + self.initSuccessThumbs(); + }, 100); + }; + fnError = function (jqXHR, textStatus, errorThrown) { + var errMsg = self.parseError(jqXHR, errorThrown, (allFiles ? files[i].name : null)); + setTimeout(function () { + if (allFiles) { + updateUploadLog(i, previewId); + } + self.uploadStatus[previewId] = 100; + self.setPreviewError($thumb, i); + $.extend(true, params, self.getOutData(jqXHR)); + self.showUploadError(errMsg, params); + }, 100); + }; + formdata.append(self.uploadFileAttr, files[i], self.filenames[i]); + formdata.append('file_id', i); + self.ajaxSubmit(fnBefore, fnSuccess, fnComplete, fnError, previewId, i); + }, + uploadBatch: function () { + var self = this, files = self.filestack, total = files.length, params = {}, fnBefore, fnSuccess, fnError, + fnComplete, hasPostData = self.filestack.length > 0 || !$.isEmptyObject(self.uploadExtraData), + setAllUploaded; + self.formdata = new FormData(); + if (total === 0 || !hasPostData || self.abort(params)) { + return; + } + setAllUploaded = function () { + $.each(files, function (key) { + self.updateStack(key, undefined); + }); + self.clearFileInput(); + }; + fnBefore = function (jqXHR) { + self.lock(); + var outData = self.getOutData(jqXHR); + if (self.showPreview) { + self.getThumbs().each(function () { + var $thumb = $(this), $btnUpload = $thumb.find('.kv-file-upload'), + $btnDelete = $thumb.find('.kv-file-remove'); + if (!$thumb.hasClass('file-preview-success')) { + self.setThumbStatus($thumb, 'Loading'); + addCss($thumb, 'file-uploading'); + } + $btnUpload.attr('disabled', true); + $btnDelete.attr('disabled', true); + }); + } + self.raise('filebatchpreupload', [outData]); + if (self.abort(outData)) { + jqXHR.abort(); + self.setProgress(100); + } + }; + fnSuccess = function (data, textStatus, jqXHR) { + /** @namespace data.errorkeys */ + var outData = self.getOutData(jqXHR, data), $thumbs = self.getThumbs(), key = 0, + keys = isEmpty(data) || isEmpty(data.errorkeys) ? [] : data.errorkeys; + if (isEmpty(data) || isEmpty(data.error)) { + self.raise('filebatchuploadsuccess', [outData]); + setAllUploaded(); + if (self.showPreview) { + $thumbs.each(function () { + var $thumb = $(this), $btnUpload = $thumb.find('.kv-file-upload'); + $thumb.find('.kv-file-upload').hide(); + self.setThumbStatus($thumb, 'Success'); + $thumb.removeClass('file-uploading'); + $btnUpload.removeAttr('disabled'); + }); + self.initUploadSuccess(data); + } else { + self.reset(); + } + } else { + if (self.showPreview) { + $thumbs.each(function () { + var $thumb = $(this), $btnDelete = $thumb.find('.kv-file-remove'), + $btnUpload = $thumb.find('.kv-file-upload'); + $thumb.removeClass('file-uploading'); + $btnUpload.removeAttr('disabled'); + $btnDelete.removeAttr('disabled'); + if (keys.length === 0) { + self.setPreviewError($thumb); + return; + } + if ($.inArray(key, keys) !== -1) { + self.setPreviewError($thumb); + } else { + $thumb.find('.kv-file-upload').hide(); + self.setThumbStatus($thumb, 'Success'); + self.updateStack(key, undefined); + } + key++; + }); + self.initUploadSuccess(data); + } + self.showUploadError(data.error, outData, 'filebatchuploaderror'); + } + }; + fnComplete = function () { + self.setProgress(100); + self.unlock(); + self.initSuccessThumbs(); + self.clearFileInput(); + self.raise('filebatchuploadcomplete', [self.filestack, self.getExtraData()]); + }; + fnError = function (jqXHR, textStatus, errorThrown) { + var outData = self.getOutData(jqXHR), errMsg = self.parseError(jqXHR, errorThrown); + self.showUploadError(errMsg, outData, 'filebatchuploaderror'); + self.uploadFileCount = total - 1; + if (!self.showPreview) { + return; + } + self.getThumbs().each(function () { + var $thumb = $(this), key = $thumb.attr('data-fileindex'); + $thumb.removeClass('file-uploading'); + if (self.filestack[key] !== undefined) { + self.setPreviewError($thumb); + } + }); + self.getThumbs().removeClass('file-uploading'); + self.getThumbs(' .kv-file-upload').removeAttr('disabled'); + self.getThumbs(' .kv-file-delete').removeAttr('disabled'); + }; + $.each(files, function (key, data) { + if (!isEmpty(files[key])) { + self.formdata.append(self.uploadFileAttr, data, self.filenames[key]); + } + }); + self.ajaxSubmit(fnBefore, fnSuccess, fnComplete, fnError); + }, + uploadExtraOnly: function () { + var self = this, params = {}, fnBefore, fnSuccess, fnComplete, fnError; + self.formdata = new FormData(); + if (self.abort(params)) { + return; + } + fnBefore = function (jqXHR) { + self.lock(); + var outData = self.getOutData(jqXHR); + self.raise('filebatchpreupload', [outData]); + self.setProgress(50); + params.data = outData; + params.xhr = jqXHR; + if (self.abort(params)) { + jqXHR.abort(); + self.setProgress(100); + } + }; + fnSuccess = function (data, textStatus, jqXHR) { + var outData = self.getOutData(jqXHR, data); + if (isEmpty(data) || isEmpty(data.error)) { + self.raise('filebatchuploadsuccess', [outData]); + self.clearFileInput(); + self.initUploadSuccess(data); + } else { + self.showUploadError(data.error, outData, 'filebatchuploaderror'); + } + }; + fnComplete = function () { + self.setProgress(100); + self.unlock(); + self.clearFileInput(); + self.raise('filebatchuploadcomplete', [self.filestack, self.getExtraData()]); + }; + fnError = function (jqXHR, textStatus, errorThrown) { + var outData = self.getOutData(jqXHR), errMsg = self.parseError(jqXHR, errorThrown); + params.data = outData; + self.showUploadError(errMsg, outData, 'filebatchuploaderror'); + }; + self.ajaxSubmit(fnBefore, fnSuccess, fnComplete, fnError); + }, + upload: function () { + var self = this, totLen = self.getFileStack().length, params = {}, + i, outData, len, hasExtraData = !$.isEmptyObject(self.getExtraData()); + if (self.minFileCount > 0 && self.getFileCount(totLen) < self.minFileCount) { + self.noFilesError(params); + return; + } + if (!self.isUploadable || self.isDisabled || (totLen === 0 && !hasExtraData)) { + return; + } + self.resetUpload(); + self.$progress.removeClass('hide'); + self.uploadCount = 0; + self.uploadStatus = {}; + self.uploadLog = []; + self.lock(); + self.setProgress(2); + if (totLen === 0 && hasExtraData) { + self.uploadExtraOnly(); + return; + } + len = self.filestack.length; + self.hasInitData = false; + if (self.uploadAsync) { + outData = self.getOutData(); + self.raise('filebatchpreupload', [outData]); + self.fileBatchCompleted = false; + self.uploadCache = {content: [], config: [], tags: [], append: true}; + self.uploadAsyncCount = self.getFileStack().length; + for (i = 0; i < len; i++) { + self.uploadCache.content[i] = null; + self.uploadCache.config[i] = null; + self.uploadCache.tags[i] = null; + } + for (i = 0; i < len; i++) { + if (self.filestack[i] !== undefined) { + self.uploadSingle(i, self.filestack, true); + } + } + return; + } + self.uploadBatch(); + }, + initFileActions: function () { + var self = this; + if (!self.showPreview) { + return; + } + self.$preview.find('.kv-file-remove').each(function () { + var $el = $(this), $frame = $el.closest('.file-preview-frame'), hasError, + id = $frame.attr('id'), ind = $frame.attr('data-fileindex'), n, cap, status; + handler($el, 'click', function () { + status = self.raise('filepreremove', [id, ind]); + if (status === false || !self.validateMinCount()) { + return false; + } + hasError = $frame.hasClass('file-preview-error'); + self.cleanMemory($frame); + $frame.fadeOut('slow', function () { + self.updateStack(ind, undefined); + self.clearObjects($frame); + $frame.remove(); + if (id && hasError) { + self.$errorContainer.find('li[data-file-id="' + id + '"]').fadeOut('fast', function () { + $(this).remove(); + if (!self.errorsExist()) { + self.resetErrors(); + } + }); + } + var filestack = self.getFileStack(true), len = filestack.length, chk = previewCache.count( + self.id), + hasThumb = self.showPreview && self.$preview.find('.file-preview-frame').length; + self.clearFileInput(); + if (len === 0 && chk === 0 && !hasThumb) { + self.reset(); + } else { + n = chk + len; + cap = n > 1 ? self.getMsgSelected(n) : (filestack[0] ? self.getFileNames()[0] : ''); + self.setCaption(cap); + } + self.raise('fileremoved', [id, ind]); + }); + }); + }); + self.$preview.find('.kv-file-upload').each(function () { + var $el = $(this); + handler($el, 'click', function () { + var $frame = $el.closest('.file-preview-frame'), + ind = $frame.attr('data-fileindex'); + if (!$frame.hasClass('file-preview-error')) { + self.uploadSingle(ind, self.filestack, false); + } + }); + }); + }, + hideFileIcon: function () { + if (this.overwriteInitial) { + this.$captionContainer.find('.kv-caption-icon').hide(); + } + }, + showFileIcon: function () { + this.$captionContainer.find('.kv-caption-icon').show(); + }, + addError: function (msg) { + var self = this, $error = self.$errorContainer; + if (msg && $error.length) { + $error.html(self.errorCloseButton + msg); + handler($error.find('.kv-error-close'), 'click', function () { + $error.fadeOut('slow'); + }); + } + }, + resetErrors: function (fade) { + var self = this, $error = self.$errorContainer; + self.isError = false; + self.$container.removeClass('has-error'); + $error.html(''); + if (fade) { + $error.fadeOut('slow'); + } else { + $error.hide(); + } + }, + showFolderError: function (folders) { + var self = this, $error = self.$errorContainer; + if (!folders) { + return; + } + self.addError(self.msgFoldersNotAllowed.replace(/\{n}/g, folders)); + $error.fadeIn(800); + addCss(self.$container, 'has-error'); + self.raise('filefoldererror', [folders]); + }, + showUploadError: function (msg, params, event) { + var self = this, $error = self.$errorContainer, ev = event || 'fileuploaderror', + e = params && params.id ? '
  • ' + msg + '
  • ' : '
  • ' + msg + '
  • '; + if ($error.find('ul').length === 0) { + self.addError('
      ' + e + '
    '); + } else { + $error.find('ul').append(e); + } + $error.fadeIn(800); + self.raise(ev, [params]); + self.$container.removeClass('file-input-new'); + addCss(self.$container, 'has-error'); + return true; + }, + showError: function (msg, params, event) { + var self = this, $error = self.$errorContainer, ev = event || 'fileerror'; + params = params || {}; + params.reader = self.reader; + self.addError(msg); + $error.fadeIn(800); + self.raise(ev, [params]); + if (!self.isUploadable) { + self.clearFileInput(); + } + self.$container.removeClass('file-input-new'); + addCss(self.$container, 'has-error'); + self.$btnUpload.attr('disabled', true); + return true; + }, + errorHandler: function (evt, caption) { + var self = this, err = evt.target.error; + /** @namespace err.NOT_FOUND_ERR */ + /** @namespace err.SECURITY_ERR */ + /** @namespace err.NOT_READABLE_ERR */ + if (err.code === err.NOT_FOUND_ERR) { + self.showError(self.msgFileNotFound.replace('{name}', caption)); + } else if (err.code === err.SECURITY_ERR) { + self.showError(self.msgFileSecured.replace('{name}', caption)); + } else if (err.code === err.NOT_READABLE_ERR) { + self.showError(self.msgFileNotReadable.replace('{name}', caption)); + } else if (err.code === err.ABORT_ERR) { + self.showError(self.msgFilePreviewAborted.replace('{name}', caption)); + } else { + self.showError(self.msgFilePreviewError.replace('{name}', caption)); + } + }, + parseFileType: function (file) { + var self = this, isValid, vType, cat, i; + for (i = 0; i < defaultPreviewTypes.length; i += 1) { + cat = defaultPreviewTypes[i]; + isValid = isSet(cat, self.fileTypeSettings) ? self.fileTypeSettings[cat] : defaultFileTypeSettings[cat]; + vType = isValid(file.type, file.name) ? cat : ''; + if (!isEmpty(vType)) { + return vType; + } + } + return 'other'; + }, + previewDefault: function (file, previewId, isDisabled) { + if (!this.showPreview) { + return; + } + var self = this, frameClass = '', fname = file ? file.name : '', + /** @namespace objUrl.createObjectURL */ + data = objUrl.createObjectURL(file), ind = previewId.slice(previewId.lastIndexOf('-') + 1), + config = self.previewSettings.other || defaultPreviewSettings.other, + footer = self.renderFileFooter(file.name, config.width), + previewOtherTemplate = self.parseFilePreviewIcon(self.getPreviewTemplate('other'), fname); + if (isDisabled === true) { + if (!self.isUploadable) { + footer += '
    ' + self.fileActionSettings.indicatorError + '
    '; + } + } + self.clearDefaultPreview(); + self.$preview.append("\n" + previewOtherTemplate + .replace(/\{previewId}/g, previewId) + .replace(/\{frameClass}/g, frameClass) + .replace(/\{fileindex}/g, ind) + .replace(/\{caption}/g, self.slug(file.name)) + .replace(/\{width}/g, config.width) + .replace(/\{height}/g, config.height) + .replace(/\{type}/g, file.type) + .replace(/\{data}/g, data) + .replace(/\{footer}/g, footer)); + if (isDisabled === true && self.isUploadable) { + self.setThumbStatus($('#' + previewId), 'Error'); + } + }, + previewFile: function (i, file, theFile, previewId, data) { + if (!this.showPreview) { + return; + } + var self = this, cat = self.parseFileType(file), fname = file ? file.name : '', caption = self.slug(fname), + content, strText, types = self.allowedPreviewTypes, mimes = self.allowedPreviewMimeTypes, + tmplt = self.getPreviewTemplate(cat), chkTypes = types && types.indexOf(cat) >= 0, id, + config = isSet(cat, self.previewSettings) ? self.previewSettings[cat] : defaultPreviewSettings[cat], + chkMimes = mimes && mimes.indexOf(file.type) !== -1, + footer = self.renderFileFooter(caption, config.width), modal = '', + ind = previewId.slice(previewId.lastIndexOf('-') + 1); + if (chkTypes || chkMimes) { + tmplt = self.parseFilePreviewIcon(tmplt, fname.split('.').pop()); + if (cat === 'text') { + strText = htmlEncode(theFile.target.result); + id = 'text-' + uniqId(); + content = tmplt.replace(/\{zoom}/g, self.getLayoutTemplate('zoom')); + modal = self.getLayoutTemplate('modal').replace('{id}', id) + .replace(/\{title}/g, caption) + .replace(/\{body}/g, strText).replace(/\{heading}/g, self.msgZoomModalHeading); + content = content.replace(/\{previewId}/g, previewId).replace(/\{caption}/g, caption) + .replace(/\{width}/g, config.width).replace(/\{height}/g, config.height) + .replace(/\{frameClass}/g, '').replace(/\{zoomInd}/g, self.zoomIndicator) + .replace(/\{footer}/g, footer).replace(/\{fileindex}/g, ind) + .replace(/\{type}/g, file.type).replace(/\{zoomTitle}/g, self.msgZoomTitle) + .replace(/\{dialog}/g, "$('#" + id + "').modal('show')") + .replace(/\{data}/g, strText) + modal; + } else { + content = tmplt.replace(/\{previewId}/g, previewId).replace(/\{caption}/g, caption) + .replace(/\{frameClass}/g, '').replace(/\{type}/g, file.type).replace(/\{fileindex}/g, ind) + .replace(/\{width}/g, config.width).replace(/\{height}/g, config.height) + .replace(/\{footer}/g, footer).replace(/\{data}/g, data); + } + self.clearDefaultPreview(); + self.$preview.append("\n" + content); + self.validateImage(i, previewId, caption, file.type); + } else { + self.previewDefault(file, previewId); + } + }, + slugDefault: function (text) { + return isEmpty(text) ? '' : String(text).replace(/[\-\[\]\/\{}:;#%=\(\)\*\+\?\\\^\$\|<>&"']/g, '_'); + }, + readFiles: function (files) { + this.reader = new FileReader(); + var self = this, $el = self.$element, $preview = self.$preview, reader = self.reader, + $container = self.$previewContainer, $status = self.$previewStatus, msgLoading = self.msgLoading, + msgProgress = self.msgProgress, previewInitId = self.previewInitId, numFiles = files.length, + settings = self.fileTypeSettings, ctr = self.filestack.length, readFile, + throwError = function (msg, file, previewId, index) { + var p1 = $.extend(true, {}, self.getOutData({}, {}, files), {id: previewId, index: index}), + p2 = {id: previewId, index: index, file: file, files: files}; + self.previewDefault(file, previewId, true); + if (self.isUploadable) { + self.addToStack(undefined); + } + setTimeout(readFile(index + 1), 100); + self.initFileActions(); + if (self.removeFromPreviewOnError) { + $('#' + previewId).remove(); + } + return self.isUploadable ? self.showUploadError(msg, p1) : self.showError(msg, p2); + }; + + self.loadedImages = []; + self.totalImagesCount = 0; + + $.each(files, function (key, file) { + var func = self.fileTypeSettings.image || defaultFileTypeSettings.image; + if (func && func(file.type)) { + self.totalImagesCount++; + } + }); + + readFile = function (i) { + if (isEmpty($el.attr('multiple'))) { + numFiles = 1; + } + if (i >= numFiles) { + if (self.isUploadable && self.filestack.length > 0) { + self.raise('filebatchselected', [self.getFileStack()]); + } else { + self.raise('filebatchselected', [files]); + } + $container.removeClass('file-thumb-loading'); + $status.html(''); + return; + } + var node = ctr + i, previewId = previewInitId + "-" + node, isText, file = files[i], + caption = self.slug(file.name), fileSize = (file.size || 0) / 1000, checkFile, fileExtExpr = '', + previewData = objUrl.createObjectURL(file), fileCount = 0, j, msg, typ, chk, + fileTypes = self.allowedFileTypes, strTypes = isEmpty(fileTypes) ? '' : fileTypes.join(', '), + fileExt = self.allowedFileExtensions, strExt = isEmpty(fileExt) ? '' : fileExt.join(', '); + if (!isEmpty(fileExt)) { + fileExtExpr = new RegExp('\\.(' + fileExt.join('|') + ')$', 'i'); + } + fileSize = fileSize.toFixed(2); + if (self.maxFileSize > 0 && fileSize > self.maxFileSize) { + msg = self.msgSizeTooLarge.replace('{name}', caption) + .replace('{size}', fileSize) + .replace('{maxSize}', self.maxFileSize); + self.isError = throwError(msg, file, previewId, i); + return; + } + if (!isEmpty(fileTypes) && isArray(fileTypes)) { + for (j = 0; j < fileTypes.length; j += 1) { + typ = fileTypes[j]; + checkFile = settings[typ]; + chk = (checkFile !== undefined && checkFile(file.type, caption)); + fileCount += isEmpty(chk) ? 0 : chk.length; + } + if (fileCount === 0) { + msg = self.msgInvalidFileType.replace('{name}', caption).replace('{types}', strTypes); + self.isError = throwError(msg, file, previewId, i); + return; + } + } + if (fileCount === 0 && !isEmpty(fileExt) && isArray(fileExt) && !isEmpty(fileExtExpr)) { + chk = caption.match(fileExtExpr); + fileCount += isEmpty(chk) ? 0 : chk.length; + if (fileCount === 0) { + msg = self.msgInvalidFileExtension.replace('{name}', caption).replace('{extensions}', + strExt); + self.isError = throwError(msg, file, previewId, i); + return; + } + } + if (!self.showPreview) { + self.addToStack(file); + setTimeout(readFile(i + 1), 100); + self.raise('fileloaded', [file, previewId, i, reader]); + return; + } + if ($preview.length > 0 && FileReader !== undefined) { + $status.html(msgLoading.replace('{index}', i + 1).replace('{files}', numFiles)); + $container.addClass('file-thumb-loading'); + reader.onerror = function (evt) { + self.errorHandler(evt, caption); + }; + reader.onload = function (theFile) { + self.previewFile(i, file, theFile, previewId, previewData); + self.initFileActions(); + }; + reader.onloadend = function () { + msg = msgProgress + .replace('{index}', i + 1).replace('{files}', numFiles) + .replace('{percent}', 50).replace('{name}', caption); + setTimeout(function () { + $status.html(msg); + self.updateFileDetails(numFiles); + readFile(i + 1); + }, 100); + self.raise('fileloaded', [file, previewId, i, reader]); + }; + reader.onprogress = function (data) { + if (data.lengthComputable) { + var fact = (data.loaded / data.total) * 100, progress = Math.ceil(fact); + msg = msgProgress.replace('{index}', i + 1).replace('{files}', numFiles) + .replace('{percent}', progress).replace('{name}', caption); + setTimeout(function () { + $status.html(msg); + }, 100); + } + }; + isText = isSet('text', settings) ? settings.text : defaultFileTypeSettings.text; + if (isText(file.type, caption)) { + reader.readAsText(file, self.textEncoding); + } else { + reader.readAsArrayBuffer(file); + } + } else { + self.previewDefault(file, previewId); + setTimeout(function () { + readFile(i + 1); + self.updateFileDetails(numFiles); + }, 100); + self.raise('fileloaded', [file, previewId, i, reader]); + } + self.addToStack(file); + }; + + readFile(0); + self.updateFileDetails(numFiles, false); + }, + updateFileDetails: function (numFiles) { + var self = this, $el = self.$element, fileStack = self.getFileStack(), + name = ($el[0].files[0] && $el[0].files[0].name) || (fileStack.length && fileStack[0].name) || '', + label = self.slug(name), n = self.isUploadable ? fileStack.length : numFiles, + nFiles = previewCache.count(self.id) + n, log = n > 1 ? self.getMsgSelected(nFiles) : label; + if (self.isError) { + self.$previewContainer.removeClass('file-thumb-loading'); + self.$previewStatus.html(''); + self.$captionContainer.find('.kv-caption-icon').hide(); + } else { + self.showFileIcon(); + } + self.setCaption(log, self.isError); + self.$container.removeClass('file-input-new file-input-ajax-new'); + if (arguments.length === 1) { + self.raise('fileselect', [numFiles, label]); + } + if (previewCache.count(self.id)) { + self.initPreviewDeletes(); + } + }, + validateMinCount: function () { + var self = this, len = self.isUploadable ? self.getFileStack().length : self.$element.get(0).files.length; + if (self.validateInitialCount && self.minFileCount > 0 && self.getFileCount(len - 1) < self.minFileCount) { + self.noFilesError({}); + return false; + } + return true; + }, + getFileCount: function (fileCount) { + var self = this, addCount = 0; + if (self.validateInitialCount && !self.overwriteInitial) { + addCount = previewCache.count(self.id); + fileCount += addCount; + } + return fileCount; + }, + change: function (e) { + var self = this, $el = self.$element; + if (!self.isUploadable && isEmpty($el.val()) && self.fileInputCleared) { // IE 11 fix + self.fileInputCleared = false; + return; + } + self.fileInputCleared = false; + var tfiles, msg, total, isDragDrop = arguments.length > 1, isAjaxUpload = self.isUploadable, i = 0, f, n, len, + files = isDragDrop ? e.originalEvent.dataTransfer.files : $el.get(0).files, ctr = self.filestack.length, + isSingleUpload = isEmpty($el.attr('multiple')), flagSingle = (isSingleUpload && ctr > 0), folders = 0, + throwError = function (mesg, file, previewId, index) { + var p1 = $.extend(true, {}, self.getOutData({}, {}, files), {id: previewId, index: index}), + p2 = {id: previewId, index: index, file: file, files: files}; + return self.isUploadable ? self.showUploadError(mesg, p1) : self.showError(mesg, p2); + }; + self.reader = null; + self.resetUpload(); + self.hideFileIcon(); + if (self.isUploadable) { + self.$container.find('.file-drop-zone .' + self.dropZoneTitleClass).remove(); + } + if (isDragDrop) { + tfiles = []; + while (files[i]) { + f = files[i]; + if (!f.type && f.size % 4096 === 0) { + folders++; + } else { + tfiles.push(f); + } + i++; + } + } else { + if (e.target.files === undefined) { + tfiles = e.target && e.target.value ? [ + {name: e.target.value.replace(/^.+\\/, '')} + ] : []; + } else { + tfiles = e.target.files; + } + } + if (isEmpty(tfiles) || tfiles.length === 0) { + if (!isAjaxUpload) { + self.clear(); + } + self.showFolderError(folders); + self.raise('fileselectnone'); + return; + } + self.resetErrors(); + len = tfiles.length; + total = self.isUploadable ? self.getFileStack().length + len : len; + total = self.getFileCount(total); + if (self.maxFileCount > 0 && total > self.maxFileCount) { + if (!self.autoReplace || len > self.maxFileCount) { + n = (self.autoReplace && len > self.maxFileCount) ? len : total; + msg = self.msgFilesTooMany.replace('{m}', self.maxFileCount).replace('{n}', n); + self.isError = throwError(msg, null, null, null); + self.$captionContainer.find('.kv-caption-icon').hide(); + self.setCaption('', true); + self.$container.removeClass('file-input-new file-input-ajax-new'); + return; + } + if (total > self.maxFileCount) { + self.resetPreviewThumbs(isAjaxUpload); + } + } else { + if (!isAjaxUpload || flagSingle) { + self.resetPreviewThumbs(false); + if (flagSingle) { + self.clearStack(); + } + } else { + if (isAjaxUpload && ctr === 0 && (!previewCache.count(self.id) || self.overwriteInitial)) { + self.resetPreviewThumbs(true); + } + } + } + if (self.isPreviewable) { + self.readFiles(tfiles); + } else { + self.updateFileDetails(1); + } + self.showFolderError(folders); + }, + getFileName: function (file) { + return file && file.name ? this.slug(file.name) : undefined; + }, + getFileNames: function (skipNull) { + var self = this; + return self.filenames.filter(function (n) { + return (skipNull ? n !== undefined : n !== undefined && n !== null); + }); + }, + getFileStack: function (skipNull) { + var self = this; + return self.filestack.filter(function (n) { + return (skipNull ? n !== undefined : n !== undefined && n !== null); + }); + }, + clearStack: function () { + var self = this; + self.filestack = []; + self.filenames = []; + }, + updateStack: function (i, file) { + var self = this; + self.filestack[i] = file; + self.filenames[i] = self.getFileName(file); + }, + addToStack: function (file) { + var self = this; + self.filestack.push(file); + self.filenames.push(self.getFileName(file)); + }, + setPreviewError: function ($thumb, i, val) { + var self = this; + if (i) { + self.updateStack(i, val); + } + if (self.removeFromPreviewOnError) { + $thumb.remove(); + } else { + self.setThumbStatus($thumb, 'Error'); + } + }, + checkDimensions: function (i, chk, $img, $thumb, fname, type, params) { + var self = this, msg, dim, tag = chk === 'Small' ? 'min' : 'max', + limit = self[tag + 'Image' + type], $imgEl, isValid; + if (isEmpty(limit) || !$img.length) { + return; + } + $imgEl = $img[0]; + dim = (type === 'Width') ? $imgEl.naturalWidth || $imgEl.width : $imgEl.naturalHeight || $imgEl.height; + isValid = chk === 'Small' ? dim >= limit : dim <= limit; + if (isValid) { + return; + } + msg = self['msgImage' + type + chk].replace('{name}', fname).replace('{size}', limit); + self.showUploadError(msg, params); + self.setPreviewError($thumb, i, null); + }, + validateImage: function (i, previewId, fname, ftype) { + var self = this, $preview = self.$preview, params, w1, w2, + $thumb = $preview.find("#" + previewId), $img = $thumb.find('img'); + fname = fname || 'Untitled'; + if (!$img.length) { + return; + } + handler($img, 'load', function () { + w1 = $thumb.width(); + w2 = $preview.width(); + if (w1 > w2) { + $img.css('width', '100%'); + $thumb.css('width', '97%'); + } + params = {ind: i, id: previewId}; + self.checkDimensions(i, 'Small', $img, $thumb, fname, 'Width', params); + self.checkDimensions(i, 'Small', $img, $thumb, fname, 'Height', params); + if (!self.resizeImage) { + self.checkDimensions(i, 'Large', $img, $thumb, fname, 'Width', params); + self.checkDimensions(i, 'Large', $img, $thumb, fname, 'Height', params); + } + self.raise('fileimageloaded', [previewId]); + self.loadedImages.push({ind: i, img: $img, thumb: $thumb, pid: previewId, typ: ftype}); + self.validateAllImages(); + objUrl.revokeObjectURL($img.attr('src')); + }); + }, + validateAllImages: function () { + var self = this, i, config, $img, $thumb, pid, ind, params = {}, errFunc; + if (self.loadedImages.length !== self.totalImagesCount) { + return; + } + self.raise('fileimagesloaded'); + if (!self.resizeImage) { + return; + } + errFunc = self.isUploadable ? self.showUploadError : self.showError; + for (i = 0; i < self.loadedImages.length; i++) { + config = self.loadedImages[i]; + $img = config.img; + $thumb = config.thumb; + pid = config.pid; + ind = config.ind; + params = {id: pid, 'index': ind}; + if (!self.getResizedImage($img[0], config.typ, pid, ind)) { + errFunc(self.msgImageResizeError, params, 'fileimageresizeerror'); + self.setPreviewError($thumb, ind); + } + } + self.raise('fileimagesresized'); + }, + getResizedImage: function (image, type, pid, ind) { + var self = this, width = image.naturalWidth, height = image.naturalHeight, ratio = 1, + maxWidth = self.maxImageWidth || width, maxHeight = self.maxImageHeight || height, + isValidImage = (width && height), chkWidth, chkHeight, + canvas = self.imageCanvas, context = self.imageCanvasContext; + if (!isValidImage) { + return false; + } + if (width === maxWidth && height === maxHeight) { + return true; + } + type = type || self.resizeDefaultImageType; + chkWidth = width > maxWidth; + chkHeight = height > maxHeight; + if (self.resizePreference === 'width') { + ratio = chkWidth ? maxWidth / width : (chkHeight ? maxHeight / height : 1); + } else { + ratio = chkHeight ? maxHeight / height : (chkWidth ? maxWidth / width : 1); + } + self.resetCanvas(); + width *= ratio; + height *= ratio; + canvas.width = width; + canvas.height = height; + try { + context.drawImage(image, 0, 0, width, height); + canvas.toBlob(function (blob) { + self.raise('fileimageresized', [pid, ind]); + self.filestack[ind] = blob; + }, type, self.resizeQuality); + return true; + } + catch (err) { + return false; + } + }, + initCaption: function () { + var self = this, cap = self.initialCaption || ''; + if (self.overwriteInitial || isEmpty(cap)) { + self.$caption.html(''); + return false; + } + self.setCaption(cap); + return true; + }, + setCaption: function (content, isError) { + var self = this, title, out; + if (isError) { + title = $('
    ' + self.msgValidationError + '
    ').text(); + out = '' + + self.msgValidationErrorIcon + title + ''; + } else { + if (isEmpty(content) || self.$caption.length === 0) { + return; + } + title = $('
    ' + content + '
    ').text(); + out = self.getLayoutTemplate('icon') + title; + } + self.$caption.html(out); + self.$caption.attr('title', title); + self.$captionContainer.find('.file-caption-ellipsis').attr('title', title); + }, + initBrowse: function ($container) { + var self = this; + self.$btnFile = $container.find('.btn-file'); + self.$btnFile.append(self.$element); + }, + createContainer: function () { + var self = this, + $container = $(document.createElement("div")) + .attr({"class": 'file-input file-input-new'}) + .html(self.renderMain()); + self.$element.before($container); + self.initBrowse($container); + return $container; + }, + refreshContainer: function () { + var self = this, $container = self.$container; + $container.before(self.$element); + $container.html(self.renderMain()); + self.initBrowse($container); + }, + renderMain: function () { + var self = this, dropCss = (self.isUploadable && self.dropZoneEnabled) ? ' file-drop-zone' : 'file-drop-disabled', + close = !self.showClose ? '' : self.getLayoutTemplate('close'), + preview = !self.showPreview ? '' : self.getLayoutTemplate('preview') + .replace(/\{class}/g, self.previewClass) + .replace(/\{dropClass}/g, dropCss), + css = self.isDisabled ? self.captionClass + ' file-caption-disabled' : self.captionClass, + caption = self.captionTemplate.replace(/\{class}/g, css + ' kv-fileinput-caption'); + return self.mainTemplate.replace(/\{class}/g, self.mainClass) + .replace(/\{preview}/g, preview) + .replace(/\{close}/g, close) + .replace(/\{caption}/g, caption) + .replace(/\{upload}/g, self.renderButton('upload')) + .replace(/\{remove}/g, self.renderButton('remove')) + .replace(/\{cancel}/g, self.renderButton('cancel')) + .replace(/\{browse}/g, self.renderButton('browse')); + }, + renderButton: function (type) { + var self = this, tmplt = self.getLayoutTemplate('btnDefault'), css = self[type + 'Class'], + title = self[type + 'Title'], icon = self[type + 'Icon'], label = self[type + 'Label'], + status = self.isDisabled ? ' disabled' : '', btnType = 'button'; + switch (type) { + case 'remove': + if (!self.showRemove) { + return ''; + } + break; + case 'cancel': + if (!self.showCancel) { + return ''; + } + css += ' hide'; + break; + case 'upload': + if (!self.showUpload) { + return ''; + } + if (self.isUploadable && !self.isDisabled) { + tmplt = self.getLayoutTemplate('btnLink').replace('{href}', self.uploadUrl); + } else { + btnType = 'submit'; + } + break; + case 'browse': + tmplt = self.getLayoutTemplate('btnBrowse'); + break; + default: + return ''; + } + css += type === 'browse' ? ' btn-file' : ' fileinput-' + type + ' fileinput-' + type + '-button'; + if (!isEmpty(label)) { + label = ' ' + label + ''; + } + return tmplt.replace('{type}', btnType) + .replace('{css}', css) + .replace('{title}', title) + .replace('{status}', status) + .replace('{icon}', icon) + .replace('{label}', label); + } + }; + + $.fn.fileinput = function (option) { + if (!hasFileAPISupport() && !isIE(9)) { + return; + } + var args = Array.apply(null, arguments), retvals = []; + args.shift(); + this.each(function () { + var self = $(this), data = self.data('fileinput'), options = typeof option === 'object' && option, + lang = options.language || self.data('language') || 'en', loc = {}, opts; + + if (!data) { + if (lang !== 'en' && !isEmpty($.fn.fileinputLocales[lang])) { + loc = $.fn.fileinputLocales[lang]; + } + opts = $.extend(true, {}, $.fn.fileinput.defaults, $.fn.fileinputLocales.en, loc, options, self.data()); + data = new FileInput(this, opts); + self.data('fileinput', data); + } + + if (typeof option === 'string') { + retvals.push(data[option].apply(data, args)); + } + }); + switch (retvals.length) { + case 0: + return this; + case 1: + return retvals[0]; + default: + return retvals; + } + }; + + $.fn.fileinput.defaults = { + language: 'en', + showCaption: true, + showPreview: true, + showRemove: true, + showUpload: true, + showCancel: true, + showClose: true, + showUploadedThumbs: true, + autoReplace: false, + mainClass: '', + previewClass: '', + captionClass: '', + mainTemplate: null, + initialCaption: '', + initialPreview: [], + initialPreviewDelimiter: '*$$*', + initialPreviewConfig: [], + initialPreviewThumbTags: [], + previewThumbTags: {}, + initialPreviewShowDelete: true, + removeFromPreviewOnError: true, + deleteUrl: '', + deleteExtraData: {}, + overwriteInitial: true, + layoutTemplates: defaultLayoutTemplates, + previewTemplates: defaultPreviewTemplates, + allowedPreviewTypes: defaultPreviewTypes, + allowedPreviewMimeTypes: null, + allowedFileTypes: null, + allowedFileExtensions: null, + defaultPreviewContent: null, + customLayoutTags: {}, + customPreviewTags: {}, + previewSettings: defaultPreviewSettings, + fileTypeSettings: defaultFileTypeSettings, + previewFileIcon: '', + previewFileIconClass: 'file-icon-4x', + previewFileIconSettings: {}, + previewFileExtSettings: {}, + buttonLabelClass: 'hidden-xs', + browseIcon: '', + browseClass: 'btn btn-primary', + removeIcon: '', + removeClass: 'btn btn-default', + cancelIcon: '', + cancelClass: 'btn btn-default', + uploadIcon: '', + uploadClass: 'btn btn-default', + uploadUrl: null, + uploadAsync: true, + uploadExtraData: {}, + minImageWidth: null, + minImageHeight: null, + maxImageWidth: null, + maxImageHeight: null, + resizeImage: false, + resizePreference: 'width', + resizeQuality: 0.92, + resizeDefaultImageType: 'image/jpeg', + maxFileSize: 0, + minFileCount: 0, + maxFileCount: 0, + validateInitialCount: false, + msgValidationErrorClass: 'text-danger', + msgValidationErrorIcon: ' ', + msgErrorClass: 'file-error-message', + progressThumbClass: "progress-bar progress-bar-success progress-bar-striped active", + progressClass: "progress-bar progress-bar-success progress-bar-striped active", + progressCompleteClass: "progress-bar progress-bar-success", + previewFileType: 'image', + zoomIndicator: '', + elCaptionContainer: null, + elCaptionText: null, + elPreviewContainer: null, + elPreviewImage: null, + elPreviewStatus: null, + elErrorContainer: null, + errorCloseButton: '×', + slugCallback: null, + dropZoneEnabled: true, + dropZoneTitleClass: 'file-drop-zone-title', + fileActionSettings: {}, + otherActionButtons: '', + textEncoding: 'UTF-8', + ajaxSettings: {}, + ajaxDeleteSettings: {}, + showAjaxErrorDetails: true + }; + + $.fn.fileinputLocales.en = { + fileSingle: 'file', + filePlural: 'files', + browseLabel: 'Browse …', + removeLabel: 'Remove', + removeTitle: 'Clear selected files', + cancelLabel: 'Cancel', + cancelTitle: 'Abort ongoing upload', + uploadLabel: 'Upload', + uploadTitle: 'Upload selected files', + msgZoomTitle: 'View details', + msgZoomModalHeading: 'Detailed Preview', + msgSizeTooLarge: 'File "{name}" ({size} KB) exceeds maximum allowed upload size of {maxSize} KB.', + msgFilesTooLess: 'You must select at least {n} {files} to upload.', + msgFilesTooMany: 'Number of files selected for upload ({n}) exceeds maximum allowed limit of {m}.', + msgFileNotFound: 'File "{name}" not found!', + msgFileSecured: 'Security restrictions prevent reading the file "{name}".', + msgFileNotReadable: 'File "{name}" is not readable.', + msgFilePreviewAborted: 'File preview aborted for "{name}".', + msgFilePreviewError: 'An error occurred while reading the file "{name}".', + msgInvalidFileType: 'Invalid type for file "{name}". Only "{types}" files are supported.', + msgInvalidFileExtension: 'Invalid extension for file "{name}". Only "{extensions}" files are supported.', + msgUploadAborted: 'The file upload was aborted', + msgValidationError: 'File Upload Error', + msgLoading: 'Loading file {index} of {files} …', + msgProgress: 'Loading file {index} of {files} - {name} - {percent}% completed.', + msgSelected: '{n} {files} selected', + msgFoldersNotAllowed: 'Drag & drop files only! {n} folder(s) dropped were skipped.', + msgImageWidthSmall: 'Width of image file "{name}" must be at least {size} px.', + msgImageHeightSmall: 'Height of image file "{name}" must be at least {size} px.', + msgImageWidthLarge: 'Width of image file "{name}" cannot exceed {size} px.', + msgImageHeightLarge: 'Height of image file "{name}" cannot exceed {size} px.', + msgImageResizeError: 'Could not get the image dimensions to resize.', + msgImageResizeException: 'Error while resizing the image.
    {errors}
    ', + dropZoneTitle: 'Drag & drop files here …' + }; + + $.fn.fileinput.Constructor = FileInput; + + /** + * Convert automatically file input with class 'file' into a bootstrap fileinput control. + */ + $(document).ready(function () { + var $input = $('input.file[type=file]'); + if ($input.length) { + $input.fileinput(); + } + }); })); \ No newline at end of file diff --git a/clusterix/static/lib/jquery-2.1.4.min.js b/src/static/lib/jquery-2.1.4.min.js similarity index 100% rename from clusterix/static/lib/jquery-2.1.4.min.js rename to src/static/lib/jquery-2.1.4.min.js diff --git a/src/templates/algorithms/hcluster.html b/src/templates/algorithms/hcluster.html new file mode 100644 index 0000000..d383d0c --- /dev/null +++ b/src/templates/algorithms/hcluster.html @@ -0,0 +1,43 @@ + +
    + +
    +
    + + +
    + + +
    + + +
    + + +
    + +
    +
    +
    \ No newline at end of file diff --git a/src/templates/algorithms/kmeans.html b/src/templates/algorithms/kmeans.html new file mode 100644 index 0000000..03108c8 --- /dev/null +++ b/src/templates/algorithms/kmeans.html @@ -0,0 +1,18 @@ + +
    +
    +
    + K-Means +
    +
    +
    +
    + + +
    + + +
    +
    +
    +
    \ No newline at end of file diff --git a/src/templates/index.html b/src/templates/index.html new file mode 100644 index 0000000..0da0662 --- /dev/null +++ b/src/templates/index.html @@ -0,0 +1,152 @@ + + + + Clusterix + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + +
    +

    Processing Space

    + + +
    +
    +

    Data Input

    +
    +
    + + +
    +
    + + +
    + + +
    + + +
    + + +
    + + + +
    + + + + + +
    +
    +

    Visualization Swatchboard

    +
    +
    +
    +
    +
    +
    + + + +
    +
    +
    +
    +
    + + +{% from 'loading.html' import loading %} +{{ loading() }} + + + \ No newline at end of file diff --git a/src/templates/loading.html b/src/templates/loading.html new file mode 100644 index 0000000..f2070e7 --- /dev/null +++ b/src/templates/loading.html @@ -0,0 +1,163 @@ +{% macro loading() %} +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    +
    +{% endmacro %} \ No newline at end of file diff --git a/src/templates/processing/algorithms.html b/src/templates/processing/algorithms.html new file mode 100644 index 0000000..73dd7c8 --- /dev/null +++ b/src/templates/processing/algorithms.html @@ -0,0 +1,21 @@ + +
    +

    Algorithm Definitions & Options

    +
    + +
    + +
    + + +
    + + +
    +
    \ No newline at end of file diff --git a/src/templates/processing/fields.html b/src/templates/processing/fields.html new file mode 100644 index 0000000..277cb93 --- /dev/null +++ b/src/templates/processing/fields.html @@ -0,0 +1,63 @@ + +
    +

    Field Options

    +
    + +
    +
    + +
    + + +
    +
    + +
    +
    +
    +
    + + +
    + + +
    + + +
    + + +
    + + +
    + + +
    + + + +
    \ No newline at end of file diff --git a/src/templates/processing/stats.html b/src/templates/processing/stats.html new file mode 100644 index 0000000..70ce764 --- /dev/null +++ b/src/templates/processing/stats.html @@ -0,0 +1,6 @@ + +
    +

    Stats Options

    +
    +
    +
    \ No newline at end of file diff --git a/src/templates/processing/text.html b/src/templates/processing/text.html new file mode 100644 index 0000000..462388d --- /dev/null +++ b/src/templates/processing/text.html @@ -0,0 +1,43 @@ + +
    +

    Text Options

    +
    +
    + +
    + + +
    + + +
    + + +
    + + +
    + + +
    + + +
    +
    + +

    +
    + +
    +
    +
    \ No newline at end of file diff --git a/src/utils.py b/src/utils.py new file mode 100644 index 0000000..0f3c608 --- /dev/null +++ b/src/utils.py @@ -0,0 +1,72 @@ +import os +import json +import pandas as pd +from werkzeug.utils import secure_filename +from sklearn.externals import joblib +from .config import TEMP_PATH, DATAFRAME_PATH, CLUSTERER_PATH, log + + +@log('File loaded.') +def save_file(file): + """Save file to disk.""" + try: + file_path = os.path.join(TEMP_PATH, secure_filename(file.filename)) + file.save(file_path) + except KeyError: + pass # that means that there was no file sent, so abort + + +@log('Cluster model saved.') +def save_cluster_model(cluster_model): + """Pickle cluster model.""" + joblib.dump(cluster_model, CLUSTERER_PATH) + + +@log('Cluster model loaded.') +def load__cluster_model(): + """Load pickled cluster model.""" + return joblib.load(CLUSTERER_PATH) + + +@log('Dataframe saved.') +def save_df(df): + """Pickle dataframe.""" + pd.to_pickle(df, DATAFRAME_PATH) + + +@log('Dataframe loaded.') +def load_df(): + """Load pickled dataframe.""" + return pd.read_pickle(DATAFRAME_PATH) + + +def get_attrs(req): + """Get the data attributes.""" + return json.loads(req.form.get('data')) + + +def process_and_save_dataframe(filename): + """ + Create a dataframe from the file, and save the model to the disk. + Returns a field-type dict, and a boolean of the appearance of text data. + """ + df = read_file(filename) + + fields = df.columns + types = list(map(str, df.dtypes)) + has_text = 'object' in types # object represents the text fields + + save_df(df) + return dict(list(zip(fields, types))), has_text + + +def read_file(filename): + """Process a file and return a dataframe.""" + df = pd.read_csv('{}{}'.format(TEMP_PATH, filename), + error_bad_lines=False, + encoding='latin-1') + + df.rename(columns=lambda x: x.strip().replace(" ", ""), inplace=True) + df.fillna(df.median(), inplace=True) + df.fillna(u'NaN', inplace=True) + return df diff --git a/temp_input/.gitignore b/temp/.gitignore similarity index 100% rename from temp_input/.gitignore rename to temp/.gitignore