applification: avoir un lien entre les objets importés et l'application hobo (#74372) #102

Merged
lguerin merged 8 commits from wip/74372-application-links into main 2023-04-17 16:36:05 +02:00
51 changed files with 2919 additions and 291 deletions

View File

@ -57,6 +57,7 @@ def test_data_sources_from_carddefs(pub):
create_superuser(pub)
CardDef.wipe()
pub.custom_view_class.wipe()
NamedDataSource.wipe()
app = login(get_app(pub))
resp = app.get('/backoffice/settings/data-sources/')
@ -80,10 +81,15 @@ def test_data_sources_from_carddefs(pub):
resp = app.get('/backoffice/settings/data-sources/')
assert 'Data Sources from Card Models' in resp.text
assert 'There are no data sources from card models.' not in resp.text
assert '<li><a href="http://example.net/backoffice/data/foo/">foo</a></li>' in resp.text
assert resp.pyquery('.section .objects-list li:first-child a').text() == 'foo'
assert (
'<li><a href="http://example.net/backoffice/data/foo/datasource-card-view/">foo - datasource card view</a></li>'
in resp.text
resp.pyquery('.section .objects-list li:first-child a').attr['href']
== 'http://example.net/backoffice/data/foo/'
)
assert resp.pyquery('.section .objects-list li:last-child a').text() == 'foo - datasource card view'
assert (
resp.pyquery('.section .objects-list li:last-child a').attr['href']
== 'http://example.net/backoffice/data/foo/datasource-card-view/'
)
@ -106,6 +112,8 @@ def test_data_sources_agenda_without_chrono(pub):
def test_data_sources_agenda(pub, chrono_url):
create_superuser(pub)
NamedDataSource.wipe()
CardDef.wipe()
pub.custom_view_class.wipe()
app = login(get_app(pub))
resp = app.get('/backoffice/settings/data-sources/')
@ -125,21 +133,22 @@ def test_data_sources_agenda(pub, chrono_url):
resp = app.get('/backoffice/settings/data-sources/')
assert 'Agendas' in resp.text
assert 'There are no agendas.' not in resp.text
assert '<li><a href="%s/">foobar (foobar)</a></li>' % data_source.id in resp.text
assert resp.pyquery('.section .objects-list li:first-child a').text() == 'foobar (foobar)'
assert resp.pyquery('.section .objects-list li:first-child a').attr['href'] == data_source.get_admin_url()
data_source.external_status = 'not-found'
data_source.store()
resp = app.get('/backoffice/settings/data-sources/')
assert (
'<li><a href="%s/">foobar (foobar) - <span class="extra-info">not found</span></a></li>'
% data_source.id
in resp.text
)
assert resp.pyquery('.section .objects-list li:first-child a').text() == 'foobar (foobar) - not found'
assert resp.pyquery('.section .objects-list li:first-child a').attr['href'] == data_source.get_admin_url()
assert resp.pyquery('.section .objects-list li:first-child a span.extra-info').text() == 'not found'
def test_data_sources_users(pub):
create_superuser(pub)
NamedDataSource.wipe()
CardDef.wipe()
pub.custom_view_class.wipe()
app = login(get_app(pub))
resp = app.get('/backoffice/settings/data-sources/')
@ -153,7 +162,8 @@ def test_data_sources_users(pub):
data_source.store()
resp = app.get('/backoffice/settings/data-sources/')
assert 'There are no users data sources defined.' not in resp
assert '<li><a href="%s/">foobar (foobar)</a></li>' % data_source.id in resp
assert resp.pyquery('.section .objects-list li:first-child a').text() == 'foobar (foobar)'
assert resp.pyquery('.section .objects-list li:first-child a').attr['href'] == data_source.get_admin_url()
def test_data_sources_new(pub):

View File

@ -59,7 +59,7 @@ def test_workflows_default(pub):
app = login(get_app(pub))
resp = app.get('/backoffice/workflows/')
assert 'Default' in resp.text
resp = resp.click(href=r'^_default/')
resp = resp.click(href=r'/backoffice/workflows/_default/$')
assert 'Just Submitted' in resp.text
assert 'This is the default workflow' in resp.text
# makes sure it cannot be edited
@ -219,9 +219,9 @@ def test_workflows_category(pub):
resp = app.get('/backoffice/workflows/')
assert [x.attrib['href'] for x in resp.pyquery('.single-links a')] == [
'_default/',
'_carddef_default/',
'1/',
'http://example.net/backoffice/workflows/_default/',
'http://example.net/backoffice/workflows/_carddef_default/',
'http://example.net/backoffice/workflows/1/',
]
assert 'Uncategorised' not in resp.text
@ -235,9 +235,9 @@ def test_workflows_category(pub):
# a category is defined -> an implicit "Uncategorised" section is displayed.
resp = app.get('/backoffice/workflows/')
assert [x.attrib['href'] for x in resp.pyquery('.single-links a')] == [
'_default/',
'_carddef_default/',
'1/',
'http://example.net/backoffice/workflows/_default/',
'http://example.net/backoffice/workflows/_carddef_default/',
'http://example.net/backoffice/workflows/1/',
]
assert 'Uncategorised' in resp.text
@ -257,9 +257,9 @@ def test_workflows_category(pub):
resp = app.get('/backoffice/workflows/')
assert '<h2>a new category' in resp.text
assert [x.attrib['href'] for x in resp.pyquery('.single-links a')] == [
'_default/',
'_carddef_default/',
'1/',
'http://example.net/backoffice/workflows/_default/',
'http://example.net/backoffice/workflows/_carddef_default/',
'http://example.net/backoffice/workflows/1/',
]
assert 'Uncategorised' not in resp.text

View File

@ -6,6 +6,7 @@ import xml.etree.ElementTree as ET
import pytest
from wcs.applications import Application, ApplicationElement
from wcs.blocks import BlockDef
from wcs.carddef import CardDef
from wcs.categories import (
@ -22,6 +23,7 @@ from wcs.data_sources import NamedDataSource
from wcs.fields import BlockField, CommentField, ComputedField, PageField, StringField
from wcs.formdef import FormDef
from wcs.mail_templates import MailTemplate
from wcs.sql import Equal
from wcs.wf.form import WorkflowFormFieldsFormDef
from wcs.workflows import Workflow, WorkflowBackofficeFieldsFormDef, WorkflowVariablesFieldsFormDef
from wcs.wscalls import NamedWsCall
@ -41,6 +43,8 @@ coucou = 1234
'''
)
Application.wipe()
ApplicationElement.wipe()
Category.wipe()
FormDef.wipe()
CardDefCategory.wipe()
@ -56,6 +60,7 @@ coucou = 1234
DataSourceCategory.wipe()
NamedDataSource.wipe()
NamedWsCall.wipe()
pub.custom_view_class.wipe()
return pub
@ -481,6 +486,12 @@ def test_export_import_redirect_url(pub):
mail_template_category = MailTemplateCategory(name='Test')
mail_template_category.store()
comment_template = CommentTemplate(name='Test')
comment_template.store()
comment_template_category = CommentTemplateCategory(name='Test')
comment_template_category.store()
elements = [
('forms', '/backoffice/forms/%s/' % formdef.id),
('cards', '/backoffice/cards/%s/' % carddef.id),
@ -497,6 +508,11 @@ def test_export_import_redirect_url(pub):
'mail-templates-categories',
'/backoffice/workflows/mail-templates/categories/%s/' % mail_template_category.id,
),
('comment-templates', '/backoffice/workflows/comment-templates/%s/' % comment_template.id),
(
'comment-templates-categories',
'/backoffice/workflows/comment-templates/categories/%s/' % comment_template_category.id,
),
]
for object_type, obj_url in elements:
resp = get_app(pub).get(sign_uri('/api/export-import/%s/' % object_type))
@ -521,7 +537,11 @@ def create_bundle(elements, *args):
manifest_json = {
'application': 'Test',
'slug': 'test',
'description': '',
'icon': 'foo.png',
'description': 'Foo Bar',
'documentation_url': 'http://foo.bar',
'version_number': '42.0',
'version_notes': 'foo bar blah',
'elements': elements,
}
manifest_fd = io.BytesIO(json.dumps(manifest_json, indent=2).encode())
@ -529,6 +549,13 @@ def create_bundle(elements, *args):
tarinfo.size = len(manifest_fd.getvalue())
tar.addfile(tarinfo, fileobj=manifest_fd)
icon_fd = io.BytesIO(
b'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQAAAAA3bvkkAAAACklEQVQI12NoAAAAggCB3UNq9AAAAABJRU5ErkJggg=='
)
tarinfo = tarfile.TarInfo('foo.png')
tarinfo.size = len(icon_fd.getvalue())
tar.addfile(tarinfo, fileobj=icon_fd)
for path, obj in args:
tarinfo = tarfile.TarInfo(path)
if hasattr(obj, 'export_for_application'):
@ -603,6 +630,12 @@ def test_export_import_bundle_import(pub):
mail_template.category = mail_template_category
mail_template.store()
comment_template_category = CommentTemplateCategory(name='Test')
comment_template_category.store()
comment_template = CommentTemplate(name='Test')
comment_template.category = comment_template_category
comment_template.store()
wscall = NamedWsCall(name='Test')
wscall.store()
@ -619,6 +652,8 @@ def test_export_import_bundle_import(pub):
{'type': 'workflows', 'slug': 'test', 'name': 'test'},
{'type': 'mail-templates-categories', 'slug': 'test', 'name': 'test'},
{'type': 'mail-templates', 'slug': 'test', 'name': 'test'},
{'type': 'comment-templates-categories', 'slug': 'test', 'name': 'test'},
{'type': 'comment-templates', 'slug': 'test', 'name': 'test'},
{'type': 'data-sources-categories', 'slug': 'test', 'name': 'test'},
{'type': 'data-sources', 'slug': 'test', 'name': 'test'},
{'type': 'wscalls', 'slug': 'test', 'name': 'test'},
@ -635,6 +670,8 @@ def test_export_import_bundle_import(pub):
('data-sources/test', data_source),
('mail-templates-categories/test', mail_template_category),
('mail-templates/test', mail_template),
('comment-templates-categories/test', comment_template_category),
('comment-templates/test', comment_template),
('roles/test', role),
('wscalls/test', wscall),
)
@ -648,6 +685,8 @@ def test_export_import_bundle_import(pub):
Workflow.wipe()
MailTemplateCategory.wipe()
MailTemplate.wipe()
CommentTemplateCategory.wipe()
CommentTemplate.wipe()
DataSourceCategory.wipe()
NamedDataSource.wipe()
pub.role_class.wipe()
@ -664,7 +703,7 @@ def test_export_import_bundle_import(pub):
afterjob_url = resp.json['url']
resp = get_app(pub).put(sign_uri(afterjob_url))
assert resp.json['data']['status'] == 'completed'
assert resp.json['data']['completion_status'] == '17/17 (100%)'
assert resp.json['data']['completion_status'] == '19/19 (100%)'
assert Category.count() == 1
assert FormDef.count() == 1
@ -683,11 +722,37 @@ def test_export_import_bundle_import(pub):
assert MailTemplateCategory.count() == 1
assert MailTemplate.count() == 1
assert MailTemplate.select()[0].category_id == MailTemplateCategory.select()[0].id
assert CommentTemplateCategory.count() == 1
assert CommentTemplate.count() == 1
assert CommentTemplate.select()[0].category_id == CommentTemplateCategory.select()[0].id
assert DataSourceCategory.count() == 1
assert NamedDataSource.count() == 1
assert NamedDataSource.select()[0].category_id == DataSourceCategory.select()[0].id
assert NamedWsCall.count() == 1
assert pub.custom_view_class().count() == 1
assert Application.count() == 1
application = Application.select()[0]
assert application.slug == 'test'
assert application.name == 'Test'
assert application.description == 'Foo Bar'
assert application.documentation_url == 'http://foo.bar'
assert application.version_number == '42.0'
assert application.version_notes == 'foo bar blah'
assert application.icon.base_filename == 'foo.png'
assert application.editable is False
assert ApplicationElement.count() == 15
# create some links to elements not present in manifest: they should be unlinked
element1 = ApplicationElement()
element1.application_id = application.id
element1.object_type = 'foobar'
element1.object_id = '42'
element1.store()
element2 = ApplicationElement()
element2.application_id = application.id
element2.object_type = 'foobarblah'
element2.object_id = '35'
element2.store()
# run new import to check it doesn't duplicate objects
resp = get_app(pub).put(sign_uri('/api/export-import/bundle-import/'), bundle)
@ -705,10 +770,34 @@ def test_export_import_bundle_import(pub):
assert Workflow.count() == 1
assert MailTemplateCategory.count() == 1
assert MailTemplate.count() == 1
assert CommentTemplateCategory.count() == 1
assert CommentTemplate.count() == 1
assert DataSourceCategory.count() == 1
assert NamedDataSource.count() == 1
assert pub.custom_view_class().count() == 1
assert NamedWsCall.count() == 1
assert Application.count() == 1
assert ApplicationElement.count() == 15
assert (
ApplicationElement.select(
[
Equal('application_id', application.id),
Equal('object_type', element1.object_type),
Equal('object_id', element1.object_id),
]
)
== []
)
assert (
ApplicationElement.select(
[
Equal('application_id', application.id),
Equal('object_type', element2.object_type),
Equal('object_id', element2.object_id),
]
)
== []
)
# change immutable attributes and check they are not reset
formdef = FormDef.select()[0]
@ -756,3 +845,273 @@ def test_export_import_formdef_do_not_overwrite_table_name(pub):
formdef = FormDef.select()[0]
assert formdef.table_name == 'formdata_%s_test2' % formdef.id
assert formdef.data_class().count() == 1
def test_export_import_bundle_declare(pub):
workflow_category = WorkflowCategory(name='test')
workflow_category.store()
workflow = Workflow(name='test')
workflow.store()
block_category = BlockCategory(name='test')
block_category.store()
block = BlockDef(name='test')
block.store()
role = pub.role_class(name='test')
role.store()
category = Category(name='Test')
category.store()
formdef = FormDef()
formdef.name = 'Test'
formdef.store()
card_category = CardDefCategory(name='Test')
card_category.store()
carddef = CardDef()
carddef.name = 'Test'
carddef.store()
ds_category = DataSourceCategory(name='Test')
ds_category.store()
data_source = NamedDataSource(name='Test')
data_source.store()
mail_template_category = MailTemplateCategory(name='Test')
mail_template_category.store()
mail_template = MailTemplate(name='Test')
mail_template.store()
comment_template_category = CommentTemplateCategory(name='Test')
comment_template_category.store()
comment_template = CommentTemplate(name='Test')
comment_template.store()
wscall = NamedWsCall(name='Test')
wscall.store()
bundle = create_bundle(
[
{'type': 'forms-categories', 'slug': 'test', 'name': 'test'},
{'type': 'forms', 'slug': 'test', 'name': 'test'},
{'type': 'cards-categories', 'slug': 'test', 'name': 'test'},
{'type': 'cards', 'slug': 'test', 'name': 'test'},
{'type': 'blocks-categories', 'slug': 'test', 'name': 'test'},
{'type': 'blocks', 'slug': 'test', 'name': 'test'},
{'type': 'roles', 'slug': 'test', 'name': 'test'},
{'type': 'workflows-categories', 'slug': 'test', 'name': 'test'},
{'type': 'workflows', 'slug': 'test', 'name': 'test'},
{'type': 'mail-templates-categories', 'slug': 'test', 'name': 'test'},
{'type': 'mail-templates', 'slug': 'test', 'name': 'test'},
{'type': 'comment-templates-categories', 'slug': 'test', 'name': 'test'},
{'type': 'comment-templates', 'slug': 'test', 'name': 'test'},
{'type': 'data-sources-categories', 'slug': 'test', 'name': 'test'},
{'type': 'data-sources', 'slug': 'test', 'name': 'test'},
{'type': 'wscalls', 'slug': 'test', 'name': 'test'},
],
('forms-categories/test', category),
('forms/test', formdef),
('cards-categories/test', card_category),
('cards/test', carddef),
('blocks-categories/test', block_category),
('blocks/test', block),
('workflows-categories/test', workflow_category),
('workflows/test', workflow),
('data-sources-categories/test', ds_category),
('data-sources/test', data_source),
('mail-templates-categories/test', mail_template_category),
('mail-templates/test', mail_template),
('comment-templates-categories/test', comment_template_category),
('comment-templates/test', comment_template),
('roles/test', role),
('wscalls/test', wscall),
)
resp = get_app(pub).put(sign_uri('/api/export-import/bundle-declare/'), bundle)
afterjob_url = resp.json['url']
resp = get_app(pub).put(sign_uri(afterjob_url))
assert resp.json['data']['status'] == 'completed'
assert resp.json['data']['completion_status'] == '15/15 (100%)'
assert Application.count() == 1
application = Application.select()[0]
assert application.slug == 'test'
assert application.name == 'Test'
assert application.description == 'Foo Bar'
assert application.documentation_url == 'http://foo.bar'
assert application.version_number == '42.0'
assert application.version_notes == 'foo bar blah'
assert application.icon.base_filename == 'foo.png'
assert application.editable is True
assert ApplicationElement.count() == 15
# create some links to elements not present in manifest: they should be unlinked
element1 = ApplicationElement()
element1.application_id = application.id
element1.object_type = 'foobar'
element1.object_id = '42'
element1.store()
element2 = ApplicationElement()
element2.application_id = application.id
element2.object_type = 'foobarblah'
element2.object_id = '35'
element2.store()
# and remove an object to have an unkown reference in manifest
MailTemplate.wipe()
resp = get_app(pub).put(sign_uri('/api/export-import/bundle-declare/'), bundle)
afterjob_url = resp.json['url']
resp = get_app(pub).put(sign_uri(afterjob_url))
assert resp.json['data']['status'] == 'completed'
assert Application.count() == 1
assert ApplicationElement.count() == 14
assert (
ApplicationElement.select(
[
Equal('application_id', application.id),
Equal('object_type', element1.object_type),
Equal('object_id', element1.object_id),
]
)
== []
)
assert (
ApplicationElement.select(
[
Equal('application_id', application.id),
Equal('object_type', element2.object_type),
Equal('object_id', element2.object_id),
]
)
== []
)
assert (
ApplicationElement.select(
[
Equal('application_id', application.id),
Equal('object_type', MailTemplate.xml_root_node),
]
)
== []
)
def test_export_import_bundle_unlink(pub):
application = Application()
application.slug = 'test'
application.name = 'Test'
application.version_number = 'foo'
application.store()
other_application = Application()
other_application.slug = 'other-test'
other_application.name = 'Other Test'
other_application.version_number = 'foo'
other_application.store()
workflow_category = WorkflowCategory(name='test')
workflow_category.store()
ApplicationElement.update_or_create_for_object(application, workflow_category)
workflow = Workflow(name='test')
workflow.store()
ApplicationElement.update_or_create_for_object(application, workflow)
block_category = BlockCategory(name='test')
block_category.store()
ApplicationElement.update_or_create_for_object(application, block_category)
block = BlockDef(name='test')
block.store()
ApplicationElement.update_or_create_for_object(application, block)
category = Category(name='Test')
category.store()
ApplicationElement.update_or_create_for_object(application, category)
formdef = FormDef()
formdef.name = 'Test'
formdef.store()
ApplicationElement.update_or_create_for_object(application, formdef)
card_category = CardDefCategory(name='Test')
card_category.store()
ApplicationElement.update_or_create_for_object(application, card_category)
carddef = CardDef()
carddef.name = 'Test'
carddef.store()
ApplicationElement.update_or_create_for_object(application, carddef)
ds_category = DataSourceCategory(name='Test')
ds_category.store()
ApplicationElement.update_or_create_for_object(application, ds_category)
data_source = NamedDataSource(name='Test')
data_source.store()
ApplicationElement.update_or_create_for_object(application, data_source)
mail_template_category = MailTemplateCategory(name='Test')
mail_template_category.store()
ApplicationElement.update_or_create_for_object(application, mail_template_category)
mail_template = MailTemplate(name='Test')
mail_template.store()
ApplicationElement.update_or_create_for_object(application, mail_template)
comment_template_category = CommentTemplateCategory(name='Test')
comment_template_category.store()
ApplicationElement.update_or_create_for_object(application, comment_template_category)
comment_template = CommentTemplate(name='Test')
comment_template.store()
ApplicationElement.update_or_create_for_object(application, comment_template)
wscall = NamedWsCall(name='Test')
wscall.store()
ApplicationElement.update_or_create_for_object(application, wscall)
element = ApplicationElement()
element.application_id = application.id
element.object_type = 'foobar'
element.object_id = '42'
element.store()
other_element = ApplicationElement()
other_element.application_id = other_application.id
other_element.object_type = 'foobar'
other_element.object_id = '42'
other_element.store()
assert Application.count() == 2
assert ApplicationElement.count() == 17
get_app(pub).post(sign_uri('/api/export-import/unlink/'), {'application': 'test'})
assert Application.count() == 1
assert ApplicationElement.count() == 1
assert (
Application.count(
[
Equal('id', other_application.id),
]
)
== 1
)
assert (
ApplicationElement.count(
[
Equal('application_id', other_application.id),
]
)
== 1
)
# again
get_app(pub).post(sign_uri('/api/export-import/unlink/'), {'application': 'test'})
assert Application.count() == 1
assert ApplicationElement.count() == 1

File diff suppressed because it is too large Load Diff

View File

@ -12,6 +12,7 @@ from django.utils.encoding import force_bytes
from wcs import fields
from wcs.admin.settings import UserFieldsFormDef
from wcs.applications import Application
from wcs.blocks import BlockDef
from wcs.carddef import CardDef
from wcs.fields import DateField, ItemField, StringField
@ -500,6 +501,21 @@ def test_unused_file_removal_job(pub):
# 1 attachment
assert len(glob.glob(os.path.join(pub.app_dir, 'unused-files/attachments/*/*'))) == 1
application = Application()
application.name = 'App 1'
application.slug = 'app-1'
application.icon = PicklableUpload('icon.png', 'image/png')
application.icon.receive([b'foobar'])
application.version_number = '1'
application.store()
assert application.icon.qfilename in os.listdir(os.path.join(pub.app_dir, 'uploads'))
clean_unused_files(pub)
assert application.icon.qfilename in os.listdir(os.path.join(pub.app_dir, 'uploads'))
Application.remove_object(application.id)
clean_unused_files(pub)
assert application.icon.qfilename not in os.listdir(os.path.join(pub.app_dir, 'uploads'))
# unknown unused-files-behaviour: do nothing
pub.site_options.set('options', 'unused-files-behaviour', 'foo')
formdata = formdef.data_class()()

View File

@ -169,6 +169,8 @@ def create_temporary_pub(pickle_mode=False, lazy_mode=False):
TestDef.do_table()
TestResult.do_table()
sql.WorkflowTrace.do_table()
sql.Application.do_table()
sql.ApplicationElement.do_table()
sql.init_global_table()
Review

Dans wcs/publisher.py, il manque un appel aux .do_table() dans initialize_sql; (manque aussi TestDef et TestResult, on dirait, je vais faire un ticket).

Idéalement il n'y aurait pas à répéter ça, les tests appelleraient initialize_sql, je ne sais plus ce qui compliquait ça. (mais ça relève d'un autre ticket également, ici juste copier les deux lignes dans publisher.py sera très bien).

Dans wcs/publisher.py, il manque un appel aux .do_table() dans initialize_sql; (manque aussi TestDef et TestResult, on dirait, je vais faire un ticket). Idéalement il n'y aurait pas à répéter ça, les tests appelleraient initialize_sql, je ne sais plus ce qui compliquait ça. (mais ça relève d'un autre ticket également, ici juste copier les deux lignes dans publisher.py sera très bien).
Review

ajouté dans wcs/publisher.py

ajouté dans wcs/publisher.py
conn.close()

View File

@ -21,6 +21,7 @@ from quixote.html import TemplateIO, htmltext
from wcs.admin import utils
from wcs.admin.categories import BlockCategoriesDirectory, get_categories
from wcs.admin.fields import FieldDefPage, FieldsDirectory
from wcs.backoffice.applications import ApplicationsDirectory
from wcs.backoffice.snapshots import SnapshotsDirectory
from wcs.blocks import BlockDef, BlockdefImportError
from wcs.categories import BlockCategory
@ -262,13 +263,14 @@ class BlockDirectory(FieldsDirectory):
class BlocksDirectory(Directory):
_q_exports = ['', 'new', 'categories', ('import', 'p_import')]
_q_exports = ['', 'new', 'categories', ('import', 'p_import'), ('application', 'applications_dir')]
do_not_call_in_templates = True
categories = BlockCategoriesDirectory()
def __init__(self, section):
super().__init__()
self.section = section
self.applications_dir = ApplicationsDirectory(BlockDef.xml_root_node)
def _q_traverse(self, path):
if not get_publisher().get_backoffice_root().is_global_accessible('forms'):
@ -284,21 +286,35 @@ class BlocksDirectory(Directory):
return BlockDirectory(self.section, block)
def _q_index(self):
from wcs.applications import Application
html_top(self.section, title=_('Fields Blocks'))
get_response().add_javascript(['popup.js'])
context = {
'view': self,
'applications': Application.select_for_object_type(BlockDef.xml_root_node),
'has_sidebar': True,
}
blocks = BlockDef.select(order_by='name')
Application.populate_objects(blocks)
context.update(self.get_list_context(blocks))
return template.QommonTemplateResponse(
templates=['wcs/backoffice/blocks.html'],
context=context,
is_django_native=True,
)
def get_list_context(self, blocks):
categories = BlockCategory.select()
BlockCategory.sort_by_position(categories)
blocks = BlockDef.select(order_by='name')
if categories:
categories.append(BlockCategory(_('Misc')))
for category in categories:
category.blocks = [x for x in blocks if x.category_id == category.id]
return template.QommonTemplateResponse(
templates=['wcs/backoffice/blocks.html'],
context={'view': self, 'blocks': blocks, 'categories': categories, 'has_sidebar': True},
is_django_native=True,
)
return {
'blocks': blocks,
'categories': categories,
}
def new(self):
form = Form(enctype='multipart/form-data')

View File

@ -19,6 +19,7 @@ from quixote.directory import Directory
from quixote.html import TemplateIO, htmltext
from wcs.admin import utils
from wcs.backoffice.applications import ApplicationsDirectory
from wcs.backoffice.snapshots import SnapshotsDirectory
from wcs.blocks import BlockDef
from wcs.carddef import CardDef, get_cards_graph
@ -358,7 +359,7 @@ class DataSourceCategoryPage(CategoryPage):
class CategoriesDirectory(Directory):
_q_exports = ['', 'new', 'update_order']
_q_exports = ['', 'new', 'update_order', ('application', 'applications_dir')]
base_section = 'forms'
category_class = Category
@ -368,16 +369,24 @@ class CategoriesDirectory(Directory):
do_not_call_in_templates = True
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.applications_dir = ApplicationsDirectory(self.category_class.xml_root_node)
def _q_index(self):
from wcs.applications import Application
get_response().add_javascript(['biglist.js', 'qommon.wysiwyg.js', 'popup.js'])
html_top('categories', title=_('Categories'))
categories = self.category_class.select()
self.category_class.sort_by_position(categories)
Application.populate_objects(categories)
return template.QommonTemplateResponse(
templates=['wcs/backoffice/categories.html'],
context={
'view': self,
'categories': categories,
'applications': Application.select_for_object_type(self.category_class.xml_root_node),
'has_sidebar': True,
},
is_django_native=True,

View File

@ -20,6 +20,7 @@ from quixote.html import TemplateIO, htmltext
from wcs.admin import utils
from wcs.admin.categories import CommentTemplateCategoriesDirectory, get_categories
from wcs.backoffice.applications import ApplicationsDirectory
from wcs.backoffice.snapshots import SnapshotsDirectory
from wcs.categories import CommentTemplateCategory
from wcs.comment_templates import CommentTemplate
@ -40,10 +41,14 @@ from wcs.qommon.form import (
class CommentTemplatesDirectory(Directory):
_q_exports = ['', 'new', 'categories', ('import', 'p_import')]
_q_exports = ['', 'new', 'categories', ('import', 'p_import'), ('application', 'applications_dir')]
do_not_call_in_templates = True
categories = CommentTemplateCategoriesDirectory()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.applications_dir = ApplicationsDirectory(CommentTemplate.xml_root_node)
def _q_traverse(self, path):
if not get_publisher().get_backoffice_root().is_global_accessible('workflows'):
raise errors.AccessForbiddenError()
@ -54,25 +59,35 @@ class CommentTemplatesDirectory(Directory):
return CommentTemplatePage(component)
def _q_index(self):
from wcs.applications import Application
html_top('comment_templates', title=_('Comment Templates'))
get_response().add_javascript(['popup.js'])
comment_templates = CommentTemplate.select(order_by='name')
Application.populate_objects(comment_templates)
context = {
'view': self,
'applications': Application.select_for_object_type(CommentTemplate.xml_root_node),
'has_sidebar': True,
}
context.update(self.get_list_context(comment_templates))
return template.QommonTemplateResponse(
templates=['wcs/backoffice/comment-templates.html'],
context=context,
is_django_native=True,
)
def get_list_context(self, comment_templates):
categories = CommentTemplateCategory.select()
CommentTemplateCategory.sort_by_position(categories)
comment_templates = CommentTemplate.select(order_by='name')
if categories:
categories.append(CommentTemplateCategory(_('Misc')))
for category in categories:
category.comment_templates = [x for x in comment_templates if x.category_id == category.id]
return template.QommonTemplateResponse(
templates=['wcs/backoffice/comment-templates.html'],
context={
'view': self,
'comment_templates': comment_templates,
'categories': categories,
'has_sidebar': True,
},
is_django_native=True,
)
return {
'comment_templates': comment_templates,
'categories': categories,
}
def new(self):
form = Form(enctype='multipart/form-data')

View File

@ -20,6 +20,7 @@ from quixote.html import TemplateIO, htmltext
from wcs.admin import utils
from wcs.admin.categories import DataSourceCategoriesDirectory, get_categories
from wcs.backoffice.applications import ApplicationsDirectory
from wcs.backoffice.snapshots import SnapshotsDirectory
from wcs.carddef import CardDef
from wcs.categories import DataSourceCategory
@ -498,9 +499,14 @@ class NamedDataSourcesDirectory(Directory):
'categories',
('import', 'p_import'),
('sync-agendas', 'sync_agendas'),
('application', 'applications_dir'),
]
categories = DataSourceCategoriesDirectory()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.applications_dir = ApplicationsDirectory(NamedDataSource.xml_root_node)
def _q_traverse(self, path):
if (
not get_publisher().get_backoffice_root().is_global_accessible('forms')
@ -512,12 +518,33 @@ class NamedDataSourcesDirectory(Directory):
return super()._q_traverse(path)
def _q_index(self):
from wcs.applications import Application
html_top('datasources', title=_('Data Sources'))
get_response().add_javascript(['popup.js'])
context = {
'view': self,
'has_chrono': has_chrono(get_publisher()),
'has_users': True,
'applications': Application.select_for_object_type(NamedDataSource.xml_root_node),
'has_sidebar': True,
}
data_sources = NamedDataSource.select(order_by='name')
Application.populate_objects(data_sources)
context.update(self.get_list_context(data_sources))
return template.QommonTemplateResponse(
templates=['wcs/backoffice/data-sources.html'],
context=context,
is_django_native=True,
)
def get_list_context(self, objects, application=None):
from wcs.applications import Application
data_sources = []
user_data_sources = []
agenda_data_sources = []
for ds in NamedDataSource.select(order_by='name'):
for ds in objects:
if ds.external == 'agenda':
agenda_data_sources.append(ds)
elif ds.type == 'wcs:users':
@ -532,20 +559,18 @@ class NamedDataSourcesDirectory(Directory):
category.data_sources = [x for x in data_sources if x.category_id == category.id]
generated_data_sources = list(CardDef.get_carddefs_as_data_source())
generated_data_sources.sort(key=lambda x: misc.simplify(x[1]))
return template.QommonTemplateResponse(
templates=['wcs/backoffice/data-sources.html'],
context={
'data_sources': data_sources,
'categories': categories,
'user_data_sources': user_data_sources,
'has_chrono': has_chrono(get_publisher()),
'has_users': True,
'agenda_data_sources': agenda_data_sources,
'generated_data_sources': generated_data_sources,
'has_sidebar': True,
},
is_django_native=True,
)
if application:
carddefs = application.get_objects_for_object_type(CardDef.xml_root_node, lightweight=True)
generated_data_sources = [g for g in generated_data_sources if g[0] in carddefs]
else:
Application.populate_objects([g[0] for g in generated_data_sources])
return {
'data_sources': data_sources,
'categories': categories,
'user_data_sources': user_data_sources,
'agenda_data_sources': agenda_data_sources,
'generated_data_sources': generated_data_sources,
}
def _new(self, url, breadcrumb, title, ds_type=None):
get_response().breadcrumb.append((url, breadcrumb))

View File

@ -23,6 +23,7 @@ from quixote import get_publisher, get_request, get_response, get_session, redir
from quixote.directory import AccessControlled, Directory
from quixote.html import TemplateIO, htmlescape, htmltext
from wcs.backoffice.applications import ApplicationsDirectory
from wcs.backoffice.snapshots import SnapshotsDirectory
from wcs.carddef import CardDef
from wcs.categories import Category
@ -1712,7 +1713,15 @@ class NamedDataSourcesDirectoryInForms(NamedDataSourcesDirectory):
class FormsDirectory(AccessControlled, Directory):
_q_exports = ['', 'new', ('import', 'p_import'), 'blocks', 'categories', ('data-sources', 'data_sources')]
_q_exports = [
'',
'new',
('import', 'p_import'),
'blocks',
'categories',
('data-sources', 'data_sources'),
('application', 'applications_dir'),
]
category_class = Category
categories = CategoriesDirectory()
@ -1736,6 +1745,10 @@ class FormsDirectory(AccessControlled, Directory):
'Do note it is disabled by default.'
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.applications_dir = ApplicationsDirectory(self.formdef_class.xml_root_node)
def html_top(self, title):
return html_top(self.section, title)
@ -1757,16 +1770,33 @@ class FormsDirectory(AccessControlled, Directory):
return False
def _q_index(self):
from wcs.applications import Application
self.html_top(title=self.top_title)
get_response().add_javascript(['widget_list.js', 'select2.js', 'popup.js'])
context = {
'view': self,
'has_roles': bool(get_publisher().role_class.count()),
'applications': Application.select_for_object_type(self.formdef_class.xml_root_node),
'has_sidebar': True,
}
formdefs = self.formdef_class.select(order_by='name', ignore_errors=True, lightweight=True)
Application.populate_objects(formdefs)
context.update(self.get_list_context(formdefs))
context.update(self.get_extra_index_context_data())
return template.QommonTemplateResponse(
templates=[self.index_template_name], context=context, is_django_native=True
)
def get_list_context(self, formdefs):
global_access = is_global_accessible(self.section)
categories = self.category_class.select()
self.category_class.sort_by_position(categories)
categories.append(self.category_class(_('Misc')))
formdefs = self.formdef_class.select(order_by='name', ignore_errors=True, lightweight=True)
has_form_with_category_set = False
for category in categories:
if not global_access:
@ -1786,18 +1816,10 @@ class FormsDirectory(AccessControlled, Directory):
# no form with a category set, do not display "Misc" title
categories[-1].name = None
context = {
'view': self,
return {
'objects': formdefs,
'categories': categories,
'has_roles': bool(get_publisher().role_class.count()),
'has_sidebar': True,
}
context.update(self.get_extra_index_context_data())
return template.QommonTemplateResponse(
templates=[self.index_template_name], context=context, is_django_native=True
)
def get_extra_index_context_data(self):
return {

View File

@ -20,6 +20,7 @@ from quixote.html import TemplateIO, htmltext
from wcs.admin import utils
from wcs.admin.categories import MailTemplateCategoriesDirectory, get_categories
from wcs.backoffice.applications import ApplicationsDirectory
from wcs.backoffice.snapshots import SnapshotsDirectory
from wcs.categories import MailTemplateCategory
from wcs.mail_templates import MailTemplate
@ -40,10 +41,14 @@ from wcs.qommon.form import (
class MailTemplatesDirectory(Directory):
_q_exports = ['', 'new', 'categories', ('import', 'p_import')]
_q_exports = ['', 'new', 'categories', ('import', 'p_import'), ('application', 'applications_dir')]
do_not_call_in_templates = True
categories = MailTemplateCategoriesDirectory()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.applications_dir = ApplicationsDirectory(MailTemplate.xml_root_node)
def _q_traverse(self, path):
if not get_publisher().get_backoffice_root().is_global_accessible('workflows'):
raise errors.AccessForbiddenError()
@ -54,25 +59,35 @@ class MailTemplatesDirectory(Directory):
return MailTemplatePage(component)
def _q_index(self):
from wcs.applications import Application
html_top('mail_templates', title=_('Mail Templates'))
get_response().add_javascript(['popup.js'])
mail_templates = MailTemplate.select(order_by='name')
Application.populate_objects(mail_templates)
context = {
'view': self,
'applications': Application.select_for_object_type(MailTemplate.xml_root_node),
'has_sidebar': True,
}
context.update(self.get_list_context(mail_templates))
return template.QommonTemplateResponse(
templates=['wcs/backoffice/mail-templates.html'],
context=context,
is_django_native=True,
)
def get_list_context(self, mail_templates):
categories = MailTemplateCategory.select()
MailTemplateCategory.sort_by_position(categories)
mail_templates = MailTemplate.select(order_by='name')
if categories:
categories.append(MailTemplateCategory(_('Misc')))
for category in categories:
category.mail_templates = [x for x in mail_templates if x.category_id == category.id]
return template.QommonTemplateResponse(
templates=['wcs/backoffice/mail-templates.html'],
context={
'view': self,
'mail_templates': mail_templates,
'categories': categories,
'has_sidebar': True,
},
is_django_native=True,
)
return {
'mail_templates': mail_templates,
'categories': categories,
}
def new(self):
form = Form(enctype='multipart/form-data')

View File

@ -28,6 +28,7 @@ from quixote.directory import Directory
from quixote.html import TemplateIO, htmltext
from wcs.admin.categories import WorkflowCategoriesDirectory, get_categories
from wcs.backoffice.applications import ApplicationsDirectory
from wcs.backoffice.snapshots import SnapshotsDirectory
from wcs.carddef import CardDef
from wcs.categories import WorkflowCategory
@ -1954,6 +1955,7 @@ class WorkflowsDirectory(Directory):
('data-sources', 'data_sources'),
('mail-templates', 'mail_templates'),
('comment-templates', 'comment_templates'),
('application', 'applications_dir'),
]
data_sources = NamedDataSourcesDirectoryInWorkflows()
@ -1962,6 +1964,10 @@ class WorkflowsDirectory(Directory):
category_class = WorkflowCategory
categories = WorkflowCategoriesDirectory()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.applications_dir = ApplicationsDirectory(Workflow.xml_root_node)
def html_top(self, title):
return html_top('workflows', title)
@ -1983,9 +1989,26 @@ class WorkflowsDirectory(Directory):
return False
def _q_index(self):
from wcs.applications import Application
self.html_top(title=_('Workflows'))
get_response().add_javascript(['popup.js'])
context = {
'view': self,
'is_global_accessible': is_global_accessible(),
'applications': Application.select_for_object_type(Workflow.xml_root_node),
'has_sidebar': True,
}
workflows = Workflow.select(order_by='name')
Application.populate_objects(workflows)
context.update(self.get_list_context(workflows))
return template.QommonTemplateResponse(
templates=['wcs/backoffice/workflows.html'], context=context, is_django_native=True
)
def get_list_context(self, workflow_qs, application=False):
formdef_workflows = [Workflow.get_default_workflow()]
workflows_in_formdef_use = set(formdef_workflows[0].id)
for formdef in FormDef.select(lightweight=True):
@ -1998,9 +2021,12 @@ class WorkflowsDirectory(Directory):
shared_workflows = []
unused_workflows = []
workflows = formdef_workflows + carddef_workflows
if application:
workflows = []
else:
workflows = formdef_workflows + carddef_workflows
for workflow in Workflow.select(order_by='name'):
for workflow in workflow_qs:
if str(workflow.id) in workflows_in_formdef_use and str(workflow.id) in workflows_in_carddef_use:
shared_workflows.append(workflow)
elif str(workflow.id) in workflows_in_formdef_use:
@ -2066,15 +2092,9 @@ class WorkflowsDirectory(Directory):
x for x in workflows + unused_workflows if x.category_id == str(category.id)
]
context = {
return {
'categories': categories,
'view': self,
'has_sidebar': True,
'is_global_accessible': is_global_accessible(),
}
return template.QommonTemplateResponse(
templates=['wcs/backoffice/workflows.html'], context=context, is_django_native=True
)
def new(self):
get_response().breadcrumb.append(('new', _('New')))

View File

@ -19,6 +19,7 @@ from quixote.directory import Directory
from quixote.html import TemplateIO, htmltext
from wcs.admin import utils
from wcs.backoffice.applications import ApplicationsDirectory
from wcs.backoffice.snapshots import SnapshotsDirectory
from wcs.qommon import _, errors, misc, template
from wcs.qommon.backoffice.menu import html_top
@ -181,18 +182,31 @@ class NamedWsCallPage(Directory):
class NamedWsCallsDirectory(Directory):
_q_exports = ['', 'new', ('import', 'p_import')]
_q_exports = ['', 'new', ('import', 'p_import'), ('application', 'applications_dir')]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.applications_dir = ApplicationsDirectory(NamedWsCall.xml_root_node)
def _q_traverse(self, path):
get_response().breadcrumb.append(('wscalls/', _('Webservice Calls')))
return super()._q_traverse(path)
def _q_index(self):
from wcs.applications import Application
html_top('wscalls', title=_('Webservice Calls'))
get_response().add_javascript(['popup.js'])
wscalls = NamedWsCall.select(order_by='name')
Application.populate_objects(wscalls)
return template.QommonTemplateResponse(
templates=['wcs/backoffice/wscalls.html'],
context={'view': self, 'wscalls': NamedWsCall.select(order_by='name'), 'has_sidebar': True},
context={
'view': self,
'wscalls': wscalls,
'applications': Application.select_for_object_type(NamedWsCall.xml_root_node),
'has_sidebar': True,
},
is_django_native=True,
)

View File

@ -24,6 +24,7 @@ from django.shortcuts import redirect
from django.urls import reverse
from wcs.api_utils import is_url_signed
from wcs.applications import Application, ApplicationElement
from wcs.blocks import BlockDef
from wcs.carddef import CardDef
from wcs.categories import (
@ -39,7 +40,7 @@ from wcs.comment_templates import CommentTemplate
from wcs.data_sources import NamedDataSource, StubNamedDataSource
from wcs.formdef import FormDef
from wcs.mail_templates import MailTemplate
from wcs.sql import Role
from wcs.sql import Equal, Role
from wcs.workflows import Workflow
from wcs.wscalls import NamedWsCall
@ -247,7 +248,7 @@ class BundleImportJob(AfterJob):
tar_io = io.BytesIO(self.tar_content)
with tarfile.open(fileobj=tar_io) as self.tar:
manifest = json.loads(self.tar.extractfile('manifest.json').read().decode())
self.app_name = manifest.get('application')
self.application = Application.update_or_create_from_manifest(manifest, self.tar)
# count number of actions
self.total_count = 0
@ -260,6 +261,9 @@ class BundleImportJob(AfterJob):
)
self.total_count += len([x for x in manifest.get('elements') if x.get('type') in object_types])
# init cache of application elements, from imported manifest
self.application_elements = set()
# first pass on formdef/carddef/blockdef/workflows to create them empty
# (name and slug); so they can be found for sure in import pass
for type in ('forms', 'cards', 'blocks', 'workflows'):
@ -269,6 +273,9 @@ class BundleImportJob(AfterJob):
for type in object_types:
self.install([x for x in manifest.get('elements') if x.get('type') == type])
# remove obsolete application elements
self.unlink_obsolete_objects()
def pre_install(self, elements):
for element in elements:
element_klass = klasses[element['type']]
@ -289,7 +296,8 @@ class BundleImportJob(AfterJob):
new_object = element_klass()
new_object.slug = slug
new_object.name = '[pre-import] %s' % xml_node_text(tree.find('name'))
new_object.store(comment=_('Application (%s)') % self.app_name)
new_object.store(comment=_('Application (%s)') % self.application.name)
self.link_object(new_object)
self.increment_count()
def install(self, elements):
@ -304,7 +312,8 @@ class BundleImportJob(AfterJob):
if existing_object is None or not hasattr(existing_object, 'id'):
raise KeyError()
except KeyError:
new_object.store(comment=_('Application (%s)') % self.app_name)
new_object.store(comment=_('Application (%s)') % self.application.name)
self.link_object(new_object)
self.increment_count()
continue
# replace
@ -327,9 +336,20 @@ class BundleImportJob(AfterJob):
'disabled',
):
setattr(new_object, attr, getattr(existing_object, attr))
new_object.store(comment=_('Application (%s) update') % self.app_name)
new_object.store(comment=_('Application (%s) update') % self.application.name)
self.link_object(new_object)
self.increment_count()
def link_object(self, obj):
element = ApplicationElement.update_or_create_for_object(self.application, obj)
self.application_elements.add((element.object_type, element.object_id))
def unlink_obsolete_objects(self):
known_elements = ApplicationElement.select([Equal('application_id', self.application.id)])
for element in known_elements:
if (element.object_type, element.object_id) not in self.application_elements:
ApplicationElement.remove_object(element.id)
@signature_required
def bundle_import(request):
@ -337,3 +357,57 @@ def bundle_import(request):
job.store()
job.run(spool=True)
return JsonResponse({'err': 0, 'url': job.get_api_status_url()})
class BundleDeclareJob(BundleImportJob):
def execute(self):
object_types = [x for x in klasses if x != 'roles']
tar_io = io.BytesIO(self.tar_content)
with tarfile.open(fileobj=tar_io) as self.tar:
manifest = json.loads(self.tar.extractfile('manifest.json').read().decode())
self.application = Application.update_or_create_from_manifest(manifest, self.tar, editable=True)
# count number of actions
self.total_count = len([x for x in manifest.get('elements') if x.get('type') in object_types])
# init cache of application elements, from manifest
self.application_elements = set()
# declare elements
for type in object_types:
self.declare([x for x in manifest.get('elements') if x.get('type') == type])
# remove obsolete application elements
self.unlink_obsolete_objects()
def declare(self, elements):
for element in elements:
element_klass = klasses[element['type']]
element_slug = element['slug']
existing_object = element_klass.get_by_slug(element_slug, ignore_errors=True)
if existing_object:
self.link_object(existing_object)
self.increment_count()
@signature_required
def bundle_declare(request):
job = BundleDeclareJob(tar_content=request.body)
job.store()
job.run(spool=True)
return JsonResponse({'err': 0, 'url': job.get_api_status_url()})
@signature_required
def unlink(request):
if request.method == 'POST' and request.POST.get('application'):
applications = Application.select([Equal('slug', request.POST['application'])])
if applications:
application = applications[0]
elements = ApplicationElement.select([Equal('application_id', application.id)])
for element in elements:
ApplicationElement.remove_object(element.id)
Application.remove_object(application.id)
return JsonResponse({'err': 0})

147
wcs/applications.py Normal file
View File

@ -0,0 +1,147 @@
# w.c.s. - web application for online forms
# Copyright (C) 2005-2023 Entr'ouvert
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, see <http://www.gnu.org/licenses/>.
import collections
import mimetypes
from quixote import get_publisher
from wcs import sql
from wcs.qommon.upload_storage import PicklableUpload
class Application(sql.Application):
id = None
slug = None
name = None
description = None
documentation_url = None
icon = None
version_number = None
version_notes = None
editable = False
visible = True
created_at = None
updated_at = None
@classmethod
def get_by_slug(cls, slug, ignore_errors=True):
objects = cls.select([sql.Equal('slug', slug)])
if objects:
return objects[0]
if ignore_errors:
return None
raise KeyError(slug)
@classmethod
def update_or_create_from_manifest(cls, manifest, tar, editable=False):
application = cls.get_by_slug(manifest.get('slug'), ignore_errors=True)
if application is None:
application = cls()
application.slug = manifest.get('slug')
application.name = manifest.get('application')
application.description = manifest.get('description')
application.documentation_url = manifest.get('documentation_url')
if manifest.get('icon'):
application.icon = PicklableUpload(manifest['icon'], mimetypes.guess_type(manifest['icon'])[0])
application.icon.receive([tar.extractfile(manifest['icon']).read()])
else:
application.icon = None
application.version_number = manifest.get('version_number') or 'unknown'
application.version_notes = manifest.get('version_notes')
application.editable = editable
application.visible = True
application.store()
return application
@classmethod
def select_for_object_type(cls, object_type):
elements = ApplicationElement.select([sql.Equal('object_type', object_type)])
application_ids = [e.application_id for e in elements]
return cls.get_ids(application_ids, ignore_errors=True, order_by='name')
@classmethod
def populate_objects(cls, objects):
object_types = {o.xml_root_node for o in objects}
elements = ApplicationElement.select([sql.Contains('object_type', object_types)])
elements_by_objects = collections.defaultdict(list)
for element in elements:
elements_by_objects[(element.object_type, element.object_id)].append(element)
application_ids = [e.application_id for e in elements]
applications_by_ids = {a.id: a for a in cls.get_ids(application_ids, ignore_errors=True)}
for obj in objects:
applications = []
elements = elements_by_objects.get((obj.xml_root_node, obj.id)) or []
for element in elements:
application = applications_by_ids.get(element.application_id)
applications.append(application)
obj._applications = sorted(applications, key=lambda a: a.name)
@classmethod
def load_for_object(cls, obj):
elements = ApplicationElement.select(
[sql.Equal('object_type', obj.xml_root_node), sql.Equal('object_id', obj.id)]
)
application_ids = [e.application_id for e in elements]
applications_by_ids = {a.id: a for a in cls.get_ids(application_ids, ignore_errors=True)}
applications = []
for element in elements:
application = applications_by_ids.get(element.application_id)
applications.append(application)
obj._applications = sorted(applications, key=lambda a: a.name)
def get_objects_for_object_type(self, object_type, lightweight=True):
elements = ApplicationElement.select(
[sql.Equal('application_id', self.id), sql.Equal('object_type', object_type)]
)
object_ids = [e.object_id for e in elements]
select_kwargs = {}
if object_type == 'formdef':
select_kwargs['lightweight'] = lightweight
return (
get_publisher()
.get_object_class(object_type)
.get_ids(object_ids, ignore_errors=True, order_by='name', **select_kwargs)
)
class ApplicationElement(sql.ApplicationElement):
id = None
application_id = None
object_type = None
object_id = None
created_at = None
updated_at = None
@classmethod
def update_or_create_for_object(cls, application, obj):
elements = cls.select(
[
sql.Equal('application_id', application.id),
sql.Equal('object_type', obj.xml_root_node),
sql.Equal('object_id', obj.id),
]
)
if elements:
element = elements[0]
element.store()
return element
element = cls()
element.application_id = application.id
element.object_type = obj.xml_root_node
element.object_id = obj.id
element.store()
return element

View File

@ -0,0 +1,140 @@
# w.c.s. - web application for online forms
# Copyright (C) 2005-2023 Entr'ouvert
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, see <http://www.gnu.org/licenses/>.
from quixote import get_response, redirect
from quixote.directory import Directory
from wcs.qommon import errors, misc, template
from wcs.qommon.backoffice.menu import html_top
class ApplicationsDirectory(Directory):

On pourrait avoir un _q_index qui ferait juste redirect('..'), pour ne pas donner une 404 sur un chemin partiel.

On pourrait avoir un `_q_index` qui ferait juste redirect('..'), pour ne pas donner une 404 sur un chemin partiel.

fait

fait
_q_exports = ['']
def __init__(self, object_type):
self.object_type = object_type
def _q_index(self):
return redirect('..')
def _q_lookup(self, component):
from wcs.applications import Application
application = Application.get_by_slug(component, ignore_errors=True)
if not application:
raise errors.TraversalError()
return ApplicationDirectory(self.object_type, application)
class ApplicationDirectory(Directory):
_q_exports = ['', 'icon', 'logo']
formdef_objects_template = 'wcs/backoffice/application_formdefs.html'
carddef_objects_template = 'wcs/backoffice/application_formdefs.html'
workflow_objects_template = 'wcs/backoffice/application_workflows.html'
block_objects_template = 'wcs/backoffice/application_blocks.html'
mailtemplate_objects_template = 'wcs/backoffice/application_mailtemplates.html'
commenttemplate_objects_template = 'wcs/backoffice/application_commenttemplates.html'
datasource_objects_template = 'wcs/backoffice/application_datasources.html'
wscall_objects_template = 'wcs/backoffice/application_wscalls.html'
def __init__(self, object_type, application):

Ça devrait plutôt être application/%s/

Actuellement de https://.../backoffice/forms/application/1/ c'est un lien /backoffice/forms/1/ qui est ajouté à l'href. (pour le moment comme c'est toujours le dernier élément du fil d'ariane et qu'il n'apparait pas, on ne risque pas de clic sur le mauvais lien, mais autant éviter la surprise si jamais des sous-pages sont ajoutées).

Ça devrait plutôt être application/%s/ Actuellement de https://.../backoffice/forms/application/1/ c'est un lien /backoffice/forms/1/ qui est ajouté à l'href. (pour le moment comme c'est toujours le dernier élément du fil d'ariane et qu'il n'apparait pas, on ne risque pas de clic sur le mauvais lien, mais autant éviter la surprise si jamais des sous-pages sont ajoutées).

fait

fait
self.object_type = object_type
self.application = application
def _q_traverse(self, path):
get_response().breadcrumb.append(('application/%s/' % self.application.slug, self.application.name))
return super()._q_traverse(path)
def _q_index(self):
html_top('', self.application.name)
return template.QommonTemplateResponse(templates=[self.get_template()], context=self.get_context())
def get_template(self):
if hasattr(self, '%s_objects_template' % self.object_type.replace('-', '')):
return getattr(self, '%s_objects_template' % self.object_type.replace('-', ''))
return 'wcs/backoffice/application_objects.html'
def get_context(self):
context = {
'application': self.application,
}
objects = self.application.get_objects_for_object_type(self.object_type)
if hasattr(self, 'get_%s_objects_context' % self.object_type.replace('-', '')):
context.update(
getattr(self, 'get_%s_objects_context' % self.object_type.replace('-', ''))(objects)
)
else:
context['objects'] = objects
return context
def get_formdef_objects_context(self, objects):
from wcs.admin.forms import FormsDirectory
return FormsDirectory().get_list_context(objects)
def get_carddef_objects_context(self, objects):
from wcs.backoffice.cards import CardsDirectory
return CardsDirectory().get_list_context(objects)
def get_workflow_objects_context(self, objects):
from wcs.admin.workflows import WorkflowsDirectory
return WorkflowsDirectory().get_list_context(objects, application=True)
def get_block_objects_context(self, objects):
from wcs.admin.blocks import BlocksDirectory
return BlocksDirectory(None).get_list_context(objects)
def get_mailtemplate_objects_context(self, objects):
from wcs.admin.mail_templates import MailTemplatesDirectory
return MailTemplatesDirectory().get_list_context(objects)
def get_commenttemplate_objects_context(self, objects):
from wcs.admin.comment_templates import CommentTemplatesDirectory
return CommentTemplatesDirectory().get_list_context(objects)
def get_datasource_objects_context(self, objects):
from wcs.admin.data_sources import NamedDataSourcesDirectory
return NamedDataSourcesDirectory().get_list_context(objects, self.application)
def icon(self):
return self._icon(size=(16, 16))
def logo(self):
return self._icon(size=(64, 64))
def _icon(self, size):
response = get_response()
if self.application.icon and self.application.icon.can_thumbnail():
try:
content = misc.get_thumbnail(
self.application.icon.get_fs_filename(),
content_type=self.application.icon.content_type,
size=size,
)
response.set_content_type('image/png')
return content
except misc.ThumbnailError:
raise errors.TraversalError()
else:
raise errors.TraversalError()

View File

@ -237,7 +237,7 @@ class CardDefPage(FormDefPage):
class CardsDirectory(FormsDirectory):
_q_exports = ['', 'new', ('import', 'p_import'), 'categories', 'svg']
_q_exports = ['', 'new', ('import', 'p_import'), 'categories', 'svg', ('application', 'applications_dir')]
category_class = CardDefCategory
categories = CardDefCategoriesDirectory()

View File

@ -1883,6 +1883,7 @@ class FormDef(StorableObject):
return odict
def __setstate__(self, dict):
super().__setstate__(dict)
self.__dict__ = dict
self._workflow = None
self._start_page = None
@ -2077,6 +2078,7 @@ def clean_unused_files(publisher, **kwargs):
known_filenames.update([x for x in glob.glob(os.path.join(publisher.app_dir, 'attachments/*/*'))])
def accumulate_filenames():
from wcs.applications import Application
from wcs.carddef import CardDef
for formdef in FormDef.select(ignore_migration=True) + CardDef.select(ignore_migration=True):
@ -2094,6 +2096,10 @@ def clean_unused_files(publisher, **kwargs):
if is_upload(field_data):
yield field_data.get_fs_filename()
for application in Application.select():
if is_upload(application.icon):
yield application.icon.get_fs_filename()
used_filenames = set()
for filename in accumulate_filenames():
if not filename: # alternative storage

View File

@ -406,6 +406,8 @@ class WcsPublisher(QommonPublisher):
sql.Audit.do_table()
sql.TestDef.do_table()
sql.TestResult.do_table()
sql.Application.do_table()
sql.ApplicationElement.do_table()
sql.do_meta_table()
from .carddef import CardDef
from .formdef import FormDef
@ -495,7 +497,15 @@ class WcsPublisher(QommonPublisher):
def get_object_class(self, object_type):
from wcs.blocks import BlockDef
from wcs.carddef import CardDef
from wcs.categories import BlockCategory, CardDefCategory, Category, WorkflowCategory
from wcs.categories import (
BlockCategory,
CardDefCategory,
Category,
CommentTemplateCategory,
DataSourceCategory,
MailTemplateCategory,
WorkflowCategory,
)
from wcs.comment_templates import CommentTemplate
from wcs.data_sources import NamedDataSource
from wcs.formdef import FormDef
@ -516,6 +526,9 @@ class WcsPublisher(QommonPublisher):
CardDefCategory,
WorkflowCategory,
BlockCategory,
MailTemplateCategory,
CommentTemplateCategory,
DataSourceCategory,
):
if klass.xml_root_node == object_type:
return klass

View File

@ -701,7 +701,7 @@ def can_thumbnail(content_type):
return False
def get_thumbnail(filepath, content_type=None):
def get_thumbnail(filepath, content_type=None, size=None):
if not filepath or not can_thumbnail(content_type or ''):
raise ThumbnailError()
@ -711,11 +711,16 @@ def get_thumbnail(filepath, content_type=None):
os.mkdir(thumbs_dir)
except FileExistsError:
pass
thumb_filepath = os.path.join(thumbs_dir, hashlib.sha256(force_bytes(filepath)).hexdigest())
thumb_filepath = force_bytes(filepath)
if size:
thumb_filepath = '%s-%s-%s' % (thumb_filepath, *size)
thumb_filepath = os.path.join(thumbs_dir, hashlib.sha256(force_bytes(thumb_filepath)).hexdigest())
if os.path.exists(thumb_filepath):
with open(thumb_filepath, 'rb') as f:
return f.read()
size = size or (500, 300)
# generate thumbnail
if content_type == 'application/pdf':
try:
@ -761,7 +766,7 @@ def get_thumbnail(filepath, content_type=None):
image = image.rotate(90, expand=1)
try:
image.thumbnail((500, 300))
image.thumbnail(size)
except (ValueError, SyntaxError):
# PIL can raise syntax error on broken PNG files
# * File "PIL/PngImagePlugin.py", line 119, in read

View File

@ -2635,3 +2635,7 @@ span.test-failure::before {
font-family: FontAwesome;
content: "\f00d"; /* times */
}
.application-logo, .application-icon {
vertical-align: middle;
}

View File

@ -16,6 +16,7 @@
import _thread
import builtins
import copy
import copyreg
import errno
import operator
@ -451,6 +452,17 @@ class StorableObject:
def __init__(self, id=None):
self.id = id
def __getstate__(self):
odict = copy.copy(self.__dict__)
if '_applications' in odict:
del odict['_applications']
return odict
def __setstate__(self, ndict):
self.__dict__ = ndict
if hasattr(self, '_applications'):
delattr(self, '_applications')
def is_readonly(self):
return getattr(self, 'readonly', False)
@ -1084,6 +1096,15 @@ class StorableObject:
return None, None
return snapshots[0].timestamp, snapshots[0].user_id
def get_applications(self):
from wcs.applications import Application
if getattr(self, '_applications', None) is None:
Application.load_for_object(self)
return self._applications
applications = property(get_applications)
@classonlymethod
def wipe(cls):
tmpdir = tempfile.mkdtemp(prefix='wiping', dir=os.path.join(get_publisher().app_dir))

View File

@ -4691,6 +4691,201 @@ class Audit(SqlMixin):
return first_id
class Application(SqlMixin):
_table_name = 'applications'
_table_static_fields = [
('id', 'serial'),
('slug', 'varchar'),
('name', 'varchar'),
('description', 'text'),
('documentation_url', 'varchar'),
('icon', 'bytea'),
('version_number', 'varchar'),
('version_notes', 'text'),
('editable', 'boolean'),
('visible', 'boolean'),
('created_at', 'timestamptz'),
('updated_at', 'timestamptz'),
]
id = None
@classmethod
@guard_postgres
def do_table(cls):
conn, cur = get_connection_and_cursor()
table_name = cls._table_name
cur.execute(
'''SELECT COUNT(*) FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name = %s''',
lguerin marked this conversation as resolved Outdated

Selon les volumes, effectivement, un index sur (object_type, object_id) ne serait pas de trop. Tout dépendra des volumes.

Selon les volumes, effectivement, un index sur (object_type, object_id) ne serait pas de trop. Tout dépendra des volumes.

index ajouté

index ajouté
(table_name,),
)
if cur.fetchone()[0] == 0:
cur.execute(
'''CREATE TABLE %s (id SERIAL PRIMARY KEY,
lguerin marked this conversation as resolved Outdated

Donc à chaque appel de do_table, on va supprimer et recréer la contrainte, et donc l'index lié. Pas fan du tout.

Donc à chaque appel de do_table, on va supprimer et recréer la contrainte, et donc l'index lié. Pas fan du tout.

Il y aurait / tu aurais une syntaxe en "IF NOT EXISTS" pour faire ça ?

Il y aurait / tu aurais une syntaxe en "IF NOT EXISTS" pour faire ça ?

j'ai fait une requête pour aller voir dans `information_schema.constraint_column_usage

j'ai fait une requête pour aller voir dans `information_schema.constraint_column_usage
slug VARCHAR NOT NULL,
name VARCHAR NOT NULL,
description TEXT,
documentation_url VARCHAR,
icon BYTEA,
version_number VARCHAR NOT NULL,
version_notes TEXT,
editable BOOLEAN,
visible BOOLEAN,
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL
)'''
% table_name
)
cur.execute('CREATE UNIQUE INDEX IF NOT EXISTS %s_slug ON %s (slug)' % (table_name, table_name))
conn.commit()
cur.close()
@guard_postgres
def store(self):
sql_dict = {x[0]: getattr(self, x[0], None) for x in self._table_static_fields if x[0] != 'id'}
sql_dict['updated_at'] = localtime()
if self.icon:
sql_dict['icon'] = bytearray(pickle.dumps(self.icon, protocol=2))
conn, cur = get_connection_and_cursor()
column_names = list(sql_dict.keys())
if not self.id:
sql_dict['created_at'] = sql_dict['updated_at']
sql_statement = '''INSERT INTO %s (id, %s)
VALUES (DEFAULT, %s)
RETURNING id''' % (
self._table_name,
', '.join(column_names),
', '.join(['%%(%s)s' % x for x in column_names]),
)
cur.execute(sql_statement, sql_dict)
self.id = cur.fetchone()[0]
else:
sql_dict['id'] = self.id
sql_statement = '''UPDATE %s SET %s WHERE id = %%(id)s RETURNING id''' % (
self._table_name,
', '.join(['%s = %%(%s)s' % (x, x) for x in column_names]),
)
cur.execute(sql_statement, sql_dict)
conn.commit()
cur.close()
@classmethod
def _row2ob(cls, row, **kwargs):
o = cls.__new__(cls)
for field, value in zip(cls._table_static_fields, tuple(row)):
if value and field[1] in ('bytea'):
value = pickle_loads(value)
setattr(o, field[0], value)
return o
@classmethod
def get_data_fields(cls):
return []
class ApplicationElement(SqlMixin):
_table_name = 'application_elements'
_table_static_fields = [
('id', 'serial'),
('application_id', 'integer'),
('object_type', 'varchar'),
('object_id', 'varchar'),
('created_at', 'timestamptz'),
('updated_at', 'timestamptz'),
]
id = None
@classmethod
@guard_postgres
def do_table(cls):
conn, cur = get_connection_and_cursor()
table_name = cls._table_name
cur.execute(
'''SELECT COUNT(*) FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name = %s''',
(table_name,),
)
if cur.fetchone()[0] == 0:
cur.execute(
'''CREATE TABLE %s (id SERIAL PRIMARY KEY,
application_id INTEGER NOT NULL,
object_type varchar NOT NULL,
object_id varchar NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL
)'''
% table_name
)
cur.execute(
'CREATE INDEX IF NOT EXISTS %s_object_idx ON %s (object_type, object_id)'
% (table_name, table_name)
)
cur.execute(
'''SELECT COUNT(*) FROM information_schema.constraint_column_usage
WHERE table_name = %s
AND constraint_name=%s''',
(table_name, '%s_unique' % table_name),
)
if cur.fetchone()[0] == 0:
cur.execute(
'ALTER TABLE %s ADD CONSTRAINT %s_unique UNIQUE (application_id, object_type, object_id)'
% (table_name, table_name)
)
conn.commit()
cur.close()
@guard_postgres
def store(self):
sql_dict = {x[0]: getattr(self, x[0], None) for x in self._table_static_fields if x[0] != 'id'}
sql_dict['updated_at'] = localtime()
conn, cur = get_connection_and_cursor()
column_names = list(sql_dict.keys())
if not self.id:
sql_dict['created_at'] = sql_dict['updated_at']
sql_statement = '''INSERT INTO %s (id, %s)
VALUES (DEFAULT, %s)
RETURNING id''' % (
self._table_name,
', '.join(column_names),
', '.join(['%%(%s)s' % x for x in column_names]),
)
cur.execute(sql_statement, sql_dict)
self.id = cur.fetchone()[0]
else:
sql_dict['id'] = self.id
sql_statement = '''UPDATE %s SET %s WHERE id = %%(id)s RETURNING id''' % (
self._table_name,
', '.join(['%s = %%(%s)s' % (x, x) for x in column_names]),
)
cur.execute(sql_statement, sql_dict)
conn.commit()
cur.close()
@classmethod
def _row2ob(cls, row, **kwargs):
o = cls.__new__(cls)
for attr, value in zip([x[0] for x in cls._table_static_fields], row):
setattr(o, attr, value)
return o
@classmethod
def get_data_fields(cls):
return []
class classproperty:
def __init__(self, f):
self.f = f
@ -5067,7 +5262,7 @@ def get_period_total(
# latest migration, number + description (description is not used
# programmaticaly but will make sure git conflicts if two migrations are
# separately added with the same number)
SQL_LEVEL = (83, 'add test_result table')
SQL_LEVEL = (84, 'add application tables')
def migrate_global_views(conn, cur):
@ -5253,6 +5448,10 @@ def migrate():
if sql_level < 83:
# 83: add test_result table
TestResult.do_table()
if sql_level < 84:
# 84: add application tables
Application.do_table()
ApplicationElement.do_table()
if sql_level < 52:
# 2: introduction of formdef_id in views
# 5: add concerned_roles_array, is_at_endpoint and fts to views

View File

@ -0,0 +1,5 @@
{% extends "wcs/backoffice/application_objects.html" %}
{% block content %}
{% include 'wcs/backoffice/includes/blocks.html' %}
{% endblock %}

View File

@ -0,0 +1,5 @@
{% extends "wcs/backoffice/application_objects.html" %}
{% block content %}
{% include 'wcs/backoffice/includes/comment-templates.html' %}
{% endblock %}

View File

@ -0,0 +1,5 @@
{% extends "wcs/backoffice/application_objects.html" %}
{% block content %}
{% include 'wcs/backoffice/includes/data-sources.html' %}
{% endblock %}

View File

@ -0,0 +1,5 @@
{% extends "wcs/backoffice/application_objects.html" %}
{% block content %}
{% include 'wcs/backoffice/includes/forms.html' %}
{% endblock %}

View File

@ -0,0 +1,5 @@
{% extends "wcs/backoffice/application_objects.html" %}
{% block content %}
{% include 'wcs/backoffice/includes/mail-templates.html' %}
{% endblock %}

View File

@ -0,0 +1,19 @@
{% extends "wcs/backoffice/base.html" %}
{% load i18n %}
{% block appbar-title %}
{% if application.icon %}
<img src="logo" alt="" class="application-logo" />
{% endif %}
{{ application.name }}
{% endblock %}
{% block content %}
<ul class="objects-list single-links">
{% for item in objects %}
<li>
<a href="{{ item.get_admin_url }}">{{ item.name }}</a>
</li>
{% endfor %}
</ul>
{% endblock %}

View File

@ -0,0 +1,5 @@
{% extends "wcs/backoffice/application_objects.html" %}
{% block content %}
{% include 'wcs/backoffice/includes/workflows.html' %}
{% endblock %}

View File

@ -0,0 +1,5 @@
{% extends "wcs/backoffice/application_objects.html" %}
{% block content %}
{% include 'wcs/backoffice/includes/wscalls.html' with wscalls=objects %}
{% endblock %}

View File

@ -10,31 +10,10 @@
<h3>{% trans "Navigation" %}</h3>
<a class="button button-paragraph" href="categories/">{% trans "Categories" %}</a>
{% include 'wcs/backoffice/includes/applications.html' %}
{% endblock %}
{% block body %}
{% if categories %}
{% for category in categories %}
{% if category.blocks %}
<div class="section">
<h2>{{ category.name }}</h2>
<ul class="objects-list single-links">
{% for block in category.blocks %}
<li><a href="{{ block.id }}/">{{ block.name }}</a></li>
{% endfor %}
</ul>
</div>
{% endif %}
{% endfor %}
{% elif blocks %}
<ul class="objects-list single-links">
{% for block in blocks %}
<li><a href="{{ block.id }}/">{{ block.name }}</a></li>
{% endfor %}
</ul>
{% else %}
<div class="infonotice">
{% trans "There are no field blocks defined." %}
</div>
{% endif %}
{% include 'wcs/backoffice/includes/blocks.html' %}
{% endblock %}

View File

@ -17,6 +17,8 @@
<a class="button button-paragraph" href="../forms/blocks/">{% trans "Fields blocks" %}</a>
<a class="button button-paragraph" href="../forms/data-sources/">{% trans "Data sources" %}</a>
{% endif %}
{% include 'wcs/backoffice/includes/applications.html' %}
{% endif %}
{% endblock %}

View File

@ -6,6 +6,8 @@
{% block sidebar-content %}
<h3>{% trans "Actions" %}</h3>
<a class="button button-paragraph" rel="popup" href="new">{% trans "New Category" %}</a>
{% include 'wcs/backoffice/includes/applications.html' %}
{% endblock %}
{% block body %}
@ -13,7 +15,12 @@
{% if categories %}
<ul class="biglist sortable" id="category-list">
{% for category in categories %}
<li class="biglistitem" id="itemId_{{ category.id }}"><a href="{{ category.id }}/">{{ category.name }}</a></li>
<li class="biglistitem" id="itemId_{{ category.id }}">
<a href="{{ category.id }}/">
{% include 'wcs/backoffice/includes/application_icons.html' with object=category %}
{{ category.name }}
</a>
</li>
{% endfor %}
</ul>
{% else %}

View File

@ -10,31 +10,10 @@
<h3>{% trans "Navigation" %}</h3>
<a class="button button-paragraph" href="categories/">{% trans "Categories" %}</a>
{% include 'wcs/backoffice/includes/applications.html' %}
{% endblock %}
{% block body %}
{% if categories %}
{% for category in categories %}
{% if category.comment_templates %}
<div class="section">
<h2>{{ category.name }}</h2>
<ul class="objects-list single-links">
{% for comment_template in category.comment_templates %}
<li><a href="{{ comment_template.id }}/">{{ comment_template.name }}</a></li>
{% endfor %}
</ul>
</div>
{% endif %}
{% endfor %}
{% elif comment_templates %}
<ul class="objects-list single-links">
{% for comment_template in comment_templates %}
<li><a href="{{ comment_template.id }}/">{{ comment_template.name }}</a></li>
{% endfor %}
</ul>
{% else %}
<div class="infonotice">
{% trans "There are no comment templates defined." %}
</div>
{% endif %}
{% include 'wcs/backoffice/includes/comment-templates.html' %}
{% endblock %}

View File

@ -16,85 +16,10 @@
<h3>{% trans "Navigation" %}</h3>
<a class="button button-paragraph" href="categories/">{% trans "Categories" %}</a>
{% include 'wcs/backoffice/includes/applications.html' %}
{% endblock %}
{% block content %}
{% if has_users %}
<div class="section foldable">
<h2>{% trans "Users Data Sources" %}</h2>
{% if user_data_sources %}
<ul class="objects-list single-links">
{% for data_source in user_data_sources %}
<li><a href="{{ data_source.id }}/">{{ data_source.name }} ({{ data_source.slug }})</a></li>
{% endfor %}
</ul>
{% else %}
<div>
{% trans "There are no users data sources defined." %}
</div>
{% endif %}
</div>
{% endif %}
{% if categories %}
{% for category in categories %}
{% if category.data_sources %}
<div class="section foldable">
<h2>{{ category.name }}</h2>
<ul class="objects-list single-links">
{% for data_source in category.data_sources %}
<li><a href="{{ data_source.id }}/">{{ data_source.name }} ({{ data_source.slug }})</a></li>
{% endfor %}
</ul>
</div>
{% endif %}
{% endfor %}
{% else %}
{% if data_sources %}
<div class="section foldable">
<h2>{% trans "Manually Configured Data Sources" %}</h2>
<ul class="objects-list single-links">
{% for data_source in data_sources %}
<li><a href="{{ data_source.id }}/">{{ data_source.name }} ({{ data_source.slug }})</a></li>
{% endfor %}
</ul>
</div>
{% else %}
<div>
{% trans "There are no data sources defined." %}
</div>
{% endif %}
{% endif %}
<div class="section foldable">
<h2>{% trans "Data Sources from Card Models" %} - {% trans "automatically configured" %}</h2>
{% if generated_data_sources %}
<ul class="objects-list single-links">
{% for data_source in generated_data_sources %}
<li><a href="{{ data_source.0.get_url }}{% if data_source.3 %}{{ data_source.3.get_url_slug }}/{% endif %}">{{ data_source.1 }}</a></li>
{% endfor %}
</ul>
{% else %}
<div>
{% trans "There are no data sources from card models." %}
</div>
{% endif %}
</div>
{% if has_chrono %}
<div class="section foldable">
<h2>{% trans "Agendas" %} - {% trans "automatically configured" %}</h2>
{% if agenda_data_sources %}
<ul class="objects-list single-links">
{% for data_source in agenda_data_sources %}
<li><a href="{{ data_source.id }}/">{{ data_source.name }} ({{ data_source.slug }}){% if data_source.external_status == 'not-found' %} - <span class="extra-info">{% trans "not found" %}</span>{% endif %}</a></li>
{% endfor %}
</ul>
{% else %}
<div>
{% trans "There are no agendas." %}
</div>
{% endif %}
</div>
{% endif %}
{% include 'wcs/backoffice/includes/data-sources.html' %}
{% endblock %}

View File

@ -7,23 +7,7 @@
{% if not has_roles %}
<p>{% trans "You first have to define roles." %}</p>
{% elif objects %}
{% for category in categories %}
{% if category.objects %}
<div class="section">
{% if category.name %}<h2>{{ category.name }}</h2>{% endif %}
<ul class="objects-list single-links">
{% for item in category.objects %}
<li {% if item.disabled %}class="disabled"{% endif %}><a href="{{ item.id }}/">{{ item.name }}
{% if item.disabled and item.disabled_redirection %}
<span class="extra-info">- {% trans "redirection" %}</span>
{% endif %}
</a>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% endfor %}
{% include 'wcs/backoffice/includes/forms.html' %}
{% else %}
<div class="infonotice">
{% block no-objects %}
@ -47,5 +31,7 @@
<a class="button button-paragraph" href="blocks/">{% trans "Fields blocks" %}</a>
<a class="button button-paragraph" href="data-sources/">{% trans "Data sources" %}</a>
{% endif %}
{% include 'wcs/backoffice/includes/applications.html' %}
{% endif %}
{% endblock %}

View File

@ -0,0 +1,5 @@
{% for application in object.applications %}
{% if application.icon %}
<img src="application/{{ application.slug }}/icon" alt="" class="application-icon" width="16" />

Pour les liens application/.../ je serais à préférer l'utilisation du slug, plutôt que l'id de l'application.

Pour les <img>, comme on connait les dimension, on pourrait les mettre dans les attributs, width="16" height="16" ici; ça éviterait un petit temps de décalage du texte au moment où l'icône apparait.

Pour les liens application/.../ je serais à préférer l'utilisation du slug, plutôt que l'id de l'application. Pour les `<img>`, comme on connait les dimension, on pourrait les mettre dans les attributs, width="16" height="16" ici; ça éviterait un petit temps de décalage du texte au moment où l'icône apparait.

fait, j'ai posé le slug dans toutes les urls, et ajouté une méthode get_by_slug à Application (qui n'hérite pas de StorableObject)

fait, j'ai posé le slug dans toutes les urls, et ajouté une méthode get_by_slug à Application (qui n'hérite pas de StorableObject)

et j'ai posé un width=16 aussi, mais pas le height; l'icône n'est pas forcément carrée (mais elle fait 16px de large, toujours)

et j'ai posé un width=16 aussi, mais pas le height; l'icône n'est pas forcément carrée (mais elle fait 16px de large, toujours)
{% endif %}
{% endfor %}

View File

@ -0,0 +1,13 @@
{% load i18n %}
{% if applications %}
<h3>{% trans "Applications" %}</h3>
{% for application in applications %}
<a class="button button-paragraph" href="application/{{ application.slug }}/">
{% if application.icon %}
<img src="application/{{ application.slug }}/icon" alt="" class="application-icon" width="16" />
{% endif %}
{{ application.name }}
</a>
{% endfor %}
{% endif %}

View File

@ -0,0 +1,35 @@
{% load i18n %}
{% if categories %}
{% for category in categories %}
{% if category.blocks %}
<div class="section">
<h2>{{ category.name }}</h2>
<ul class="objects-list single-links">
{% for block in category.blocks %}
<li>
<a href="{{ block.get_admin_url }}">
{% if not application %}{% include 'wcs/backoffice/includes/application_icons.html' with object=block %}{% endif %}
{{ block.name }}
</a>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% endfor %}
{% elif blocks %}
<ul class="objects-list single-links">
{% for block in blocks %}
<li>
<a href="{{ block.get_admin_url }}">
{% if not application %}{% include 'wcs/backoffice/includes/application_icons.html' with object=block %}{% endif %}
{{ block.name }}
</a>
</li>
{% endfor %}
</ul>
{% else %}
<div class="infonotice">
{% trans "There are no field blocks defined." %}
</div>
{% endif %}

View File

@ -0,0 +1,35 @@
{% load i18n %}
{% if categories %}
{% for category in categories %}
{% if category.comment_templates %}
<div class="section">
<h2>{{ category.name }}</h2>
<ul class="objects-list single-links">
{% for comment_template in category.comment_templates %}
<li>
<a href="{{ comment_template.get_admin_url }}">
{% if not application %}{% include 'wcs/backoffice/includes/application_icons.html' with object=comment_template %}{% endif %}
{{ comment_template.name }}
</a>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% endfor %}
{% elif comment_templates %}
<ul class="objects-list single-links">
{% for comment_template in comment_templates %}
<li>
<a href="{{ comment_template.get_admin_url }}">
{% if not application %}{% include 'wcs/backoffice/includes/application_icons.html' with object=comment_template %}{% endif %}
{{ comment_template.name }}
</a>
</li>
{% endfor %}
</ul>
{% else %}
<div class="infonotice">
{% trans "There are no comment templates defined." %}
</div>
{% endif %}

View File

@ -0,0 +1,102 @@
{% load i18n %}
<div class="section foldable">
<h2>{% trans "Users Data Sources" %}</h2>
{% if user_data_sources %}
<ul class="objects-list single-links">
{% for data_source in user_data_sources %}
<li>
<a href="{{ data_source.get_admin_url }}">
{% if not application %}{% include 'wcs/backoffice/includes/application_icons.html' with object=data_source %}{% endif %}
{{ data_source.name }} ({{ data_source.slug }})
</a>
</li>
{% endfor %}
</ul>
{% else %}
<div>
{% trans "There are no users data sources defined." %}
</div>
{% endif %}
</div>
{% if categories %}
{% for category in categories %}
{% if category.data_sources %}
<div class="section foldable">
<h2>{{ category.name }}</h2>
<ul class="objects-list single-links">
{% for data_source in category.data_sources %}
<li>
<a href="{{ data_source.get_admin_url }}">
{% if not application %}{% include 'wcs/backoffice/includes/application_icons.html' with object=data_source %}{% endif %}
{{ data_source.name }} ({{ data_source.slug }})
</a>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% endfor %}
{% else %}
{% if data_sources %}
<div class="section foldable">
<h2>{% trans "Manually Configured Data Sources" %}</h2>
<ul class="objects-list single-links">
{% for data_source in data_sources %}
<li>
<a href="{{ data_source.get_admin_url }}">
{% if not application %}{% include 'wcs/backoffice/includes/application_icons.html' with object=data_source %}{% endif %}
{{ data_source.name }} ({{ data_source.slug }})
</a>
</li>
{% endfor %}
</ul>
</div>
{% else %}
<div>
{% trans "There are no data sources defined." %}
</div>
{% endif %}
{% endif %}
<div class="section foldable">
<h2>{% trans "Data Sources from Card Models" %} - {% trans "automatically configured" %}</h2>
{% if generated_data_sources %}
<ul class="objects-list single-links">
{% for data_source in generated_data_sources %}
<li>
<a href="{{ data_source.0.get_url }}{% if data_source.3 %}{{ data_source.3.get_url_slug }}/{% endif %}">
{% if not application %}{% include 'wcs/backoffice/includes/application_icons.html' with object=data_source.0 %}{% endif %}
{{ data_source.1 }}
</a>
</li>
{% endfor %}
</ul>
{% else %}
<div>
{% trans "There are no data sources from card models." %}
</div>
{% endif %}
</div>
{% if has_chrono %}
<div class="section foldable">
<h2>{% trans "Agendas" %} - {% trans "automatically configured" %}</h2>
{% if agenda_data_sources %}
<ul class="objects-list single-links">
{% for data_source in agenda_data_sources %}
<li>
<a href="{{ data_source.get_admin_url }}">
{% if not application %}{% include 'wcs/backoffice/includes/application_icons.html' with object=data_source %}{% endif %}
{{ data_source.name }} ({{ data_source.slug }}){% if data_source.external_status == 'not-found' %} - <span class="extra-info">{% trans "not found" %}</span>{% endif %}
</a>
</li>
{% endfor %}
</ul>
{% else %}
<div>
{% trans "There are no agendas." %}
</div>
{% endif %}
</div>
{% endif %}

View File

@ -0,0 +1,20 @@
{% load i18n %}
{% for category in categories %}
{% if category.objects %}
<div class="section">
{% if category.name %}<h2>{{ category.name }}</h2>{% endif %}
<ul class="objects-list single-links">
{% for item in category.objects %}
<li {% if item.disabled %}class="disabled"{% endif %}><a href="{{ item.get_admin_url }}">
{% if not application %}{% include 'wcs/backoffice/includes/application_icons.html' with object=item %}{% endif %}
{{ item.name }}
{% if item.disabled and item.disabled_redirection %}
<span class="extra-info">- {% trans "redirection" %}</span>
{% endif %}
</a>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% endfor %}

View File

@ -0,0 +1,35 @@
{% load i18n %}
{% if categories %}
{% for category in categories %}
{% if category.mail_templates %}
<div class="section">
<h2>{{ category.name }}</h2>
<ul class="objects-list single-links">
{% for mail_template in category.mail_templates %}
<li>
<a href="{{ mail_template.get_admin_url }}">
{% if not application %}{% include 'wcs/backoffice/includes/application_icons.html' with object=mail_template %}{% endif %}
{{ mail_template.name }}
</a>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% endfor %}
{% elif mail_templates %}
<ul class="objects-list single-links">
{% for mail_template in mail_templates %}
<li>
<a href="{{ mail_template.get_admin_url }}">
{% if not application %}{% include 'wcs/backoffice/includes/application_icons.html' with object=mail_template %}{% endif %}
{{ mail_template.name }}
</a>
</li>
{% endfor %}
</ul>
{% else %}
<div class="infonotice">
{% trans "There are no mail templates defined." %}
</div>
{% endif %}

View File

@ -0,0 +1,20 @@
{% load i18n %}
{% for category in categories %}
{% if category.objects %}
<div class="section">
{% if category.name %}<h2>{{ category.name }}</h2>{% endif %}
<ul class="objects-list single-links">
{% for item in category.objects %}
<li class="{{ item.css_class }}">
<a href="{{ item.get_admin_url }}">
{% if not application %}{% include 'wcs/backoffice/includes/application_icons.html' with object=item %}{% endif %}
{{ item.name }}
{% if item.usage_label %}<span class="badge">{{ item.usage_label }}</span>{% endif %}
</a>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% endfor %}

View File

@ -0,0 +1,11 @@
{% load i18n %}
<ul class="objects-list single-links">
{% for wscall in wscalls %}
<li>
<a href="{{ wscall.id }}/">
{% include 'wcs/backoffice/includes/application_icons.html' with object=wscall %}
{{ wscall.name }} ({{ wscall.slug }})
</a>
</li>
{% endfor %}
</ul>

View File

@ -10,31 +10,10 @@
<h3>{% trans "Navigation" %}</h3>
<a class="button button-paragraph" href="categories/">{% trans "Categories" %}</a>
{% include 'wcs/backoffice/includes/applications.html' %}
{% endblock %}
{% block body %}
{% if categories %}
{% for category in categories %}
{% if category.mail_templates %}
<div class="section">
<h2>{{ category.name }}</h2>
<ul class="objects-list single-links">
{% for mail_template in category.mail_templates %}
<li><a href="{{ mail_template.id }}/">{{ mail_template.name }}</a></li>
{% endfor %}
</ul>
</div>
{% endif %}
{% endfor %}
{% elif mail_templates %}
<ul class="objects-list single-links">
{% for mail_template in mail_templates %}
<li><a href="{{ mail_template.id }}/">{{ mail_template.name }}</a></li>
{% endfor %}
</ul>
{% else %}
<div class="infonotice">
{% trans "There are no mail templates defined." %}
</div>
{% endif %}
{% include 'wcs/backoffice/includes/mail-templates.html' %}
{% endblock %}

View File

@ -4,21 +4,7 @@
{% block appbar-title %}{% trans "Workflows" %}{% endblock %}
{% block body %}
{% for category in categories %}
{% if category.objects %}
<div class="section">
{% if category.name %}<h2>{{ category.name }}</h2>{% endif %}
<ul class="objects-list single-links">
{% for item in category.objects %}
<li class="{{ item.css_class }}"><a href="{{ item.id }}/">{{ item.name }}
{% if item.usage_label %}<span class="badge">{{ item.usage_label }}</span>{% endif %}
</a>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% endfor %}
{% include 'wcs/backoffice/includes/workflows.html' %}
{% endblock %}
{% block sidebar-content %}
@ -33,4 +19,6 @@
<a class="button button-paragraph" href="mail-templates/">{% trans "Mail Templates" %}</a>
<a class="button button-paragraph" href="comment-templates/">{% trans "Comment Templates" %}</a>
{% endif %}
{% include 'wcs/backoffice/includes/applications.html' %}
{% endblock %}

View File

@ -7,15 +7,13 @@
<h3>{% trans "Actions" %}</h3>
<a class="button button-paragraph" href="new">{% trans "New webservice call" %}</a>
<a class="button button-paragraph" rel="popup" href="import">{% trans "Import" %}</a>
{% include 'wcs/backoffice/includes/applications.html' %}
{% endblock %}
{% block body %}
{% if wscalls %}
<ul class="objects-list single-links">
{% for wscall in wscalls %}
<li><a href="{{ wscall.id }}/">{{ wscall.name }} ({{ wscall.slug }})</a></li>
{% endfor %}
</ul>
{% include 'wcs/backoffice/includes/wscalls.html' %}
{% else %}
<div class="infonotice">
{% trans "There are no webservice calls defined." %}

View File

@ -26,6 +26,8 @@ urlpatterns = [
path('__provision__/', api.provisionning),
path('api/export-import/', api_export_import.index, name='api-export-import'),
path('api/export-import/bundle-import/', api_export_import.bundle_import),
path('api/export-import/bundle-declare/', api_export_import.bundle_declare),
path('api/export-import/unlink/', api_export_import.unlink),
re_path(
r'^api/export-import/(?P<objects>[\w-]+)/$',
api_export_import.objects_list,
Review

En évolutions diverses, je note ici (mais pas pour ce ticket déjà bien chargé),

  • affichage de l'info de l'application sur la page de l'objet en lui-même (pas uniquement sur la page d'index); ça passe peut-être d'abord par revoir/unifier les barres latérales.
En évolutions diverses, je note ici (mais pas pour ce ticket déjà bien chargé), * affichage de l'info de l'application sur la page de l'objet en lui-même (pas uniquement sur la page d'index); ça passe peut-être d'abord par revoir/unifier les barres latérales.
Review

j'y pensais aussi, mais pour ce premier ticket je me suis dit que c'était déjà bien assez

j'y pensais aussi, mais pour ce premier ticket je me suis dit que c'était déjà bien assez