Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

datatable #46

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ include =
*/src/collective/*
omit =
*/test*
*/upgrades*
/home/*/.buildout/eggs/*
/home/travis/buildout-cache/eggs/*
/home/travis/virtualenv/*
Expand All @@ -16,4 +17,4 @@ omit =
*/lib/*
*.txt
*.rst
*/upgrades.py
*/upgrades.py
32 changes: 32 additions & 0 deletions .github/workflows/legacy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: Legacy tests

on: [push]

jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
python: ["3.8"]
plone: ["52"]
steps:
- uses: actions/checkout@v4
- name: Cache eggs
uses: actions/cache@v4
with:
path: eggs
key: ${{ runner.OS }}-build-python${{ matrix.python }}-${{ matrix.plone }}
- name: Set up Python ${{ matrix.python }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}
- name: Install dependencies
run: |
pip install -r requirements.txt -c constraints_plone${{ matrix.plone }}.txt
cp test_plone${{ matrix.plone }}.cfg buildout.cfg
- name: Install buildout
run: |
buildout -N -t 3 code-analysis:return-status-codes=True
- name: Run tests
run: |
bin/test
4 changes: 2 additions & 2 deletions .github/workflows/meta.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ jobs:
uses: plone/meta/.github/workflows/dependencies.yml@main
release_ready:
uses: plone/meta/.github/workflows/release_ready.yml@main
circular:
uses: plone/meta/.github/workflows/circular.yml@main
# circular:
# uses: plone/meta/.github/workflows/circular.yml@main

##
# To modify the list of default jobs being created add in .meta.toml:
Expand Down
7 changes: 6 additions & 1 deletion CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,14 @@ Changelog
3.0.3 (unreleased)
------------------

- Save attachments as blobfile in the storage adapter, add a view to download them, returns
attachment info in the restapi @form-data endpoint.
[mamico]
- Fix: if there are multiple forms on a page, each csv button downloads the record of all the forms,
now if there is a block_id parameter, the csv is filtered on that.
[mamico]
- Subject templating
[folix-01]

- Handle the edge cases where the `blocks` attribute is not set.
[mamico]

Expand Down
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,12 @@ python-dateutil = ['dateutil']
'souper.plone' = ['souper', 'repoze.catalog']
# extra packages
ignore-packages = [
# these are packages defined in extras_require
'collective.honeypot', 'plone.formwidget.hcaptcha', 'plone.formwidget.recaptcha',
'collective.volto.blocksfield', 'collective.z3cform.norobots',
'plone.app.iterate',
# XXX: This is a temporary fix for make the package usable with Plone 5.2 and 6.0
'plone.base', 'Products.CMFPlone',
]

##
Expand Down
7 changes: 4 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
"Framework :: Plone :: 5.2",
"Framework :: Plone :: 6.0",
"Programming Language :: Python",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
Expand All @@ -50,7 +49,7 @@
package_dir={"": "src"},
include_package_data=True,
zip_safe=False,
python_requires=">=3.7",
python_requires=">=3.8",
install_requires=[
"setuptools",
"z3c.jbot",
Expand All @@ -60,9 +59,11 @@
"plone.keyring",
"plone.i18n",
"plone.memoize",
"plone.namedfile",
"plone.protect",
"plone.registry",
"plone.restapi>=8.36.0",
"plone.base",
"plone.schema",
"Products.GenericSetup",
"Products.PortalTransforms",
"souper.plone",
Expand Down
15 changes: 15 additions & 0 deletions src/collective/volto/formsupport/browser/configure.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,21 @@
template="send_mail_template_table.pt"
permission="zope2.View"
/>

<browser:page
name="saved_data"
for="plone.dexterity.interfaces.IDexterityContent"
class=".saved_data.SavedData"
permission="cmf.ModifyPortalContent"
/>

<browser:page
name="download"
for=".saved_data.ISavedDataTraverse"
class=".saved_data.AttachmentDownload"
permission="cmf.ModifyPortalContent"
/>

<browser:page
name="email-confirm-view"
for="*"
Expand Down
55 changes: 55 additions & 0 deletions src/collective/volto/formsupport/browser/saved_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
from collective.volto.formsupport.interfaces import IFormDataStore
from plone.namedfile.utils import set_headers
from plone.namedfile.utils import stream_data
from Products.Five.browser import BrowserView
from zExceptions import NotFound
from zope.component import getMultiAdapter
from zope.interface import implementer
from zope.publisher.interfaces import IPublishTraverse


class ISavedDataTraverse(IPublishTraverse):
pass


@implementer(ISavedDataTraverse)
class SavedData(BrowserView):
pass


@implementer(IPublishTraverse)
class AttachmentDownload(BrowserView):
def __init__(self, context, request):
super().__init__(context, request)
self.record_id = None
self.field_id = None
self.filename = None

def publishTraverse(self, request, name):
"""
e.g.
https://nohost/page/saved_data/@@download/record_id/field_id/filename
"""
if self.record_id is None:
self.record_id = int(name)
elif self.field_id is None:
self.field_id = name
elif self.filename is None:
self.filename = name
else:
raise NotFound("Not found")
return self

def __call__(self):
store = getMultiAdapter((self.context.context, self.request), IFormDataStore)
# data = FormData(self.context, self.request)
try:
record = store.soup.get(self.record_id)
except KeyError:
raise NotFound("Record not found")
try:
field = record.attrs.get(self.field_id)
except KeyError:
raise NotFound("Field not found")
set_headers(field, self.request.response, filename=field.filename)
return stream_data(field)
30 changes: 26 additions & 4 deletions src/collective/volto/formsupport/datamanager/catalog.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from base64 import b64decode
from collective.volto.formsupport import logger
from collective.volto.formsupport.interfaces import IFormDataStore
from collective.volto.formsupport.utils import get_blocks
from copy import deepcopy
from datetime import datetime
from plone.dexterity.interfaces import IDexterityContent
from plone.namedfile import NamedBlobFile
from plone.restapi.deserializer import json_body
from repoze.catalog.catalog import Catalog
from repoze.catalog.indexes.field import CatalogFieldIndex
Expand Down Expand Up @@ -79,25 +81,45 @@ def add(self, data):
return None

fields = {
x["field_id"]: x.get("custom_field_id", x.get("label", x["field_id"]))
for x in form_fields
f["field_id"]: {
"label": f.get("custom_field_id", f.get("label", f["field_id"])),
"type": f.get("field_type", "text"),
}
for f in form_fields
}

record = Record()
fields_labels = {}
fields_order = []
for field_data in data:
field_id = field_data.get("field_id", "")
value = field_data.get("value", "")
if field_id in fields:
record.attrs[field_id] = value
fields_labels[field_id] = fields[field_id]
field = fields[field_id]
record.attrs[field_id] = self.storedValue(value, field["type"])
fields_labels[field_id] = field["label"]
fields_order.append(field_id)
# else: skip the field
record.attrs["fields_labels"] = fields_labels
record.attrs["fields_order"] = fields_order
record.attrs["date"] = datetime.now()
record.attrs["block_id"] = self.block_id
return self.soup.add(record)

def storedValue(self, value, type):
if type == "attachment":
if value:
if value.get("encoding") == "base64":
data = b64decode(value["data"])
else:
data = value["data"]
return NamedBlobFile(
data=data,
filename=value.get("filename"),
contentType=value.get("content-type", "application/octet-stream"),
)
return value

def length(self):
return len([x for x in self.soup.data.values()])

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
from collective.volto.formsupport.interfaces import ICaptchaSupport
from collective.volto.formsupport.interfaces import ICollectiveVoltoFormsupportLayer
from plone import api
from plone.base.interfaces import IPloneSiteRoot


try:
from plone.base.interfaces import IPloneSiteRoot
except ImportError:
from Products.CMFPlone.interfaces import IPloneSiteRoot

from plone.restapi.behaviors import IBlocks
from plone.restapi.interfaces import IBlockFieldSerializationTransformer
from zope.component import adapter
Expand Down
16 changes: 13 additions & 3 deletions src/collective/volto/formsupport/restapi/services/form_data/csv.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from collective.volto.formsupport.interfaces import IFormDataStore
from io import StringIO
from plone.namedfile import NamedBlobFile
from plone.restapi.serializer.converters import json_compatible
from plone.restapi.services import Service
from zExceptions import NotFound
from zope.component import getMultiAdapter

import csv
Expand All @@ -14,12 +16,15 @@ class FormDataExportGet(Service):
def __init__(self, context, request):
super().__init__(context, request)
self.form_fields_order = []
self.form_block = {}
self.form_block = None
self.block_id = self.request.get("block_id")

blocks = getattr(context, "blocks", {})
if not blocks:
return
raise NotFound("No blocks found")
for id, block in blocks.items():
if self.block_id and id != self.block_id:
continue
block_type = block.get("@type", "")
if block_type == "form":
self.form_block = block
Expand Down Expand Up @@ -69,6 +74,8 @@ def get_data(self):

rows = []
for item in store.search():
if self.block_id and item.attrs.get("block_id") != self.block_id:
continue
data = {}
fields_labels = item.attrs.get("fields_labels", {})
for k in self.get_ordered_keys(item):
Expand All @@ -78,7 +85,10 @@ def get_data(self):
label = fields_labels.get(k, k)
if label not in columns and label not in fixed_columns:
columns.append(label)
data[label] = json_compatible(value)
if isinstance(value, NamedBlobFile):
data[label] = value.filename
else:
data[label] = json_compatible(value)
for k in fixed_columns:
# add fixed columns values
value = item.attrs.get(k, None)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from datetime import timedelta
from plone import api
from plone.memoize import view
from plone.namedfile import NamedBlobFile
from plone.restapi.interfaces import IExpandableElement
from plone.restapi.serializer.converters import json_compatible
from plone.restapi.services import Service
Expand Down Expand Up @@ -97,10 +98,21 @@ def expand_records(self, record):
for k, v in record.attrs.items():
if k in ["fields_labels", "fields_order"]:
continue
data[k] = {
"value": json_compatible(v),
"label": fields_labels.get(k, k),
}
if isinstance(v, NamedBlobFile):
data[k] = {
"value": {
"url": f"{self.context.absolute_url()}/saved_data/@@download/{record.intid}/{k}/{v.filename}",
"filename": v.filename,
"contentType": v.contentType,
"size": v.getSize(),
},
"label": fields_labels.get(k, k),
}
else:
data[k] = {
"value": json_compatible(v),
"label": fields_labels.get(k, k),
}
data["id"] = record.intid
return data

Expand Down
Loading