From 3cf71a365f190faf54377fa2e175718877948ce5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sybren=20A=2E=20St=C3=BCvel?= Date: Wed, 26 Oct 2016 17:18:53 +0200 Subject: [PATCH] =?UTF-8?q?Forms=20for=20attachments=20work,=20VERY=20HACK?= =?UTF-8?q?ISH=20Hardcodedness=E2=84=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pillar/api/node_types/asset.py | 1 - pillar/web/nodes/attachments.py | 50 ++++++++++++ pillar/web/nodes/forms.py | 140 ++++++++++++++------------------ pillar/web/nodes/routes.py | 89 +++++++------------- pillar/web/utils/forms.py | 4 +- 5 files changed, 144 insertions(+), 140 deletions(-) diff --git a/pillar/api/node_types/asset.py b/pillar/api/node_types/asset.py index e53ce807..be71fc64 100644 --- a/pillar/api/node_types/asset.py +++ b/pillar/api/node_types/asset.py @@ -43,7 +43,6 @@ node_type_asset = { }, 'form_schema': { 'content_type': {'visible': False}, - 'attachments': {'visible': False}, 'order': {'visible': False}, 'tags': {'visible': False}, 'categories': {'visible': False} diff --git a/pillar/web/nodes/attachments.py b/pillar/web/nodes/attachments.py index 5b57f178..ce9d8936 100644 --- a/pillar/web/nodes/attachments.py +++ b/pillar/web/nodes/attachments.py @@ -4,9 +4,11 @@ import re from bson import ObjectId import flask import pillarsdk +import wtforms from pillar.api.node_types import ATTACHMENT_SLUG_REGEX from pillar.web.utils import system_util +from pillar.web.utils.forms import build_file_select_form, CustomFormField shortcode_re = re.compile(r'@\[(%s)\]' % ATTACHMENT_SLUG_REGEX) log = logging.getLogger(__name__) @@ -86,3 +88,51 @@ def render_attachment_file_image(sdk_file): variations = {var.size: var for var in sdk_file.variations} return flask.render_template('nodes/attachments/file_image.html', file=sdk_file, vars=variations) + + +def attachment_form_group_create(schema_prop): + """Creates a wtforms.FieldList for attachments.""" + + file_select_form_group = _attachment_build_single_field(schema_prop) + field = wtforms.FieldList(CustomFormField(file_select_form_group), min_entries=1) + + return field + + +def _attachment_build_single_field(schema_prop): + # Ugly hard-coded schema. + fake_schema = { + 'slug': schema_prop['propertyschema'], + 'oid': schema_prop['valueschema']['schema']['oid'], + } + file_select_form_group = build_file_select_form(fake_schema) + return file_select_form_group + + +def attachment_form_group_set_data(db_prop_value, schema_prop, field_list): + """Populates the attachment form group with data from MongoDB.""" + + assert isinstance(db_prop_value, dict) + + # Extra entries are caused by min_entries=1 in the form creation. + while len(field_list): + field_list.pop_entry() + + for slug, att_data in sorted(db_prop_value.iteritems()): + file_select_form_group = _attachment_build_single_field(schema_prop) + subform = file_select_form_group() + + # Even uglier hard-coded + subform.slug = slug + subform.oid = att_data['oid'] + + field_list.append_entry(subform) + + +def attachment_form_parse_post_data(data): + """Returns a dict that can be stored in the node.properties.attachments.""" + + # Moar ugly hardcodedness. + attachments = {allprops['slug']: {'oid': allprops['oid']} + for allprops in data} + return attachments diff --git a/pillar/web/nodes/forms.py b/pillar/web/nodes/forms.py index fcb27ee0..6edaa9ef 100644 --- a/pillar/web/nodes/forms.py +++ b/pillar/web/nodes/forms.py @@ -19,14 +19,30 @@ from wtforms import FieldList from wtforms.validators import DataRequired from pillar.web.utils import system_util from pillar.web.utils.forms import FileSelectField -from pillar.web.utils.forms import ProceduralFileSelectForm from pillar.web.utils.forms import CustomFormField from pillar.web.utils.forms import build_file_select_form +from . import attachments + log = logging.getLogger(__name__) -def add_form_properties(form_class, node_schema, form_schema, prefix=''): +def iter_node_properties(node_type): + """Generator, iterates over all node properties with form schema.""" + + node_schema = node_type['dyn_schema'].to_dict() + form_schema = node_type['form_schema'].to_dict() + + for prop_name, prop_schema in node_schema.iteritems(): + prop_fschema = form_schema.get(prop_name, {}) + + if not prop_fschema.get('visible', True): + continue + + yield prop_name, prop_schema, prop_fschema + + +def add_form_properties(form_class, node_type): """Add fields to a form based on the node and form schema provided. :type node_schema: dict :param node_schema: the validation schema used by Cerberus @@ -37,33 +53,16 @@ def add_form_properties(form_class, node_schema, form_schema, prefix=''): show and hide) """ - for prop, schema_prop in node_schema.iteritems(): - form_prop = form_schema.get(prop, {}) - if prop == 'items': - continue - if not form_prop.get('visible', True): - continue - prop_name = "{0}{1}".format(prefix, prop) + for prop_name, schema_prop, form_prop in iter_node_properties(node_type): # Recursive call if detects a dict field_type = schema_prop['type'] - if field_type == 'dict': - # This works if the dictionary schema is hardcoded. - # If we define it using propertyschema and valueschema, this - # validation pattern does not work and crahses. - add_form_properties(form_class, schema_prop['schema'], - form_prop['schema'], "{0}__".format(prop_name)) - continue - if field_type == 'list': - if prop == 'attachments': - # class AttachmentForm(Form): - # pass - # AttachmentForm.file = FileSelectField('file') - # AttachmentForm.size = StringField() - # AttachmentForm.slug = StringField() - field = FieldList(CustomFormField(ProceduralFileSelectForm)) - elif prop == 'files': + if field_type == 'dict': + assert prop_name == 'attachments' + field = attachments.attachment_form_group_create(schema_prop) + elif field_type == 'list': + if prop_name == 'files': schema = schema_prop['schema']['schema'] file_select_form = build_file_select_form(schema) field = FieldList(CustomFormField(file_select_form), @@ -112,8 +111,6 @@ def get_node_form(node_type): class ProceduralForm(Form): pass - node_schema = node_type['dyn_schema'].to_dict() - form_prop = node_type['form_schema'].to_dict() parent_prop = node_type['parent'] ProceduralForm.name = StringField('Name', validators=[DataRequired()]) @@ -126,7 +123,7 @@ def get_node_form(node_type): ProceduralForm.picture = FileSelectField('Picture', file_format='image') ProceduralForm.node_type = HiddenField(default=node_type['name']) - add_form_properties(ProceduralForm, node_schema, form_prop) + add_form_properties(ProceduralForm, node_type) return ProceduralForm() @@ -166,59 +163,44 @@ def process_node_form(form, node_id=None, node_type=None, user=None): if form.parent.data != "": node.parent = form.parent.data - def update_data(node_schema, form_schema, prefix=""): - for pr in node_schema: - schema_prop = node_schema[pr] - form_prop = form_schema.get(pr, {}) - if pr == 'items': - continue - if 'visible' in form_prop and not form_prop['visible']: - continue - prop_name = "{0}{1}".format(prefix, pr) - if schema_prop['type'] == 'dict': - update_data( - schema_prop['schema'], - form_prop['schema'], - "{0}__".format(prop_name)) - continue - data = form[prop_name].data - if schema_prop['type'] == 'dict': - if data == 'None': - continue - elif schema_prop['type'] == 'integer': - if data == '': - data = 0 - else: - data = int(form[prop_name].data) - elif schema_prop['type'] == 'datetime': - data = datetime.strftime(data, - app.config['RFC1123_DATE_FORMAT']) - elif schema_prop['type'] == 'list': - if pr == 'attachments': - # data = json.loads(data) - data = [dict(field='description', files=data)] - elif pr == 'files': - # Only keep those items that actually refer to a file. - data = [file_item for file_item in data - if file_item.get('file')] - # elif pr == 'tags': - # data = [tag.strip() for tag in data.split(',')] - elif schema_prop['type'] == 'objectid': - if data == '': - # Set empty object to None so it gets removed by the - # SDK before node.update() - data = None + for prop_name, schema_prop, form_prop in iter_node_properties(node_type): + data = form[prop_name].data + if schema_prop['type'] == 'dict': + data = attachments.attachment_form_parse_post_data(data) + elif schema_prop['type'] == 'integer': + if data == '': + data = 0 else: - if pr in form: - data = form[prop_name].data - path = prop_name.split('__') - if len(path) > 1: - recursive_prop = recursive( - path, node.properties.to_dict(), data) - node.properties = recursive_prop + data = int(form[prop_name].data) + elif schema_prop['type'] == 'datetime': + data = datetime.strftime(data, current_app.config['RFC1123_DATE_FORMAT']) + elif schema_prop['type'] == 'list': + if prop_name == 'files': + # Only keep those items that actually refer to a file. + data = [file_item for file_item in data + if file_item.get('file')] else: - node.properties[prop_name] = data - update_data(node_schema, form_schema) + log.warning('Ignoring property %s of type %s', + prop_name, schema_prop['type']) + # elif pr == 'tags': + # data = [tag.strip() for tag in data.split(',')] + elif schema_prop['type'] == 'objectid': + if data == '': + # Set empty object to None so it gets removed by the + # SDK before node.update() + data = None + else: + if prop_name in form: + data = form[prop_name].data + path = prop_name.split('__') + assert len(path) == 1 + if len(path) > 1: + recursive_prop = recursive( + path, node.properties.to_dict(), data) + node.properties = recursive_prop + else: + node.properties[prop_name] = data + ok = node.update(api=api) if not ok: log.warning('Unable to update node: %s', node.error) diff --git a/pillar/web/nodes/routes.py b/pillar/web/nodes/routes.py index 678eec3e..d9d8f623 100644 --- a/pillar/web/nodes/routes.py +++ b/pillar/web/nodes/routes.py @@ -22,7 +22,6 @@ from wtforms import SelectMultipleField from flask_login import login_required from jinja2.exceptions import TemplateNotFound -import pillar.web.nodes.attachments from pillar.web.utils import caching from pillar.web.nodes.forms import get_node_form from pillar.web.nodes.forms import process_node_form @@ -35,7 +34,7 @@ from pillar.web.utils.forms import ProceduralFileSelectForm from pillar.web.utils.forms import build_file_select_form from pillar.web import system_util -from . import finders +from . import finders, attachments blueprint = Blueprint('nodes', __name__) log = logging.getLogger(__name__) @@ -261,7 +260,7 @@ def _view_handler_asset(node, template_path, template_action, link_allowed): # Treat it as normal file (zip, blend, application, etc) asset_type = 'file' - node['description'] = pillar.web.nodes.attachments.render_attachments(node, node['description']) + node['description'] = attachments.render_attachments(node, node['description']) template_path = os.path.join(template_path, asset_type) @@ -315,27 +314,18 @@ def edit(node_id): """Generic node editing form """ - def set_properties(dyn_schema, form_schema, node_properties, form, - prefix="", - set_data=True): + def set_properties(dyn_schema, form_schema, node_properties, form, set_data, + prefix=""): """Initialize custom properties for the form. We run this function once before validating the function with set_data=False, so that we can set any multiselect field that was originally specified empty and fill it with the current choices. """ - for prop in dyn_schema: - schema_prop = dyn_schema[prop] - form_prop = form_schema.get(prop, {}) - prop_name = "{0}{1}".format(prefix, prop) - if schema_prop['type'] == 'dict': - set_properties( - schema_prop['schema'], - form_prop['schema'], - node_properties[prop_name], - form, - "{0}__".format(prop_name)) - continue + log.debug('set_properties(..., prefix=%r, set_data=%r) called', prefix, set_data) + + for prop, schema_prop in dyn_schema.iteritems(): + prop_name = "{0}{1}".format(prefix, prop) if prop_name not in form: continue @@ -359,49 +349,32 @@ def edit(node_id): form[prop_name].choices = [(d, d) for d in db_prop_value] # Choices should be a tuple with value and name + if not set_data: + continue + # Assign data to the field - if set_data: - if prop_name == 'attachments': - for attachment_collection in db_prop_value: - for a in attachment_collection['files']: - attachment_form = ProceduralFileSelectForm() - attachment_form.file = a['file'] - attachment_form.slug = a['slug'] - attachment_form.size = 'm' - form[prop_name].append_entry(attachment_form) + if prop_name == 'attachments': + attachments.attachment_form_group_set_data(db_prop_value, schema_prop, + form[prop_name]) + elif prop_name == 'files': + subschema = schema_prop['schema']['schema'] + # Extra entries are caused by min_entries=1 in the form + # creation. + field_list = form[prop_name] + while len(field_list) > len(db_prop_value): + field_list.pop_entry() - elif prop_name == 'files': - schema = schema_prop['schema']['schema'] - # Extra entries are caused by min_entries=1 in the form - # creation. - field_list = form[prop_name] - if len(db_prop_value) > 0: - while len(field_list): - field_list.pop_entry() + for file_data in db_prop_value: + file_form_class = build_file_select_form(subschema) + subform = file_form_class() + for key, value in file_data.iteritems(): + setattr(subform, key, value) + field_list.append_entry(subform) - for file_data in db_prop_value: - file_form_class = build_file_select_form(schema) - subform = file_form_class() - for key, value in file_data.iteritems(): - setattr(subform, key, value) - field_list.append_entry(subform) - - # elif prop_name == 'tags': - # form[prop_name].data = ', '.join(data) - else: - form[prop_name].data = db_prop_value + # elif prop_name == 'tags': + # form[prop_name].data = ', '.join(data) else: - # Default population of multiple file form list (only if - # we are getting the form) - if request.method == 'POST': - continue - if prop_name == 'attachments': - if not db_prop_value: - attachment_form = ProceduralFileSelectForm() - attachment_form.file = 'file' - attachment_form.slug = '' - attachment_form.size = '' - form[prop_name].append_entry(attachment_form) + form[prop_name].data = db_prop_value api = system_util.pillar_api() node = Node.find(node_id, api=api) @@ -446,7 +419,7 @@ def edit(node_id): if node.parent: form.parent.data = node.parent - set_properties(dyn_schema, form_schema, node_properties, form) + set_properties(dyn_schema, form_schema, node_properties, form, set_data=True) # Get previews node.picture = get_file(node.picture, api=api) if node.picture else None diff --git a/pillar/web/utils/forms.py b/pillar/web/utils/forms.py index d0770d29..94debb3f 100644 --- a/pillar/web/utils/forms.py +++ b/pillar/web/utils/forms.py @@ -158,8 +158,8 @@ class CustomFormFieldWidget(object): class CustomFormField(FormField): - def __init__(self, name, **kwargs): - super(CustomFormField, self).__init__(name, **kwargs) + def __init__(self, form_class, **kwargs): + super(CustomFormField, self).__init__(form_class, **kwargs) self.widget = CustomFormFieldWidget()