| # Copyright 2011 the Melange authors. |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| """Module that generates the lists. |
| Methods for adding typed columns: |
| addPlainTextColumn(col_id, name, func) |
| addDateColumn(col_id, name, func) |
| addBirthDateColumn(col_id, name, func) |
| addNumericalColumn(col_id, name, func) |
| addHtmlColumn(col_id, name, func) |
| |
| Methods for adding complex columns: |
| addSimpleColumn(col_id, name) |
| addSimpleParentColumn(col_id, name) |
| addParentColumn(col_id, name, trans) |
| addDictColumn(col_id, name) |
| |
| Methods for adding row buttons: |
| addSimpleRedirectRowButton(col_id, button_id, caption, url, |
| classes=None, new_window=False) |
| addCustomRedirectRowButton(col_id, button_id, caption, func, |
| classes=None, new_window=False) |
| |
| Methods for adding buttons: |
| addSimpleRedirectButton(button_id, caption, url, new_window=True) |
| addCustomRedirectButton(button_id, caption, func, new_window=True) |
| addPostButton(button_id, caption, url, bounds, keys, |
| addPostEditButton(button_id, caption, url='', keys=None, refresh='current') |
| """ |
| |
| import datetime |
| import json |
| import logging |
| |
| from google.appengine.datastore import datastore_query |
| from google.appengine.ext import db |
| from google.appengine.ext import ndb |
| |
| from django.utils import dateformat |
| from django.utils import html |
| |
| from soc.views.template import Template |
| |
| DATETIME_FORMAT = 'Y-m-d H:i:s' |
| DATE_FORMAT = 'Y-m-d' |
| BIRTHDATE_FORMAT = 'd-m-Y' |
| |
| # These together form the valid Column Types |
| # #ifihadenums |
| PLAIN_TEXT = 'plain_text' |
| NUMERICAL = 'numerical' |
| DATE = 'date' |
| BIRTHDATE = 'birthdate' |
| HTML = 'html' |
| |
| |
| # TODO(nathaniel): look into making the ColumnType family of classes |
| # module-private |
| class ColumnType(object): |
| # TODO(daniel): add doc string |
| """ |
| """ |
| |
| def safe(self, value): |
| """Returns a safe representation of the specified value which can be safely |
| rendered as HTML in the list. |
| |
| This method should be overridden by all non-abstract subclasses. |
| |
| Args: |
| value: the specified value for which to return the safe representation |
| """ |
| raise NotImplementedError |
| |
| |
| class PlainTextColumnType(ColumnType): |
| """Class which represents a column which contains textual values. |
| |
| As it may hold arbitrary string of bytes, the returned value must be |
| HTML escaped. |
| """ |
| |
| def safe(self, value): |
| """See ColumnType.safe for specification.""" |
| return html.conditional_escape(value) |
| |
| |
| class NumericalColumnType(ColumnType): |
| """Class which represents a column which contains numerical values.""" |
| |
| def safe(self, value): |
| """Returns the safe representation of the specified value. It is assumed |
| that only numerical values are passed here, so the the output is not HTML |
| escaped. |
| |
| Args: |
| value: the specified string or a number for which |
| to return the safe representation |
| |
| Returns: |
| numerical representation of the specified value or the empty string for |
| None and the empty string. |
| |
| Raises: |
| ValueError: if the specified value is invalid |
| TypeError: if the specified value is neither a number nor a string |
| """ |
| # 0 is a valid value, so 'if not value' is not appropriate here |
| if value is None or value == '': |
| return '' |
| elif isinstance(value, (int, long, float)): |
| return value |
| else: |
| try: |
| return int(value) |
| except ValueError: |
| return float(value) |
| |
| |
| class DateColumnType(ColumnType): |
| """Class which represents a column which contains a date value.""" |
| |
| def __init__(self, birthdate=False): |
| """If birthdate is true, the BIRTHDATE_FORMAT will be used.""" |
| self.birthdate = birthdate |
| |
| def safe(self, value): |
| """Returns the safe representation of the specified value. It is assumed |
| that only date values are passed here, so the the output is not HTML |
| escaped. |
| |
| Args: |
| value: the specified string or a date for which |
| to return the safe representation |
| |
| Returns: |
| textual representation of the specified value or the empty string for |
| None and the empty string. |
| |
| Raises: |
| ValueError: if the specified value is invalid |
| TypeError: if the specified value is neither a number nor a string |
| """ |
| if not value: |
| return 'N/A' |
| elif self.birthdate: |
| return dateformat.format(value, BIRTHDATE_FORMAT) |
| elif isinstance(value, datetime.datetime): |
| return dateformat.format(value, DATETIME_FORMAT) |
| else: |
| return dateformat.format(value, DATE_FORMAT) |
| |
| |
| class HtmlColumnType(ColumnType): |
| """Class which represents a column which contains HTML content.""" |
| |
| def safe(self, value): |
| """Returns the safe representation of the specified value. The output is |
| not HTML escaped so it is developer's responsibility to assure it does |
| not contain any malicious content. |
| |
| Args: |
| value: the specified value for which to return the safe representation |
| """ |
| return value |
| |
| |
| class ColumnTypeFactory(object): |
| """Parametric factory which creates concrete instances of.""" |
| |
| @classmethod |
| def create(cls, column_type): |
| """Returns an instance of the subclass of ColumnType class which |
| corresponds to the specified column_type parameter. |
| |
| Args: |
| column_type: the specified column type which must be one of the constant |
| values specified in ColumnType class. |
| """ |
| if column_type == PLAIN_TEXT: |
| return PlainTextColumnType() |
| elif column_type == NUMERICAL: |
| return NumericalColumnType() |
| elif column_type == HTML: |
| return HtmlColumnType() |
| elif column_type == DATE: |
| return DateColumnType() |
| elif column_type == BIRTHDATE: |
| return DateColumnType(birthdate=True) |
| else: |
| raise ValueError("Invalid column_type: %s" % column_type) |
| |
| def getListIndex(request): |
| """Returns the index of the requested list.""" |
| if 'idx' in request.GET: |
| idx = request.GET['idx'] |
| elif 'idx' in request.POST: |
| idx = request.POST['idx'] |
| else: |
| return -1 |
| |
| return int(idx) if idx.isdigit() else -1 |
| |
| |
| class Prefetcher(object): |
| """Class used to prefetch objects on list data construction. |
| |
| It is used to obtain arbitrary values that can be used at the point |
| the rows of a list are being constructed in order to achieve |
| better performance. |
| |
| Subclasses must implement prefetch() method. |
| """ |
| |
| def prefetch(self, entities): |
| """Does the prefetching work for the specified list of entities and |
| returns the prefetched data. |
| |
| Args: |
| entities: list of entities for which data should be prefetched |
| |
| Returns: |
| a tuple that contains two elements: |
| - a list that contains dictionaries with prefetched keys |
| and corresponding values |
| - a dict # TODO(daniel): document this structure |
| """ |
| raise NotImplementedError |
| |
| |
| class EmptyPrefetcher(Prefetcher): |
| """Trivial implementation of Prefetcher that does not prefetch any data.""" |
| |
| def prefetch(self, entities): |
| """See Prefetcher.prefetch for specification.""" |
| return [], {} |
| |
| |
| EMPTY_PREFETCHER = EmptyPrefetcher() |
| |
| |
| class ModelPrefetcher(Prefetcher): |
| """Prefetcher for the specified model and fields.""" |
| |
| def __init__(self, model, fields, parent=False): |
| """Initializes a new instance for the specified values. |
| |
| Args: |
| model: model for which data will be prefetched |
| fields: list of model fields which will be prefetched |
| parent: whether the parents of entities should be prefetched or not |
| """ |
| self._model = model |
| self._fields = fields |
| self._parent = parent |
| |
| def prefetch(self, entities): |
| """Prefetches the requested fields for the specified list of entities. |
| |
| Relevant values are automatically assigned to the corresponding fields |
| in the entities. |
| |
| Args: |
| entities: a list of entities belonging to the model specified with |
| the prefetcher |
| |
| Returns: |
| a tuple which contains an empty list and an empty dictionary |
| """ |
| prefetchFields(self._model, self._fields, entities, self._parent) |
| # TODO(daniel): prefetched entities should be returned here |
| return [], {} |
| |
| |
| # TODO(daniel): this class should be replaced by ListModelPrefetcher |
| class ListFieldPrefetcher(Prefetcher): |
| """Prefetcher which handles fields that store list of values.""" |
| |
| def __init__(self, model, list_fields): |
| """Initializes a new instance for the specified values. |
| |
| Args: |
| model: model for which data will be prefetched |
| list_fields: list of fields which are represented by db.ListProperty |
| in the specified model |
| """ |
| self._model = model |
| self._list_fields = list_fields |
| |
| def prefetch(self, entities): |
| """See Prefetcher.prefetch for specification.""" |
| prefetched_entities = prefetchListFields( |
| self._model, self._list_fields, entities) |
| return [prefetched_entities], {} |
| |
| |
| class ListModelPrefetcher(Prefetcher): |
| """Prefetcher for the specified model and fields which may also handle |
| fields that store list of values. |
| """ |
| |
| def __init__(self, model, fields, list_fields, parent=False): |
| """Initializes a new instance for the specified values. |
| |
| Args: |
| model: model for which data will be prefetched |
| fields: list of model fields which will be prefetched |
| list_fields: list of fields which are represented by db.ListProperty |
| in the specified model |
| parent: whether the parents of entities should be prefetched or not |
| """ |
| self._model = model |
| self._fields = fields |
| self._list_fields = list_fields |
| self._parent = parent |
| |
| def prefetch(self, entities): |
| """Uses async versions of prefetchers and distribute the keys manually. |
| |
| See Prefetcher.prefetch for specification. |
| """ |
| # Get the future objects for model fields and list fields by using |
| # the async versions of the corresponding prefetch methods. |
| mf_future = _prefetchFieldsAsync( |
| self._model, self._fields, entities, self._parent) |
| lf_future = _prefetchListFieldsAsync( |
| self._model, self._list_fields, entities) |
| |
| # now block until model prefetching completes and distribute the keys |
| # once the processing is finished |
| prefetched_mf = mf_future.get_result() |
| _processPrefetchedFields( |
| prefetched_mf, self._model, self._fields, entities, self._parent) |
| |
| # block on list prefetching to complete |
| prefetched_lf = lf_future.get_result() |
| prefetched_lf = dict((i.key(), i) for i in prefetched_lf if i) |
| |
| # Return the prefetched list fields dict as part of the |
| # prefetching protocol |
| return [prefetched_lf], {} |
| |
| |
| class ListConfiguration(object): |
| """Resembles the configuration of a list. This object is sent to the client |
| on page load. |
| |
| See the wiki page on ListProtocols for more information |
| (http://code.google.com/p/soc/wiki/ListsProtocol). |
| |
| Public fields are: |
| description: The description as shown to the end user. |
| autowidth: Whether the width of the columns should be automatically set. |
| height: Whether the height of the list should be automatically set. |
| multiselect: If true then the list will have a column with checkboxes which |
| allows the user to select a number of rows. |
| toolbar: [boolean, string] showing if and where the toolbar with buttons |
| should be present. |
| """ |
| |
| VALID_EDIT_TYPES = [ |
| 'text', 'textarea', 'select', 'checkbox', 'password', |
| 'button', 'image', 'file' |
| ] |
| |
| def __init__(self, add_key_column=True): |
| """Initializes the configuration. |
| |
| If add_key_column is set will add a 'key' column with the key id/name. |
| """ |
| self._col_names = [] |
| self._col_model = [] |
| self._col_map = {} |
| self._col_functions = {} |
| self._row_num = 50 |
| self._row_list = [5, 10, 20, 50, 100, 500, 1000] |
| self.autowidth = True |
| self._sortname = '' |
| self._sortorder = 'asc' |
| self._footer_row = False |
| self.height = 'auto' |
| self.multiselect = False |
| self.toolbar = [True, 'top'] |
| |
| self._buttons = {} |
| self._button_functions = {} |
| self._row_operation = {} |
| self._row_operation_func = None |
| self._row_buttons = {} |
| self._row_button_functions = {} |
| |
| self._templates = {} |
| |
| self._features = None |
| |
| if add_key_column: |
| # TODO(nathaniel): instance method called from within object constructor. |
| self._addKeyColumn() |
| |
| def _addKeyColumn(self): |
| """Adds a column for the key. |
| |
| The content of the column will be the entity id, if the entity key has a |
| parent, it will be included in the key name. |
| |
| For example, the content of the column for the 'melange' entity in the |
| 'gsoc2008' program would be 'gsoc2008/melange'. |
| """ |
| def getKeyName(e, *args): |
| keys = [] |
| if isinstance(e, ndb.Model): |
| key = e.key |
| else: |
| key = e.key() |
| while key: |
| if isinstance(e, ndb.Model): |
| key_id = key.id() |
| else: |
| key_id = key.id_or_name() |
| keys.append(str(key_id)) |
| key = key.parent() |
| return '/'.join(keys) |
| |
| self._addColumn('key', 'Key', getKeyName, hidden=True) |
| |
| def setDefaultPagination(self, row_num, row_list=None): |
| """Sets the default pagination. |
| |
| If row_num is False then pagination is disabled, and the row_list |
| argument is ignored. |
| |
| Args: |
| row_num: The number of rows that should be shown on a page on default. |
| row_list: List of integers which is the allowed pagination size a user |
| can can choose from. |
| """ |
| if not row_num: |
| self._row_num = -1 |
| self._row_list = [] |
| return |
| |
| self._row_num = row_num |
| |
| if row_list: |
| self.row_list = row_list |
| |
| def _addColumn(self, col_id, name, func, width=None, resizable=True, |
| hidden=False, searchhidden=True, options=None, |
| column_type=PLAIN_TEXT): |
| """Adds a column to the end of the list. |
| |
| Args: |
| col_id: A unique identifier of this column. |
| name: The header of the column that is shown to the user. |
| func: The function to be called when rendering this column for |
| a single entity. This function should take an entity as first |
| argument and args and kwargs if needed. The return value will |
| be displayed to the user for that column. If func returns None, |
| an empty string will be displayed instead. |
| width: The width of the column. |
| resizable: Whether the width of the column should be resizable by the |
| end user. |
| hidden: Whether the column should be hidden by default. |
| searchhidden: Whether this column should be searchable when hidden. |
| options: An array of (regexp, display_value) tuples. |
| column_type: One of the types specified in ColumnType class. |
| """ |
| if self._col_functions.get(col_id): |
| logging.warning('Column with id %s is already defined', col_id) |
| |
| if not callable(func): |
| raise TypeError('Given function is not callable') |
| |
| model = { |
| 'name': col_id, |
| 'index': col_id, |
| 'resizable': resizable, |
| 'hidden': hidden, |
| 'column_type': column_type, |
| } |
| |
| if width: |
| model['width'] = width |
| |
| if options: |
| values = ";".join("%s:%s" % i for i in options) |
| |
| model["stype"] = "select" |
| model["editoptions"] = dict(value=values) |
| |
| if searchhidden: |
| model["searchoptions"] = { |
| "searchhidden": True |
| } |
| |
| self._col_model.append(model) |
| self._col_map[col_id] = model |
| self._col_names.append(name) |
| self._col_functions[col_id] = func |
| |
| def addPlainTextColumn(self, col_id, name, func, **kwargs): |
| """Adds a plain text column to the end of the list. |
| |
| The values may contain arbitrary content which will be HTML escaped. |
| """ |
| self._addColumn( |
| col_id, name, func, column_type=PLAIN_TEXT, **kwargs) |
| |
| def addDateColumn(self, col_id, name, func, **kwargs): |
| """Adds a date column to the end of the list. |
| |
| It is expected that all the values in this columns will be dates. |
| The rendered output will not be HTML escaped. |
| """ |
| self._addColumn(col_id, name, func, column_type=DATE, **kwargs) |
| |
| def addBirthDateColumn(self, col_id, name, func, **kwargs): |
| """Adds a date column to the end of the list. |
| |
| It is expected that all the values in this columns will be dates. |
| The rendered output will not be HTML escaped. |
| """ |
| self._addColumn(col_id, name, func, column_type=BIRTHDATE, **kwargs) |
| |
| def addNumericalColumn(self, col_id, name, func, **kwargs): |
| """Adds a numerical column to the end of the list. |
| |
| It is expected that all the values in this columns will be numbers. |
| The rendered output will not be HTML escaped. |
| """ |
| self._addColumn( |
| col_id, name, func, column_type=NUMERICAL, **kwargs) |
| |
| def addHtmlColumn(self, col_id, name, func, **kwargs): |
| """Adds a HTML column to the end of the list. |
| |
| The content of the column may contain arbitrary HTML code which will be |
| rendered on the page without being escaped. It is vulnerable to malicious |
| inputs, so it should never be used for values which are entered by users. |
| """ |
| self._addColumn( |
| col_id, name, func, column_type=HTML, **kwargs) |
| |
| def addSimpleColumn(self, col_id, name, **kwargs): |
| """Adds a column to the end of the list which uses the id of the column as |
| attribute name of the entity to get the data from. |
| |
| This method is basically a shorthand for _addColumn with the function as |
| lambda ent, *args: getattr(ent, id). |
| |
| Args: |
| col_id: A unique identifier of this column and name of the field to get |
| the data from. |
| name: The header of the column that is shown to the user. |
| **kwargs: passed on to _addColumn |
| """ |
| func = lambda ent, *args: getattr(ent, col_id) |
| self._addColumn(col_id, name, func, **kwargs) |
| |
| def addSimpleParentColumn(self, col_id, name, **kwargs): |
| """Adds a column to the end of the list which uses the id of the column as |
| attribute name of the entity to get the data from. Rather than use the |
| entity directory, the entities parent is looked up in an 'ents' dictionary |
| that is passed in by means of a prefetcher. |
| |
| This method is basically a shorthand for _addColumn with the function as |
| lambda e, ents, *args: getattr(ents[e.parent_key()], col_id) |
| |
| Args: |
| col_id: A unique identifier of this column and name of the field to get |
| the data from. |
| name: The header of the column that is shown to the user. |
| **kwargs: passed on to _addColumn |
| """ |
| def func(entity, prefetched_entities, *args): |
| parent_entity = prefetched_entities[entity.parent_key()] |
| return getattr(parent_entity, col_id) |
| |
| self._addColumn(col_id, name, func, **kwargs) |
| |
| def addParentColumn(self, col_id, name, trans, **kwargs): |
| """Adds a column to the end of the list which uses the passed in trans |
| function to translate the parent entity into the desired data. Rather than |
| use the entity directory, the entities parent is looked up in an 'ents' |
| dictionary that is passed in by means of a prefetcher |
| |
| This method is basically a shorthand for _addColumn with the function as |
| lambda e, ents, *args: trans(ents[e.parent_key()], *args) |
| |
| Args: |
| col_id: A unique identifier of this column and name of the field to get |
| the data from. |
| name: The header of the column that is shown to the user. |
| trans: The transformation function that is passed the parent entity. |
| **kwargs: passed on to _addColumn |
| """ |
| def func(entity, prefetched_entities, *args): |
| parent_entity = prefetched_entities[entity.parent_key()] |
| return trans(parent_entity, *args) |
| |
| self._addColumn(col_id, name, func, **kwargs) |
| |
| def addDictColumn(self, col_id, name, **kwargs): |
| """Adds a column to the end of the list which uses the id of the column as |
| key of the dictionary to get the data from. |
| |
| This method is basically a shorthand for _addColumn with the function as |
| lambda d, *args: d[id]. |
| |
| Args: |
| col_id: A unique identifier of this column and name of the field to get |
| the data from. |
| name: The header of the column that is shown to the user. |
| **kwargs: passed on to _addColumn |
| """ |
| func = lambda d, *args: d[col_id] |
| self._addColumn(col_id, name, func, **kwargs) |
| |
| def __addRowButton(self, col_id, button_id, caption, type, classes, |
| parameters): |
| """Internal method for adding row buttons so that the uniqueness of |
| the column id can be checked. |
| |
| Args: |
| col_id: a unique identifier of the column that the button should be |
| displayed on |
| button_id: a unique identifier of the button which should be |
| unique per column |
| type: type of the button |
| classes: css classes that should be appended to the button |
| parameters: a dictionary of parameters and their values which should be |
| associated with the button |
| """ |
| |
| column_row_buttons = self._row_buttons.get(col_id, {}) |
| if column_row_buttons and column_row_buttons.get(button_id): |
| logging.warning('Button with name %s is already defined for column %s', |
| button_id, col_id) |
| |
| button_config = { |
| 'caption': caption, |
| 'type': type, |
| 'classes': classes if classes is not None else [], |
| 'parameters': parameters |
| } |
| |
| column_row_buttons[button_id] = button_config |
| self._row_buttons[col_id] = column_row_buttons |
| |
| def addSimpleRedirectRowButton(self, col_id, button_id, caption, url, |
| classes=None, new_window=False): |
| """Adds a simple redirect row button the the specified column with |
| the same link for each entity. |
| """ |
| |
| # always return the static url |
| func = lambda e, *args: url |
| |
| self.addCustomRedirectRowButton(col_id, button_id, caption, func, |
| classes, new_window) |
| |
| def addCustomRedirectRowButton(self, col_id, button_id, caption, func, |
| classes=None, new_window=False): |
| """Adds a custom redirect row button to the specified column. |
| """ |
| parameters = { |
| 'new_window': new_window |
| } |
| self.__addRowButton(col_id, button_id, caption, 'redirect_simple', |
| classes, parameters) |
| column_row_button_functions = self._row_button_functions.get(col_id, {}) |
| column_row_button_functions[button_id] = func |
| self._row_button_functions[col_id] = column_row_button_functions |
| |
| def __addButton(self, col_id, caption, bounds, col_type, parameters): |
| """Internal method for adding buttons so that the uniqueness of the id can |
| be checked. |
| """ |
| if self._buttons.get(col_id): |
| logging.warning('Button with id %s is already defined', col_id) |
| |
| button_config = { |
| 'id': col_id, |
| 'caption': caption, |
| 'type': col_type, |
| 'parameters': parameters |
| } |
| |
| if bounds: |
| button_config['bounds'] = bounds |
| |
| self._buttons[col_id] = button_config |
| |
| def setColumnEditable(self, col_id, editable, edittype=None, editoptions=None): |
| """Sets the editability for the specified column. |
| |
| Args: |
| editable: A boolean indicating whether the column should be editable. |
| edittype: A string indicating the type of values that should be entered, |
| see VALID_EDIT_TYPES for a list of valid values. |
| editoptions: A dictionary with options for the edit field. |
| """ |
| model = self._col_map.get(col_id) |
| |
| if not model: |
| raise ValueError('Id %s is not a defined column (Known columns %s)' |
| % (col_id, self._col_map.keys())) |
| |
| if edittype and edittype not in self.VALID_EDIT_TYPES: |
| raise ValueError("Invalid edit type '%s', known edit types: %s" % ( |
| edittype, self.VALID_EDIT_TYPES)) |
| |
| model['editable'] = editable |
| |
| if edittype: |
| model['edittype'] = edittype |
| |
| if editoptions: |
| model['editoptions'] = editoptions |
| |
| def setColumnSummary(self, col_id, summary_type, summary_tpl): |
| """Sets the column summary for the specified column. |
| |
| Args: |
| summary_type: the summary type |
| summary_tpl: the summary template |
| """ |
| model = self._col_map.get(col_id) |
| |
| if not model: |
| raise ValueError('Id %s is not a defined column (Known columns %s)' |
| % (col_id, self._col_map.keys())) |
| |
| model['summaryType'] = summary_type |
| model['summaryTpl'] = summary_tpl |
| |
| self._footer_row = True |
| |
| def setColumnExtra(self, col_id, **kwargs): |
| """Sets the column 'extra' field. |
| |
| Args: |
| col_id: The unique identifier of the column. |
| **kwargs: the contents of the 'extra' field. |
| """ |
| model = self._col_map.get(col_id) |
| |
| if not model: |
| raise ValueError('Id %s is not a defined column (Known columns %s)' |
| % (col_id, self._col_map.keys())) |
| |
| if model.get('extra'): |
| logging.warning('Column with id %s already has extra defined', col_id) |
| |
| model['extra'] = kwargs |
| |
| def addTemplateColumn(self, col_id, name, template, **kwargs): |
| """Adds a new template column.""" |
| self._addColumn(col_id, name, lambda *args, **kwargs: '', **kwargs) |
| |
| if self._templates.get(col_id): |
| logging.warning( |
| 'Template column with id %s already has template defined.', col_id) |
| |
| self._templates[col_id] = template |
| |
| def addSimpleRedirectButton(self, button_id, caption, url, new_window=True): |
| """Adds a button to the list that simply opens a URL. |
| |
| Args: |
| button_id: The unique id the button. |
| caption: The display string shown to the end user. |
| url: The url to redirect the user to. |
| new_window: Boolean indicating whether the url should open in a new |
| window. |
| """ |
| parameters = { |
| 'link': url, |
| 'new_window': new_window |
| } |
| bounds = [0, 'all'] |
| # add a simple redirect button that is always active. |
| self.__addButton(button_id, caption, bounds, 'redirect_simple', parameters) |
| |
| def addCustomRedirectButton(self, button_id, caption, func, new_window=True): |
| """Adds a button to the list that simply opens a URL. |
| |
| Args: |
| button_id: The unique id of the button. |
| caption: The display string shown to the end user. |
| func: The function to generate a url to redirect the user to. |
| This function should take an entity as first argument and args and |
| kwargs if needed. The return value of this function should be a |
| dictionary with the value for 'link' set to the url to redirect the |
| user to. A value for the key 'caption' can also be returned to |
| dynamically change the caption off the button. |
| new_window: Boolean indicating whether the url should open in a new |
| window. |
| """ |
| if not callable(func): |
| raise TypeError('Given function is not callable') |
| |
| parameters = {'new_window': new_window} |
| # add a custom redirect button that is active on a single row |
| self.__addButton(id, caption, [1, 1], 'redirect_custom', parameters) |
| self._button_functions[id] = func |
| |
| def addPostButton(self, button_id, caption, url, bounds, keys, |
| refresh='current', redirect=False): |
| """This button is used when there is something to send to the backend in a |
| POST request. |
| |
| Sets multiselect to True. |
| |
| Args: |
| button_id: The unique id of the button. |
| caption: The display string shown to the end user. |
| url: The URL to make the POST request to. |
| bounds: An array of size two with integers or of an integer and the |
| keyword "all". This indicates how many rows need to be selected |
| for the button to be pressable. |
| keys: A list of column identifiers of which the content of the selected |
| rows will be send to the server when the button is pressed. |
| refresh: Indicates which list to refresh, is the current list by default. |
| The keyword 'all' can be used to refresh all lists on the page or |
| a integer index referring to the idx of the list to refresh can |
| be given. |
| redirect: Set to True to have the user be redirected to a URL returned by |
| the URL where the POST request hits. |
| """ |
| self.multiselect = True |
| |
| parameters = { |
| 'url': url, |
| 'keys': keys, |
| 'refresh': refresh, |
| 'redirect': redirect, |
| } |
| self.__addButton(button_id, caption, bounds, 'post', parameters) |
| |
| def addPostEditButton( |
| self, button_id, caption, url='', keys=None, refresh='current'): |
| """This button is used when all changed values should be posted. |
| |
| Args: |
| See addPostButton |
| """ |
| parameters = { |
| 'url': url, |
| 'refresh': refresh, |
| } |
| |
| if keys: |
| parameters['keys'] = keys |
| |
| self.__addButton(button_id, caption, None, 'post_edit', parameters) |
| |
| def setRowAction(self, func, new_window=True): |
| """The redirects the user to a URL when clicking on a row in the list. |
| |
| This sets multiselect to False as indicated in the protocol spec. |
| |
| Args: |
| func: The function that returns the url to redirect the user to. |
| This function should take an entity as first argument and args and |
| kwargs if needed. |
| new_window: Boolean indicating whether the url should open in a new |
| window. |
| """ |
| if not callable(func): |
| raise TypeError('Given function is not callable') |
| |
| self.multiselect = False |
| |
| parameters = {'new_window': new_window} |
| self._row_operation = { |
| 'type': 'redirect_custom', |
| 'parameters': parameters |
| } |
| self._row_operation_func = func |
| |
| def setDefaultSort(self, col_id, order='asc'): |
| """Sets the default sort order for the list. |
| |
| Args: |
| id: The id of the column to sort on by default. If this evaluates to |
| False then the default sort order will be removed. |
| order: The order in which to sort, either 'asc' or 'desc'. |
| The default value is 'asc'. |
| """ |
| if col_id and col_id not in self._col_map: |
| raise ValueError('Id %s is not a defined column (Known columns %s)' |
| % (col_id, self._col_map.keys())) |
| |
| if order not in ['asc', 'desc']: |
| raise ValueError('%s is not a valid order' % order) |
| |
| self._sortname = col_id if col_id else '' |
| self._sortorder = order |
| |
| def setFeatures(self, features): |
| """Sets features for the list. |
| """ |
| self._features = features |
| |
| |
| class ListFeatures(object): |
| """Represents features of the list which define, for instance, which |
| elements should be displayed. |
| """ |
| |
| @classmethod |
| def defaultFeatures(cls): |
| """Constructs a default ListFeatures object which may be used, when a list |
| does not define one on its own. |
| """ |
| |
| features = cls() |
| features.setCookieService(True) |
| features.setColumnSearch(True, True) |
| features.setColumnShowHide(True) |
| features.setSearchDialog(True) |
| features.setCsvExport(True) |
| features.setGlobalSearch(False, '') |
| features.setGlobalSort(False, '') |
| features.setHideHeaders(False) |
| return features |
| |
| def __init__(self): |
| """Initializes values of the newly created object. |
| """ |
| |
| self._cookie_service = { |
| 'enabled': False, |
| } |
| |
| self._column_search = { |
| 'enabled': False, |
| 'regexp': False |
| } |
| |
| self._columns_show_hide = { |
| 'enabled': False |
| } |
| |
| self._search_dialog = { |
| 'enabled': False |
| } |
| |
| self._csv_export = { |
| 'enabled': False |
| } |
| |
| self._global_search = { |
| 'enabled': False, |
| 'element_path': '' |
| } |
| |
| self._global_sort = { |
| 'enabled': False, |
| 'element_paths': '' |
| } |
| |
| self._hide_headers = { |
| 'enabled': False |
| } |
| |
| def setCookieService(self, enabled): |
| self._cookie_service['enabled'] = enabled |
| |
| def setColumnSearch(self, enabled, regexp): |
| self._column_search['enabled'] = enabled |
| self._column_search['regexp'] = regexp |
| |
| def setColumnShowHide(self, enabled): |
| self._columns_show_hide['enabled'] = enabled |
| |
| def setSearchDialog(self, enabled): |
| self._search_dialog['enabled'] = enabled |
| |
| def setCsvExport(self, enabled): |
| self._csv_export['enabled'] = enabled |
| |
| def setGlobalSearch(self, enabled, element_path): |
| if enabled: |
| if not element_path: |
| logging.warning('Trying to enable global search with no element_path') |
| else: |
| if element_path: |
| logging.warning('Non empty element_path in disabled global search') |
| |
| self._global_search['enabled'] = enabled |
| self._global_search['element_path'] = element_path |
| |
| def setGlobalSort(self, enabled, element_paths): |
| if enabled: |
| if not element_paths: |
| logging.warning('Trying to enable global sort with no element_paths') |
| else: |
| if element_paths: |
| logging.warning('Non empty element_paths in disabled global sort') |
| |
| self._global_sort['enabled'] = enabled |
| self._global_sort['element_paths'] = element_paths |
| |
| def setHideHeaders(self, enabled): |
| self._hide_headers['enabled'] = enabled |
| |
| def get(self): |
| """Returns a dictionary which contains all the features. |
| """ |
| |
| return { |
| 'cookie_service': self._cookie_service, |
| 'column_search': self._column_search, |
| 'columns_show_hide': self._columns_show_hide, |
| 'search_dialog': self._search_dialog, |
| 'csv_export': self._csv_export, |
| 'global_search': self._global_search, |
| 'global_sort': self._global_sort, |
| 'hide_headers': self._hide_headers, |
| } |
| |
| class ListConfigurationResponse(Template): |
| """Class that builds the template for configuring a list. |
| """ |
| |
| def __init__(self, data, config, idx, description='', preload_list=True): |
| """Initializes the configuration. |
| |
| Args: |
| data: a RequestData object |
| config: A ListConfiguration object. |
| idx: A number uniquely identifying this list. ValueError will be raised if |
| not an int. |
| description: The description of this list, as should be shown to the |
| user. |
| preload_list: Boolean to indicate whether the list should be loaded |
| when this configuration is rendered. If you want the list to be |
| loaded later (such as in the iconic dashboard) set preload_list |
| to False. |
| """ |
| self._data = data |
| self._config = config |
| self._idx = int(idx) |
| self._description = description |
| self._preload_list = preload_list |
| |
| super(ListConfigurationResponse, self).__init__(data) |
| |
| def context(self): |
| """Returns the context for the current template. |
| """ |
| configuration = self._constructConfigDict() |
| |
| context = { |
| 'idx': self._idx, |
| 'configuration': json.dumps(configuration), |
| 'description': self._description, |
| 'preload_list': self._preload_list |
| } |
| return context |
| |
| def _constructConfigDict(self): |
| """Builds the core of the list configuration that is sent to the client. |
| |
| Among other things this configuration defines the columns and buttons |
| present on the list. |
| """ |
| configuration = { |
| 'autowidth': self._config.autowidth, |
| 'colNames': self._config._col_names, |
| 'colModel': self._config._col_model, |
| 'height': self._config.height, |
| 'rowList': self._config._row_list, |
| 'rowNum': self._config._row_num, |
| 'sortname': self._config._sortname, |
| 'sortorder': self._config._sortorder, |
| 'multiselect': self._config.multiselect, |
| 'multiboxonly': self._config.multiselect, |
| 'toolbar': self._config.toolbar, |
| } |
| |
| if self._config._footer_row: |
| configuration['footerrow'] = self._config._footer_row |
| |
| if self._config._features: |
| features = self._config._features |
| else: |
| features = ListFeatures.defaultFeatures() |
| |
| operations = { |
| 'buttons': self._config._buttons, |
| 'row': self._config._row_operation, |
| } |
| |
| listConfiguration = { |
| 'configuration': configuration, |
| 'features': features.get(), |
| 'operations': operations, |
| 'templates': self._config._templates, |
| } |
| return listConfiguration |
| |
| def templatePath(self): |
| """Returns the path to the template that should be used in render(). |
| """ |
| return 'soc/list/list.html' |
| |
| |
| class ListContentResponse(object): |
| """Class that builds the response for a list content request. |
| """ |
| |
| def __init__(self, request, config): |
| """Initializes the list response. |
| |
| The request given can define the start parameter in the GET request |
| otherwise an empty string will be used indicating a request for the first |
| batch. |
| |
| Public fields: |
| start: The start argument as parsed from the request. |
| next: The value that should be used to query for the next set of |
| rows. In other words what start will be on the next roundtrip. |
| limit: The maximum number of rows to return as indicated by the request, |
| defaults to 50. This is not enforced by this object. |
| |
| Args: |
| request: The HTTPRequest containing the request for data. |
| config: A ListConfiguration object |
| """ |
| self._request = request |
| self._config = config |
| |
| self.__rows = [] |
| |
| get_args = request.GET |
| self.next = '' |
| self.start = html.escape(get_args.get('start', '')) |
| self.limit = int(get_args.get('limit', 50)) |
| |
| def addRow(self, entity, *args, **kwargs): |
| """Renders a row for a single entity. |
| |
| Args: |
| entity: The entity to render. |
| args: The args passed to the render functions defined in the config. |
| kwargs: The kwargs passed to the render functions defined in the config. |
| """ |
| columns = {} |
| for col_id, func in self._config._col_functions.iteritems(): |
| col_model = self._config._col_map.get(col_id, {}) |
| value = func(entity, *args, **kwargs) |
| if value is None: |
| value = '' |
| column_type = ColumnTypeFactory.create(col_model['column_type']) |
| columns[col_id] = column_type.safe(value) |
| |
| row = {} |
| buttons = {} |
| row_buttons = {} |
| |
| if self._config._row_operation_func: |
| # perform the row operation function to retrieve the link |
| link = self._config._row_operation_func(entity, *args, **kwargs) |
| if link: |
| row['link'] = link |
| |
| for button_id, func in self._config._button_functions.iteritems(): |
| # The function called here should return a dictionary with 'link' and |
| # an optional 'caption' as keys. |
| buttons[button_id] = func(entity, *args, **kwargs) |
| |
| for col_id, buttons in self._config._row_buttons.iteritems(): |
| row_buttons[col_id] = { |
| 'buttons_def': {}, |
| } |
| for button_id, button_config in buttons.iteritems(): |
| func = self._config._row_button_functions[col_id][button_id] |
| link = func(entity) |
| if link: |
| button_config['parameters']['link'] = link |
| row_buttons[col_id]['buttons_def'][button_id] = button_config |
| |
| operations = { |
| 'row': row, |
| 'buttons': buttons, |
| 'row_buttons': row_buttons |
| } |
| |
| data = { |
| 'columns': columns, |
| 'operations': operations, |
| } |
| self.__rows.append(data) |
| |
| def content(self): |
| """Returns the object that should be parsed to JSON. |
| """ |
| data = {self.start: self.__rows} |
| return {'data': data, |
| 'next': self.next} |
| |
| |
| def collectKeys(prop, data): |
| """Collects all keys for the specified property.""" |
| keys = (prop.get_value_for_datastore(i) for i in data) |
| return [i for i in keys if i] |
| |
| |
| def collectParentKeys(data): |
| """Collects all parent keys for the specified data.""" |
| keys = (i.parent_key() for i in data) |
| return [i for i in keys if i] |
| |
| |
| def distributeKeys(prop, data, prefetched_dict): |
| """Distributes the keys for the specified property.""" |
| for i in data: |
| key = prop.get_value_for_datastore(i) |
| |
| if key not in prefetched_dict: |
| continue |
| |
| value = prefetched_dict[key] |
| setattr(i, prop.name, value) |
| |
| |
| def distributeParentKeys(data, prefetched_dict): |
| """Distributes the keys for the parent property. |
| |
| Uses an AppEngine internal api (the _parent property). See also: |
| https://groups.google.com/forum/#!topic/google-appengine-python/eBAzvJRAvH8 |
| """ |
| for i in data: |
| key = i.parent_key() |
| |
| if key not in prefetched_dict: |
| continue |
| |
| value = prefetched_dict[key] |
| try: |
| # BAD BAD BAD |
| i._parent = value |
| except Exception as e: |
| logging.exception(e) |
| |
| |
| def _prefetchFieldsAsync(model, fields, data, parent): |
| """Prefetches the specified fields in data asynchronously. |
| |
| NOTE: The key difference here is that, we don't redistribute the keys! The |
| caller is expected to do it. |
| """ |
| keys = [] |
| |
| for field in fields: |
| prop = getattr(model, field, None) |
| |
| if not prop: |
| logging.exception( |
| 'Model %s does not have attribute %s', model.kind(), field) |
| return |
| |
| if not isinstance(prop, db.ReferenceProperty): |
| logging.exception( |
| 'Property %s of %s is not a ReferenceProperty but a %s', |
| field, model.kind(), prop.__class__.__name__) |
| return |
| |
| for field in fields: |
| prop = getattr(model, field) |
| keys += collectKeys(prop, data) |
| |
| if parent: |
| keys += collectParentKeys(data) |
| |
| return db.get_async(keys) |
| |
| |
| def _processPrefetchedFields(prefetched_entities, model, fields, data, parent): |
| """After prefetching the entities for fields distribute the keys. |
| """ |
| prefetched_dict = dict((i.key(), i) for i in prefetched_entities if i) |
| |
| for field in fields: |
| prop = getattr(model, field) |
| distributeKeys(prop, data, prefetched_dict) |
| |
| if parent: |
| distributeParentKeys(data, prefetched_dict) |
| |
| |
| def prefetchFields(model, fields, data, parent): |
| """Prefetches the specified fields in data. |
| """ |
| entities_future = _prefetchFieldsAsync(model, fields, data, parent) |
| prefetched_entities = entities_future.get_result() |
| |
| _processPrefetchedFields(prefetched_entities, model, fields, data, parent) |
| |
| |
| def _prefetchListFieldsAsync(model, fields, data): |
| """Prefetches the specified list fields in data asynchronously. |
| |
| NOTE: The key difference here is that, we don't distribute the keys! The |
| caller is expected to do it. |
| """ |
| for field in fields: |
| prop = getattr(model, field, None) |
| |
| if not prop: |
| logging.exception( |
| 'Model %s does not have attribute %s', model.kind(), field) |
| return |
| |
| if not isinstance(prop, db.ListProperty): |
| logging.exception( |
| 'Property %s of %s is not a ReferenceProperty but a %s', |
| field, model.kind(), prop.__class__.__name__) |
| return |
| |
| keys = [] |
| |
| for field in fields: |
| for i in data: |
| keys += getattr(i, field) |
| |
| return db.get_async(keys) |
| |
| |
| def prefetchListFields(model, fields, data): |
| """Prefetches the specified list fields in data. |
| """ |
| entities_future = _prefetchListFieldsAsync(model, fields, data) |
| prefetched_entities = entities_future.get_result() |
| |
| prefetched_dict = dict((i.key(), i) for i in prefetched_entities if i) |
| |
| return prefetched_dict |
| |
| |
| def keyStarter(start, q): |
| """Returns a starter for the specified key-based model. |
| """ |
| if not start: |
| return True |
| if '/' in start: |
| return False |
| try: |
| start_entity = db.get(start) |
| except db.BadKeyError: |
| return False |
| if not start_entity: |
| return False |
| q.filter('__key__ >=', start_entity.key()) |
| return True |
| |
| |
| class ListPrefetcher(object): |
| """Interface for list data prefetching. |
| |
| It is used to obtain arbitrary values that can be used at the point |
| the rows of a list are being constructed in order to achieve |
| better performance. |
| """ |
| |
| def prefetch(self, entities): |
| """Does the prefetching work for the specified list of entities and |
| returns the prefetched data. |
| |
| Args: |
| entities: List of entities for which additional data should be prefetched. |
| |
| Returns: |
| A dict mapping keys of prefetched values and the actual values. |
| """ |
| raise NotImplementedError |
| |
| |
| class RawQueryContentResponseBuilder(object): |
| """Builds a ListContentResponse for lists that are based on a single query. |
| """ |
| |
| def __init__(self, request, config, query, starter, |
| ender=None, skipper=None, prefetcher=None, |
| row_adder=None): |
| """Initializes the fields needed to built a response. |
| |
| Args: |
| request: The HTTPRequest containing the request for data. |
| config: The ListConfiguration object. |
| fields: The fields to query on. |
| query: The query object to use. |
| starter: The function used to retrieve the start entity. |
| ender: The function used to retrieve the value for the next start. |
| skipper: The function used to determine whether to skip a value. |
| prefetcher: A Prefetcher implementation that can be used |
| for increased performance. |
| """ |
| if not ender: |
| ender = lambda entity, is_last, next_cursor: ( |
| 'done' if is_last else ( |
| str(entity.key()) |
| if isinstance(entity, db.Model) else next_cursor.urlsafe())) |
| if not skipper: |
| skipper = lambda entity, start: False |
| if not prefetcher: |
| prefetcher = EMPTY_PREFETCHER |
| if not row_adder: |
| row_adder = lambda content_response, entity, *args: \ |
| content_response.addRow(entity, *args) |
| |
| self._request = request |
| self._config = config |
| self._query = query |
| self._starter = starter |
| self._ender = ender |
| self._skipper = skipper |
| self._prefetcher = prefetcher |
| self._row_adder = row_adder |
| |
| def build(self, *args, **kwargs): |
| """Returns a ListContentResponse containing the data as indicated by the |
| query. |
| |
| The start variable will be used as the starting key for our query, the data |
| returned does not contain the entity that is referred to by the start key. |
| The next variable will be defined as the key of the last entity returned, |
| empty if there are no entities to return. |
| |
| Args and Kwargs passed into this method will be passed along to |
| _addEntity() method. |
| """ |
| content_response = ListContentResponse(self._request, self._config) |
| |
| start = content_response.start |
| |
| if start == 'done': |
| logging.warning('Received query with "done" start key') |
| # return empty response |
| return content_response |
| |
| if not self._starter(start, self._query): |
| logging.warning('Received data query for non-existing start entity %s', start) |
| # return empty response |
| return content_response |
| |
| count = content_response.limit + 1 |
| entities = self._query.fetch(count) |
| |
| is_last = len(entities) != count |
| |
| extra_args, extra_kwargs = self._prefetcher.prefetch(entities) |
| args = list(args) + list(extra_args) |
| kwargs.update(extra_kwargs) |
| |
| for entity in entities[0:content_response.limit]: |
| if self._skipper(entity, start): |
| continue |
| self._row_adder(content_response, entity, *args, **kwargs) |
| |
| if entities: |
| content_response.next = self._ender(entities[-1], is_last, start) |
| else: |
| content_response.next = self._ender(None, True, start) |
| |
| return content_response |
| |
| def buildNDB(self, *args, **kwargs): |
| """Returns a ListContentResponse containing the data as indicated by the |
| query. |
| |
| The start variable will be used as the starting key for our query, the data |
| returned does not contain the entity that is referred to by the start key. |
| The next variable will be defined as the key of the last entity returned, |
| empty if there are no entities to return. |
| |
| Args and Kwargs passed into this method will be passed along to |
| _addEntity() method. |
| """ |
| content_response = ListContentResponse(self._request, self._config) |
| |
| start = content_response.start |
| |
| if start == 'done': |
| logging.warning('Received query with "done" start key') |
| # return empty response |
| return content_response |
| |
| count = content_response.limit |
| cursor = datastore_query.Cursor(urlsafe=start) if start else None |
| |
| entities, next_cursor, has_more = self._query.fetch_page( |
| count, start_cursor=cursor) |
| |
| is_last = not has_more or next_cursor is None |
| |
| if isinstance(self._prefetcher, ListPrefetcher): |
| cached_data = self._prefetcher.prefetch(entities) |
| args = [cached_data] |
| else: |
| extra_args, extra_kwargs = self._prefetcher.prefetch(entities) |
| args = list(args) + list(extra_args) |
| kwargs.update(extra_kwargs) |
| |
| for entity in entities: |
| if self._skipper(entity, start): |
| continue |
| self._row_adder(content_response, entity, *args, **kwargs) |
| |
| if entities: |
| content_response.next = self._ender(None, is_last, next_cursor) |
| else: |
| content_response.next = self._ender(None, True, None) |
| |
| return content_response |