diff --git a/build.gradle b/build.gradle index 6a460ca6..acc4b52a 100644 --- a/build.gradle +++ b/build.gradle @@ -32,7 +32,6 @@ dependencies { compileOnly "org.springframework.boot:spring-boot-starter-tomcat" compileOnly "org.grails:grails-dependencies" compileOnly "org.grails.plugins:gsp:${grailsVersion}" - implementation "org.grails:scaffolding-core:$grailsScaffoldingVersion" testImplementation "org.grails:grails-web-testing-support" testImplementation "org.grails:grails-gorm-testing-support" @@ -45,6 +44,8 @@ dependencies { } testRuntimeOnly "net.bytebuddy:byte-buddy:$byteBuddyVersion" + + testImplementation "org.grails:grails-datastore-gorm-hibernate5:$gormHibernate5Version" } tasks.withType(Test).configureEach { diff --git a/gradle.properties b/gradle.properties index 4e07b95e..805f782a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,6 +4,7 @@ grailsVersion=7.0.0-SNAPSHOT grailsGradlePluginVersion=7.0.0-SNAPSHOT grailsScaffoldingVersion=2.1.0 groovyVersion=4.0.23 +gormHibernate5Version=9.0.0-SNAPSHOT joddWotVersion=3.3.8 byteBuddyVersion=1.14.12 asciidoc=true diff --git a/src/docs/asciidoc/scaffolding/definitions/index.adoc b/src/docs/asciidoc/scaffolding/definitions/index.adoc new file mode 100644 index 00000000..4f7c3f52 --- /dev/null +++ b/src/docs/asciidoc/scaffolding/definitions/index.adoc @@ -0,0 +1,103 @@ +== Definitions + +The scaffolding classes provided by this module use words that have special meaning in the context of rendering HTML. + +=== Input + +Input is defined as a means for a user to input data. Typically this will come in the form of HTML elements like select or textarea. The definition is not limited to input elements because all facets of rendering inputs can be changed to include whatever makes sense for your plugin. + +=== Output + +Output is defined as a means for a user to view data. How this is represented is entirely based on whatever template rendering technology you are using. For Groovy pages it may look like `${domain.property}`. For Angular templates it may look like `{{domain.property}}`. + +=== Context + +Context is defined as the markup that surrounds or is rendered alongside domain properties. There are several different contexts this module defines. All contexts are rendered by the link:api/org/grails/scaffolding/markup/ContextMarkupRenderer.html[ContextMarkupRenderer]. + +==== List Output Context + +The markup surrounding the display of a list of domain classes. Examples are a table or a grid. + +==== Output Context (Domain) + +The markup surrounding the display of a domain class. An example is a container or ordered list. + +==== Output Context (Property) + +The markup surrounding the display of an individual domain class property. An example is a row in a grid or a list item. + +==== Input Context (Domain) + +The markup surrounding where a domain class will be created or modified. An example is a form. + +==== Input Context (Property) + +The markup surrounding the input of a domain class property. An example is a cell in a grid. + +==== Embedded Output Context + +The markup surrounding the input of the properties of an embedded domain class property. An example is a header. + +==== Embedded Input Context + +The markup surrounding the display of the properties of an embedded domain class property. An example is a fieldset. + +=== Examples + +[source,xml,indent=1] +---- +
<1> +
<2> + + <3> +
+
<2> + + <3> +
+
<4> + Address +
<2> + + <3> +
+
+ +
+---- +<1> Input Context (Domain) +<2> Input Context (Property) +<3> Rendered Domain Property +<4> Embedded Input Context + + +[source,xml,indent=1] +---- +
<1> +
+
Name
<2> +
+ ${user.name} <3> +
+ +
Age
+
+ ${user.age} <3> +
+ +
Address
<4> +
+
+
City
<2> +
+ ${user.address.city} <3> +
+
+
+
+
+---- +<1> Output Context (Domain) +<2> Output Context (Property) +<3> Rendered Domain Property +<4> Embedded Output Context \ No newline at end of file diff --git a/src/docs/asciidoc/scaffolding/extending/index.adoc b/src/docs/asciidoc/scaffolding/extending/index.adoc new file mode 100644 index 00000000..ca0d3369 --- /dev/null +++ b/src/docs/asciidoc/scaffolding/extending/index.adoc @@ -0,0 +1,59 @@ +== Extending + +All aspects of the way markup is created can be changed. + +=== Which properties are rendered + +The link:api/org/grails/scaffolding/model/DomainModelService.html[DomainModelService] is responsible for returning which properties should be rendered for any given view. To override the default behavior, register a bean with the name "domainModelService" that implements the link:api/org/grails/scaffolding/model/DomainModelService.html[DomainModelService] interface. You can extend the default implementation link:api/org/grails/scaffolding/model/DomainModelServiceImpl.html[DomainModelServiceImpl] if you wish. + +=== How the different contexts are rendered + +The link:api/org/grails/scaffolding/markup/ContextMarkupRenderer.html[ContextMarkupRenderer] is responsible for rendering all of the contexts. To override the default behavior, register a bean with the name "contextMarkupRenderer" that implements the link:api/org/grails/scaffolding/markup/ContextMarkupRenderer.html[ContextMarkupRenderer] interface. You can extend the default implementation link:api/org/grails/scaffolding/markup/ContextMarkupRendererImpl.html[ContextMarkupRendererImpl] if you wish. + +=== How properties are rendered + +The link:api/org/grails/scaffolding/markup/PropertyMarkupRenderer.html[PropertyMarkupRenderer] is responsible for rendering all of domain properties. To override the default behavior, register a bean with the name "propertyMarkupRenderer" that implements the link:api/org/grails/scaffolding/markup/PropertyMarkupRenderer.html[PropertyMarkupRenderer] interface. You can extend the default implementation link:api/org/grails/scaffolding/markup/PropertyMarkupRendererImpl.html[PropertyMarkupRendererImpl] if you wish. + +The default implementation simply defers to a domain output or input registry. The most convenient way to control how a specific type of property will be rendered is to register an input or ouput renderer to the registry. + +NOTE: All of the default renderers in the registry have a priority of < 0, so registering a custom one with a priority of > 0 will ensure it is used over the default + +==== Input Rendering + +Input renders must implement the link:api/org/grails/scaffolding/registry/DomainInputRenderer.html[DomainInputRenderer] interface. Implementations must define 2 methods. + +* `Boolean supports(DomainProperty domainProperty)` + Return true if your renderer supports the given property. See the link:api/org/grails/scaffolding/model/property/DomainProperty.html[DomainProperty] and link:http://gorm.grails.org/latest/api/org/grails/datastore/mapping/model/PersistentProperty.html[PersistentProperty] interfaces to see what data is available on the domain property instance. +* `Closure renderInput(Map defaultAttributes, DomainProperty property)` + Return a closure to be passed to a link:http://docs.groovy-lang.org/latest/html/api/groovy/xml/MarkupBuilder.html[MarkupBuilder] that renders your property + +NOTE: The default attributes passed to `renderInput` are created by the link:api/org/grails/scaffolding/markup/PropertyMarkupRenderer.html#getStandardAttributes(org.grails.scaffolding.model.property.DomainProperty)[PropertyMarkupRenderer#getStandardAttributes] method. + +To register your renderer, inject the "domainInputRendererRegistry" bean and execute `registerDomainRenderer`, passing along your renderer and its priority. + +[source,groovy,indent=1] +---- +DomainInputRendererRegistry domainInputRendererRegistry + +domainInputRendererRegistry.registerDomainRenderer(new MyCustomDomainInputRenderer(), 1) +---- + +==== Output Rendering + +Output renders must implement the link:api/org/grails/scaffolding/registry/DomainOutputRenderer.html[DomainOutputRenderer] interface. Implementations must define 3 methods. + +* `Boolean supports(DomainProperty domainProperty)` + Return true if your renderer supports the given property. See the link:api/org/grails/scaffolding/model/property/DomainProperty.html[DomainProperty] and link:http://gorm.grails.org/latest/api/org/grails/datastore/mapping/model/PersistentProperty.html[PersistentProperty] interfaces to see what data is available on the domain property instance. +* `Closure renderListOutput(DomainProperty property)` + Return a closure to be passed to a link:http://docs.groovy-lang.org/latest/html/api/groovy/xml/MarkupBuilder.html[MarkupBuilder] that renders your property in the context of a list of domain class instances +* `Closure renderOutput(DomainProperty property)` + Return a closure to be passed to a link:http://docs.groovy-lang.org/latest/html/api/groovy/xml/MarkupBuilder.html[MarkupBuilder] that renders your property in the context of a single domain class instance + +To register your renderer, inject the "domainOutputRendererRegistry" bean and execute `registerDomainRenderer`, passing along your renderer and its priority. + +[source,groovy,indent=1] +---- +DomainOutputRendererRegistry domainOutputRendererRegistry + +domainOutputRendererRegistry.registerDomainRenderer(new MyCustomDomainOutputRenderer(), 1) +---- \ No newline at end of file diff --git a/src/docs/asciidoc/scaffolding/index.adoc b/src/docs/asciidoc/scaffolding/index.adoc new file mode 100644 index 00000000..e8d56928 --- /dev/null +++ b/src/docs/asciidoc/scaffolding/index.adoc @@ -0,0 +1,17 @@ += Grails Scaffolding +:author: James Kleeh +:email: kleehj@ociweb.com +:source-highlighter: coderay +:numbered: + +== Introduction + +include::introduction.adoc[] + +include::definitions/index.adoc[] + +include::installation/index.adoc[] + +include::usage/index.adoc[] + +include::extending/index.adoc[] \ No newline at end of file diff --git a/src/docs/asciidoc/scaffolding/installation/index.adoc b/src/docs/asciidoc/scaffolding/installation/index.adoc new file mode 100644 index 00000000..bb8205af --- /dev/null +++ b/src/docs/asciidoc/scaffolding/installation/index.adoc @@ -0,0 +1,10 @@ +== Installation + +To start using this library, there are only a couple simple steps: + +. Import the library in your `build.gradle` +[source,groovy,subs="attributes",indent=1] +compile "org.grails:scaffolding-core:{version}" +. Register the scaffolding core bean configuration link:api/org/grails/scaffolding/ScaffoldingBeanConfiguration.html[ScaffoldingBeanConfiguration] +[source,groovy,indent=1] +scaffoldingBeanConfiguration(ScaffoldingBeanConfiguration) \ No newline at end of file diff --git a/src/docs/asciidoc/scaffolding/introduction.adoc b/src/docs/asciidoc/scaffolding/introduction.adoc new file mode 100644 index 00000000..a76ac991 --- /dev/null +++ b/src/docs/asciidoc/scaffolding/introduction.adoc @@ -0,0 +1,6 @@ +The Grails scaffolding core module was created to render 3 separate views: list, show, and create/edit. By default the views are rendered in a similar way to the link:https://grails-fields-plugin.github.io/grails-fields/[fields] plugin. All of the applicable rules of the link:https://grails-fields-plugin.github.io/grails-fields/[fields] plugin also apply to this module. + +Scaffolding core is designed to render markup intended to be processed by a template engine. The classes provided by this library will always render markup based on the definition of a domain class, instead of an individual instance or instances of a domain class that contain real data. + +This user guide will cover how this library works along with how developers can extend it to completely customize the results. + diff --git a/src/docs/asciidoc/scaffolding/usage/index.adoc b/src/docs/asciidoc/scaffolding/usage/index.adoc new file mode 100644 index 00000000..96ab2b05 --- /dev/null +++ b/src/docs/asciidoc/scaffolding/usage/index.adoc @@ -0,0 +1,23 @@ +== Usage + +To render markup, inject the link:api/org/grails/scaffolding/markup/DomainMarkupRenderer.html[DomainMarkupRenderer]. + +[source,groovy,indent=1] +DomainMarkupRenderer domainMarkupRenderer + +Then execute one of the methods provided: + +* `renderOutput` (Show page) +* `renderInput` (Edit/Create page) +* `renderListOutput` (List page) + +A string containing all of the markup will be returned. + +Each of the methods requires a link:http://gorm.grails.org/latest/api/org/grails/datastore/mapping/model/PersistentEntity.html[PersistentEntity] that represents the given domain class. To retrieve the link:http://gorm.grails.org/latest/api/org/grails/datastore/mapping/model/PersistentEntity.html[PersistentEntity], inject the mapping context and pass in the fully qualified domain class name. + +[source,groovy,indent=1] +---- +MappingContext grailsDomainClassMappingContext + +grailsDomainClassMappingContext.getPersistentEntity("test.foo.Bar") +---- \ No newline at end of file diff --git a/src/main/groovy/org/grails/scaffolding/ScaffoldingBeanConfiguration.groovy b/src/main/groovy/org/grails/scaffolding/ScaffoldingBeanConfiguration.groovy new file mode 100644 index 00000000..65c3ba26 --- /dev/null +++ b/src/main/groovy/org/grails/scaffolding/ScaffoldingBeanConfiguration.groovy @@ -0,0 +1,57 @@ +package org.grails.scaffolding + +import org.grails.scaffolding.markup.* +import org.grails.scaffolding.model.DomainModelService +import org.grails.scaffolding.model.DomainModelServiceImpl +import org.grails.scaffolding.model.property.DomainPropertyFactory +import org.grails.scaffolding.model.property.DomainPropertyFactoryImpl +import org.grails.scaffolding.registry.DomainInputRendererRegistry +import org.grails.scaffolding.registry.DomainOutputRendererRegistry +import org.grails.scaffolding.registry.DomainRendererRegisterer +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration +class ScaffoldingBeanConfiguration { + + @Bean + ContextMarkupRenderer contextMarkupRenderer() { + new ContextMarkupRendererImpl() + } + + @Bean + DomainMarkupRenderer domainMarkupRenderer() { + new DomainMarkupRendererImpl() + } + + @Bean + PropertyMarkupRenderer propertyMarkupRenderer() { + new PropertyMarkupRendererImpl() + } + + @Bean + DomainPropertyFactory domainPropertyFactory() { + new DomainPropertyFactoryImpl() + } + + @Bean + DomainModelService domainModelService() { + new DomainModelServiceImpl() + } + + @Bean + DomainInputRendererRegistry domainInputRendererRegistry() { + new DomainInputRendererRegistry() + } + + @Bean + DomainOutputRendererRegistry domainOutputRendererRegistry() { + new DomainOutputRendererRegistry() + } + + @Bean + DomainRendererRegisterer domainRendererRegisterer() { + new DomainRendererRegisterer() + } + +} diff --git a/src/main/groovy/org/grails/scaffolding/markup/ContextMarkupRenderer.groovy b/src/main/groovy/org/grails/scaffolding/markup/ContextMarkupRenderer.groovy new file mode 100644 index 00000000..62b8cb5c --- /dev/null +++ b/src/main/groovy/org/grails/scaffolding/markup/ContextMarkupRenderer.groovy @@ -0,0 +1,90 @@ +package org.grails.scaffolding.markup + +import org.grails.scaffolding.model.property.DomainProperty +import org.grails.datastore.mapping.model.PersistentEntity + +/** + * Used to output context surrounding any given content. Context is any markup that will be rendered + * along with any markup for domain property input or output. Input is used in this class to mean + * any HTML input type element (A way to retrieve users input). Output is used in this class to mean + * the display of a domain property on the page. + * + * An example of what might be returned with {@link #inputContext(DomainProperty,Closure)} + *
{@code
+ * { ->
+ *      div([class: "form-group"]) {
+ *          label('', [for: property.name])
+ *          content.delegate = delegate
+ *          content.call()
+ *      }}
+ * }
+ * + * @author James Kleeh + */ +interface ContextMarkupRenderer { + + /** + * Defines the context for rendering a list of domain class instances + * + * @param domainClass The domain class to be rendered + * @param properties The properties to be rendered + * @param content The content to be rendered for each property + * @return The closure to be passed to an instance of {@link groovy.xml.MarkupBuilder} + */ + Closure listOutputContext(PersistentEntity domainClass, List properties, Closure content) + + /** + * Defines the context for rendering a list of domain class properties inputs (form) + * + * @param domainClass The domain class to be rendered + * @param content The content to be rendered + * @return The closure to be passed to an instance of {@link groovy.xml.MarkupBuilder} + */ + Closure inputContext(PersistentEntity domainClass, Closure content) + + /** + * Defines the context for rendering a single domain class property input (select, textarea, etc) + * + * @param property The domain property to be rendered + * @param content The content to be rendered + * @return The closure to be passed to an instance of {@link groovy.xml.MarkupBuilder} + */ + Closure inputContext(DomainProperty property, Closure content) + + /** + * Defines the context for rendering a list domain class properties (show page) + * + * @param domainClass The domain class to be rendered + * @param content The content to be rendered + * @return The closure to be passed to an instance of {@link groovy.xml.MarkupBuilder} + */ + Closure outputContext(PersistentEntity domainClass, Closure content) + + /** + * Defines the context for rendering a single domain class property output + * + * @param property The domain property to be rendered + * @param content The content to be rendered + * @return The closure to be passed to an instance of {@link groovy.xml.MarkupBuilder} + */ + Closure outputContext(DomainProperty property, Closure content) + + /** + * Defines the context for rendering a the output of an embedded domain class property + * + * @param property The domain property to be rendered + * @param content The content to be rendered + * @return The closure to be passed to an instance of {@link groovy.xml.MarkupBuilder} + */ + Closure embeddedOutputContext(DomainProperty property, Closure content) + + /** + * Defines the context for rendering a the input of an embedded domain class property + * + * @param property The domain property to be rendered + * @param content The content to be rendered + * @return The closure to be passed to an instance of {@link groovy.xml.MarkupBuilder} + */ + Closure embeddedInputContext(DomainProperty property, Closure content) + +} \ No newline at end of file diff --git a/src/main/groovy/org/grails/scaffolding/markup/ContextMarkupRendererImpl.groovy b/src/main/groovy/org/grails/scaffolding/markup/ContextMarkupRendererImpl.groovy new file mode 100644 index 00000000..3c257ac0 --- /dev/null +++ b/src/main/groovy/org/grails/scaffolding/markup/ContextMarkupRendererImpl.groovy @@ -0,0 +1,135 @@ +package org.grails.scaffolding.markup + +import org.grails.scaffolding.model.property.DomainProperty +import grails.util.GrailsNameUtils +import groovy.transform.CompileStatic +import org.grails.datastore.mapping.model.PersistentEntity +import org.springframework.context.MessageSource + +import jakarta.annotation.Resource + +/** + * @see {@link ContextMarkupRenderer} + * @author James Kleeh + */ +class ContextMarkupRendererImpl implements ContextMarkupRenderer { + + @Resource + MessageSource messageSource + + @CompileStatic + protected String getDefaultTableHeader(DomainProperty property) { + property.defaultLabel + } + + @CompileStatic + protected String getLabelText(DomainProperty property) { + String labelText + if (property.labelKeys) { + labelText = resolveMessage(property.labelKeys, property.defaultLabel) + } + if (!labelText) { + labelText = property.defaultLabel + } + labelText + } + + @CompileStatic + protected String resolveMessage(List keysInPreferenceOrder, String defaultMessage) { + def message = keysInPreferenceOrder.findResult { key -> + messageSource.getMessage(key, [].toArray(), defaultMessage, Locale.default) ?: null + } + message ?: defaultMessage + } + + @CompileStatic + protected String toPropertyNameFormat(Class type) { + String propertyNameFormat = GrailsNameUtils.getLogicalPropertyName(type.canonicalName, '') + if (propertyNameFormat.endsWith('[]')) { + propertyNameFormat = propertyNameFormat - '[]' + 'Array' + } + propertyNameFormat + } + + @Override + Closure listOutputContext(PersistentEntity domainClass, List properties, Closure content) { + { -> + table { + thead { + tr { + properties.each { + th(getDefaultTableHeader(it)) + } + } + } + tbody { + tr { + properties.each { property -> + td(content.call(property)) + } + } + } + } + } + } + + @Override + Closure inputContext(PersistentEntity domainClass, Closure content) { + { -> + fieldset([class: "form"], content) + } + } + + @Override + Closure inputContext(DomainProperty property, Closure content) { + List classes = ['fieldcontain'] + if (property.required) { + classes << 'required' + } + { -> + content.delegate = delegate + div(class: classes.join(' ')) { + label([for: property.pathFromRoot], getLabelText(property)) { + if (property.required) { + span(class: 'required-indicator', '*') + } + } + content.call() + } + } + } + + @Override + Closure outputContext(PersistentEntity domainClass, Closure content) { + { -> + ol([class: "property-list ${domainClass.decapitalizedName}"], content) + } + } + + @Override + Closure outputContext(DomainProperty property, Closure content) { + { -> + li(class: 'fieldcontain') { + span([id: "${property.pathFromRoot}-label", class: "property-label"], getLabelText(property)) + div([class: "property-value", "aria-labelledby": "${property.pathFromRoot}-label"], content) + } + } + } + + @Override + Closure embeddedOutputContext(DomainProperty property, Closure content) { + embeddedInputContext(property, content) + } + + @Override + Closure embeddedInputContext(DomainProperty property, Closure content) { + return { -> + content.delegate = delegate + fieldset(class: "embedded ${toPropertyNameFormat(property.type)}") { + legend(getLabelText(property)) + content.call() + } + } + } + +} diff --git a/src/main/groovy/org/grails/scaffolding/markup/DomainMarkupRenderer.groovy b/src/main/groovy/org/grails/scaffolding/markup/DomainMarkupRenderer.groovy new file mode 100644 index 00000000..989e24bf --- /dev/null +++ b/src/main/groovy/org/grails/scaffolding/markup/DomainMarkupRenderer.groovy @@ -0,0 +1,37 @@ +package org.grails.scaffolding.markup + +import org.grails.datastore.mapping.model.PersistentEntity + +/** + * Used to output markup that represents a given domain class. + * + * @author James Kleeh + */ +interface DomainMarkupRenderer { + + /** + * Designed to render a "show" page that will display a single domain class instance. + * + * @param domainClass The domain class to be rendered + * @return The rendered html + */ + String renderOutput(PersistentEntity domainClass) + + /** + * Designed to render a "list" page that will display a list of domain class instances. + * + * @param domainClass The domain class to be rendered + * @return The rendered html + */ + String renderListOutput(PersistentEntity domainClass) + + + /** + * Designed to render a form that will allow users to create or edit domain class instances. + * + * @param domainClass The domain class to be rendered + * @return The rendered html + */ + String renderInput(PersistentEntity domainClass) + +} \ No newline at end of file diff --git a/src/main/groovy/org/grails/scaffolding/markup/DomainMarkupRendererImpl.groovy b/src/main/groovy/org/grails/scaffolding/markup/DomainMarkupRendererImpl.groovy new file mode 100644 index 00000000..0e3d610b --- /dev/null +++ b/src/main/groovy/org/grails/scaffolding/markup/DomainMarkupRendererImpl.groovy @@ -0,0 +1,123 @@ +package org.grails.scaffolding.markup + +import org.grails.scaffolding.model.DomainModelService +import org.grails.scaffolding.model.property.DomainProperty +import groovy.transform.CompileStatic +import groovy.xml.MarkupBuilder +import org.grails.buffer.FastStringWriter +import org.grails.datastore.mapping.model.PersistentEntity +import org.grails.datastore.mapping.model.types.Embedded +import org.springframework.beans.factory.annotation.Autowired + +/** + * @see {@link DomainMarkupRenderer} + * @author James Kleeh + */ +class DomainMarkupRendererImpl implements DomainMarkupRenderer { + + @Autowired + DomainModelService domainModelService + + @Autowired + PropertyMarkupRenderer propertyMarkupRenderer + + @Autowired + ContextMarkupRenderer contextMarkupRenderer + + static void callWithDelegate(delegate, Closure closure) { + closure.delegate = delegate + closure.call() + } + + static String outputMarkupContent(Closure closure) { + FastStringWriter writer = new FastStringWriter() + MarkupBuilder markupBuilder = new MarkupBuilder(writer) + markupBuilder.doubleQuotes = true + markupBuilder.escapeAttributes = false + closure.delegate = markupBuilder + if (closure.maximumNumberOfParameters == 1) { + closure.call(markupBuilder) + } else { + closure.call() + } + writer.toString() + } + + protected Closure renderInput(DomainProperty property) { + contextMarkupRenderer.inputContext(property, propertyMarkupRenderer.renderInput(property)) + } + + protected Closure renderOutput(DomainProperty property) { + contextMarkupRenderer.outputContext(property, propertyMarkupRenderer.renderOutput(property)) + } + + /** + * Determines how many properties will be included in the list output + */ + protected int getMaxListOutputSize() { + 7 + } + + String renderListOutput(PersistentEntity domainClass) { + List tableProperties = [] + List domainProperties = domainModelService.getListOutputProperties(domainClass) + domainProperties.each { DomainProperty property -> + if (property.persistentProperty instanceof Embedded) { + domainModelService.getOutputProperties(((Embedded)property.persistentProperty).associatedEntity).each { DomainProperty embedded -> + embedded.rootProperty = property + tableProperties.add(embedded) + } + } else { + tableProperties.add(property) + } + } + if (tableProperties.size() > maxListOutputSize) { + tableProperties = tableProperties[0..(maxListOutputSize-1)] + } + outputMarkupContent ( + contextMarkupRenderer.listOutputContext(domainClass, tableProperties) { DomainProperty domainProperty -> + propertyMarkupRenderer.renderListOutput(domainProperty) + } + ) + } + + String renderInput(PersistentEntity domainClass) { + outputMarkupContent( + contextMarkupRenderer.inputContext(domainClass) { -> + def contextDelegate = delegate + domainModelService.getInputProperties(domainClass).each { DomainProperty property -> + if (property.persistentProperty instanceof Embedded) { + callWithDelegate(contextDelegate, contextMarkupRenderer.embeddedInputContext(property) { + domainModelService.getInputProperties(((Embedded)property.persistentProperty).associatedEntity).each { DomainProperty embedded -> + embedded.rootProperty = property + callWithDelegate(contextDelegate, renderInput(embedded)) + } + }) + } else { + callWithDelegate(contextDelegate, renderInput(property)) + } + } + } + ) + } + + String renderOutput(PersistentEntity domainClass) { + outputMarkupContent( + contextMarkupRenderer.outputContext(domainClass) { -> + def contextDelegate = delegate + domainModelService.getOutputProperties(domainClass).each { DomainProperty property -> + if (property.persistentProperty instanceof Embedded) { + callWithDelegate(contextDelegate, contextMarkupRenderer.embeddedOutputContext(property) { -> + domainModelService.getOutputProperties(((Embedded)property.persistentProperty).associatedEntity).each { DomainProperty embedded -> + embedded.rootProperty = property + callWithDelegate(contextDelegate, renderOutput(embedded)) + } + }) + } else { + callWithDelegate(contextDelegate, renderOutput(property)) + } + } + } + ) + } +} diff --git a/src/main/groovy/org/grails/scaffolding/markup/PropertyMarkupRenderer.groovy b/src/main/groovy/org/grails/scaffolding/markup/PropertyMarkupRenderer.groovy new file mode 100644 index 00000000..b8a89116 --- /dev/null +++ b/src/main/groovy/org/grails/scaffolding/markup/PropertyMarkupRenderer.groovy @@ -0,0 +1,55 @@ +package org.grails.scaffolding.markup + +import org.grails.scaffolding.model.property.DomainProperty + +/** + * Used to render markup that represents a single domain class property + * + * @author James Kleeh + */ +trait PropertyMarkupRenderer { + + /** + * Builds the standard html attributes that will be passed to {@link grails.plugin.scaffolding.registry.DomainInputRenderer#renderInput} + * + * @param property The domain property to be rendered + * @return A map of the standard attributes + */ + Map getStandardAttributes(DomainProperty property) { + final String name = property.pathFromRoot + Map attributes = [:] + if (property.required) { + attributes.required = null + } + if (property.constrained && !property.constrained.editable) { + attributes.readonly = null + } + attributes.name = name + attributes.id = name + attributes + } + + /** + * Defines how a given domain class property will be rendered in the context of a list of domains class instances + * + * @param property The domain property to be rendered + * @return The closure to be passed to an instance of {@link groovy.xml.MarkupBuilder} + */ + abstract Closure renderListOutput(DomainProperty property) + + /** + * Defines how a given domain class property will be rendered in the context of a single domains class instance + * + * @param property The domain property to be rendered + * @return The closure to be passed to an instance of {@link groovy.xml.MarkupBuilder} + */ + abstract Closure renderOutput(DomainProperty property) + + /** + * Defines how a given domain class property will be rendered in the context of a form + * + * @param property The domain property to be rendered + * @return The closure to be passed to an instance of {@link groovy.xml.MarkupBuilder} + */ + abstract Closure renderInput(DomainProperty property) +} \ No newline at end of file diff --git a/src/main/groovy/org/grails/scaffolding/markup/PropertyMarkupRendererImpl.groovy b/src/main/groovy/org/grails/scaffolding/markup/PropertyMarkupRendererImpl.groovy new file mode 100644 index 00000000..c1cd205e --- /dev/null +++ b/src/main/groovy/org/grails/scaffolding/markup/PropertyMarkupRendererImpl.groovy @@ -0,0 +1,36 @@ +package org.grails.scaffolding.markup + +import org.grails.scaffolding.model.property.DomainProperty +import org.grails.scaffolding.registry.DomainOutputRendererRegistry +import org.grails.scaffolding.registry.DomainInputRendererRegistry +import groovy.transform.CompileStatic +import org.springframework.beans.factory.annotation.Autowired + +/** + * @see {@link PropertyMarkupRenderer} + * @author James Kleeh + */ +@CompileStatic +class PropertyMarkupRendererImpl implements PropertyMarkupRenderer { + + @Autowired + DomainInputRendererRegistry domainInputRendererRegistry + + @Autowired + DomainOutputRendererRegistry domainOutputRendererRegistry + + @Override + Closure renderListOutput(DomainProperty property) { + domainOutputRendererRegistry.get(property).renderListOutput(property) + } + + @Override + Closure renderOutput(DomainProperty property) { + domainOutputRendererRegistry.get(property).renderOutput(property) + } + + @Override + Closure renderInput(DomainProperty property) { + domainInputRendererRegistry.get(property).renderInput(getStandardAttributes(property), property) + } +} diff --git a/src/main/groovy/org/grails/scaffolding/model/DomainModelService.groovy b/src/main/groovy/org/grails/scaffolding/model/DomainModelService.groovy new file mode 100644 index 00000000..079ba48f --- /dev/null +++ b/src/main/groovy/org/grails/scaffolding/model/DomainModelService.groovy @@ -0,0 +1,50 @@ +package org.grails.scaffolding.model + +import org.grails.scaffolding.model.property.DomainProperty +import org.grails.datastore.mapping.model.PersistentEntity + +/** + * An API to retrieve properties from a {@link PersistentEntity} + * + * @author James Kleeh + */ +interface DomainModelService { + + /** + * The list of {@link DomainProperty} instances that allow for user input + * + * @param domainClass The persistent entity + */ + List getInputProperties(PersistentEntity domainClass) + + /** + * The list of {@link DomainProperty} instances that are to be visible + * + * @param domainClass The persistent entity + */ + List getOutputProperties(PersistentEntity domainClass) + + /** + * The list of {@link DomainProperty} instances that are to be visible in a list context + * + * @param domainClass The persistent entity + */ + List getListOutputProperties(PersistentEntity domainClass) + + /** + * The list of {@link DomainProperty} instances that allow for user input and the closure returns true for + * + * @param domainClass The persistent entity + * @param closure The closure that will be executed for each editable property + */ + List findInputProperties(PersistentEntity domainClass, Closure closure) + + /** + * Determines if the closure returns true for any input {@link DomainProperty} + * + * @param domainClass The persistent entity + * @param closure The closure that will be executed for each property + */ + Boolean hasInputProperty(PersistentEntity domainClass, Closure closure) + +} diff --git a/src/main/groovy/org/grails/scaffolding/model/DomainModelServiceImpl.groovy b/src/main/groovy/org/grails/scaffolding/model/DomainModelServiceImpl.groovy new file mode 100644 index 00000000..4feb6438 --- /dev/null +++ b/src/main/groovy/org/grails/scaffolding/model/DomainModelServiceImpl.groovy @@ -0,0 +1,162 @@ +package org.grails.scaffolding.model + +import org.grails.datastore.mapping.config.Property +import org.grails.scaffolding.model.property.Constrained +import org.grails.scaffolding.model.property.DomainProperty +import org.grails.scaffolding.model.property.DomainPropertyFactory +import grails.util.GrailsClassUtils +import groovy.transform.CompileStatic +import org.grails.datastore.mapping.model.PersistentEntity +import org.grails.datastore.mapping.model.PersistentProperty +import org.grails.datastore.mapping.model.types.Embedded +import org.springframework.beans.factory.annotation.Autowired + +import java.lang.reflect.Method + +/** + * @see {@link DomainModelService} + * @author James Kleeh + */ +@CompileStatic +class DomainModelServiceImpl implements DomainModelService { + + @Autowired + DomainPropertyFactory domainPropertyFactory + + private static Method derivedMethod + + static { + try { + derivedMethod = Property.class.getMethod("isDerived", (Class[]) null) + } catch (NoSuchMethodException | SecurityException e) { + // no-op + } + } + + /** + *

Retrieves persistent properties and excludes:

    + *
  • Any properties listed in the {@code static scaffold = [exclude: []]} property on the domain class + *
  • Any properties that have the constraint {@code [display: false]} + *
  • Any properties whose name exist in the blackList + *

+ * + * @see {@link DomainModelService#getInputProperties} + * @param domainClass The persistent entity + * @param blackList The list of domain class property names to exclude + */ + protected List getProperties(PersistentEntity domainClass, List blacklist) { + List properties = domainClass.persistentProperties.collect { + domainPropertyFactory.build(it) + } + Object scaffoldProp = GrailsClassUtils.getStaticPropertyValue(domainClass.javaClass, 'scaffold') + if (scaffoldProp instanceof Map) { + Map scaffold = (Map)scaffoldProp + if (scaffold.containsKey('exclude')) { + if (scaffold.exclude instanceof Collection) { + blacklist.addAll((Collection)scaffold.exclude) + } else if (scaffold.exclude instanceof String) { + blacklist.add((String)scaffold.exclude) + } + } + } + + properties.removeAll { + if (it.name in blacklist) { + return true + } + Constrained constrained = it.constrained + if (constrained && !constrained.display) { + return true + } + if (derivedMethod != null) { + Property property = it.mapping.mappedForm + if (derivedMethod.invoke(property, (Object[]) null)) { + return true + } + } + + false + } + properties.sort() + properties + } + + /** + *

Blacklist:

    + *
  • version + *
  • dateCreated + *
  • lastUpdated + *

+ * + * @see {@link DomainModelServiceImpl#getProperties} + * @param domainClass The persistent entity + */ + List getInputProperties(PersistentEntity domainClass) { + getProperties(domainClass, ['version', 'dateCreated', 'lastUpdated']) + } + + /** + *

Blacklist:

    + *
  • version + *

+ * + * @see {@link DomainModelServiceImpl#getProperties} + * @param domainClass The persistent entity + */ + List getOutputProperties(PersistentEntity domainClass) { + getProperties(domainClass, ['version']) + } + + /** + *

The same as {@link #getOutputProperties(org.grails.datastore.mapping.model.PersistentEntity)} except the identifier is prepended

+ * + * @see {@link DomainModelServiceImpl#getOutputProperties} + * @param domainClass The persistent entity + */ + List getListOutputProperties(PersistentEntity domainClass) { + List properties = getOutputProperties(domainClass) + properties.add(0, domainPropertyFactory.build(domainClass.identity)) + properties + } + + /** + * Will return all properties in a domain class that the provided closure returns + * true for. Searches embedded properties + * + * @see {@link DomainModelService#findInputProperties} + * @param domainClass The persistent entity + * @param closure The closure that will be executed for each editable property + */ + List findInputProperties(PersistentEntity domainClass, Closure closure) { + List properties = [] + getInputProperties(domainClass).each { DomainProperty domainProperty -> + PersistentProperty property = domainProperty.persistentProperty + if (property instanceof Embedded) { + getInputProperties(((Embedded)property).associatedEntity).each { DomainProperty embedded -> + embedded.rootProperty = domainProperty + if (closure.call(embedded)) { + properties.add(embedded) + } + } + } else { + if (closure.call(domainProperty)) { + properties.add(domainProperty) + } + } + } + properties + } + + /** + * Returns true if the provided closure returns true for any domain class + * property. Searches embedded properties + * + * @see {@link DomainModelService#hasInputProperty} + * @param domainClass The persistent entity + * @param closure The closure that will be executed for each editable property + */ + Boolean hasInputProperty(PersistentEntity domainClass, Closure closure) { + findInputProperties(domainClass, closure).size() > 0 + } + +} diff --git a/src/main/groovy/org/grails/scaffolding/model/property/Constrained.groovy b/src/main/groovy/org/grails/scaffolding/model/property/Constrained.groovy new file mode 100644 index 00000000..8512f388 --- /dev/null +++ b/src/main/groovy/org/grails/scaffolding/model/property/Constrained.groovy @@ -0,0 +1,215 @@ +package org.grails.scaffolding.model.property + + +class Constrained { + + grails.gorm.validation.Constrained constrained1 + + Constrained(grails.gorm.validation.Constrained constrained1) { + this.constrained1 = constrained1 + } + + Object callMethod(String name, Object arguments) { + if (this.constrained1 != null) { + this.constrained1.invokeMethod(name, arguments) + } else { + null + } + } + + boolean hasAppliedConstraint(String name) { + if (this.constrained1 != null) { + this.constrained1.hasAppliedConstraint(name) + } else { + false + } + } + + int getOrder() { + if (this.constrained1 != null) { + this.constrained1.order + } else { + 0 + } + } + + boolean isNullable() { + if (this.constrained1 != null) { + this.constrained1.nullable + } else { + false + } + } + + boolean isBlank() { + if (this.constrained1 != null) { + this.constrained1.blank + } else { + false + } + } + + boolean isDisplay() { + if (this.constrained1 != null) { + this.constrained1.display + } else { + true + } + } + + boolean isEditable() { + if (this.constrained1 != null) { + this.constrained1.editable + } else { + true + } + } + + List getInList() { + if (this.constrained1 != null) { + this.constrained1.inList + } else { + null + } + } + + Range getRange() { + if (this.constrained1 != null) { + this.constrained1.range + } else { + null + } + } + + Integer getScale() { + if (this.constrained1 != null) { + this.constrained1.scale + } else { + null + } + } + + Comparable getMin() { + if (this.constrained1 != null) { + this.constrained1.min + } else { + null + } + } + + Comparable getMax() { + if (this.constrained1 != null) { + this.constrained1.max + } else { + null + } + } + + Range getSize() { + if (this.constrained1 != null) { + this.constrained1.size + } else { + null + } + } + + Integer getMaxSize() { + if (this.constrained1 != null) { + this.constrained1.maxSize + } else { + null + } + } + + String getWidget() { + if (this.constrained1 != null) { + this.constrained1.widget + } else { + null + } + } + + boolean isPassword() { + if (this.constrained1 != null) { + this.constrained1.password + } else { + false + } + } + + boolean isEmail() { + if (this.constrained1 != null) { + this.constrained1.email + } else { + false + } + } + + boolean isCreditCard() { + if (this.constrained1 != null) { + this.constrained1.creditCard + } else { + false + } + } + + boolean isUrl() { + if (this.constrained1 != null) { + this.constrained1.url + } else { + false + } + } + + String getMatches() { + if (this.constrained1 != null) { + this.constrained1.matches + } else { + null + } + } + + Object getNotEqual() { + if (this.constrained1 != null) { + this.constrained1.notEqual + } else { + null + } + } + + Integer getMinSize() { + if (this.constrained1 != null) { + this.constrained1.minSize + } else { + null + } + } + + String getFormat() { + if (this.constrained1 != null) { + this.constrained1.format + } else { + null + } + } + + void applyConstraint(String constraintName, Object constrainingValue) { + if (this.constrained1 != null) { + this.constrained1.applyConstraint(constraintName, constrainingValue) + } else { + null + } + } + + Class getOwner() { + if (this.constrained1 != null) { + this.constrained1.owner + } else { + null + } + } + + boolean isNull() { + this.constrained1 == null + } +} diff --git a/src/main/groovy/org/grails/scaffolding/model/property/DomainProperty.groovy b/src/main/groovy/org/grails/scaffolding/model/property/DomainProperty.groovy new file mode 100644 index 00000000..8cca6e48 --- /dev/null +++ b/src/main/groovy/org/grails/scaffolding/model/property/DomainProperty.groovy @@ -0,0 +1,81 @@ +package org.grails.scaffolding.model.property + +import org.grails.datastore.mapping.model.PersistentEntity +import org.grails.datastore.mapping.model.PersistentProperty + +/** + * An API to join the {@link PersistentProperty} to the {@link org.springframework.validation.Validator} + * to assist with scaffolding + * + * @author James Kleeh + */ +interface DomainProperty extends PersistentProperty, Comparable { + + /** + * @return The path of the property from the root domain class + */ + String getPathFromRoot() + + /** + * @return The {@link PersistentProperty} that represents this property + */ + PersistentProperty getPersistentProperty() + + /** + * @return The {@link PersistentEntity} the property belongs to + */ + PersistentEntity getDomainClass() + + /** + * @return The constraints of the property + */ + Constrained getConstrained() + + /** + * @return The root property + */ + PersistentProperty getRootProperty() + + /** + * Sets the root property + * + * @param rootProperty The root property + */ + void setRootProperty(PersistentProperty rootProperty) + + /** + * @return The class the root property belongs to + */ + Class getRootBeanType() + + /** + * @return The class the property belongs to + */ + Class getBeanType() + + /** + * @return The type of the association + */ + Class getAssociatedType() + + /** + * @return The associated entity if the property is an assocation + */ + PersistentEntity getAssociatedEntity() + + /** + * @return Whether or not the property is required + */ + boolean isRequired() + + /** + * @return i18n message keys to resolve the label of the property + */ + List getLabelKeys() + + /** + * @return The default label for the property (natural name) + */ + String getDefaultLabel() + +} \ No newline at end of file diff --git a/src/main/groovy/org/grails/scaffolding/model/property/DomainPropertyFactory.groovy b/src/main/groovy/org/grails/scaffolding/model/property/DomainPropertyFactory.groovy new file mode 100644 index 00000000..a2af4577 --- /dev/null +++ b/src/main/groovy/org/grails/scaffolding/model/property/DomainPropertyFactory.groovy @@ -0,0 +1,25 @@ +package org.grails.scaffolding.model.property + +import org.grails.datastore.mapping.model.PersistentProperty + +/** + * A factory to create instances of {@link DomainProperty} + * + * @author James Kleeh + */ +interface DomainPropertyFactory { + + /** + * @param persistentProperty The persistent property + * @return The {@link DomainProperty} representing the {@link PersistentProperty} + */ + DomainProperty build(PersistentProperty persistentProperty) + + /** + * @param rootProperty The root property. Typically an instance of {@link org.grails.datastore.mapping.model.types.Embedded} + * @param persistentProperty The persistent property + * @return The {@link DomainProperty} representing the {@link PersistentProperty} + */ + DomainProperty build(PersistentProperty rootProperty, PersistentProperty persistentProperty) + +} diff --git a/src/main/groovy/org/grails/scaffolding/model/property/DomainPropertyFactoryImpl.groovy b/src/main/groovy/org/grails/scaffolding/model/property/DomainPropertyFactoryImpl.groovy new file mode 100644 index 00000000..4464ed9e --- /dev/null +++ b/src/main/groovy/org/grails/scaffolding/model/property/DomainPropertyFactoryImpl.groovy @@ -0,0 +1,41 @@ +package org.grails.scaffolding.model.property + +import groovy.transform.CompileStatic +import org.grails.datastore.mapping.model.MappingContext +import org.grails.datastore.mapping.model.PersistentProperty +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.beans.factory.annotation.Value + +/** + * @see {@link DomainPropertyFactory} + * @author James Kleeh + */ +@CompileStatic +class DomainPropertyFactoryImpl implements DomainPropertyFactory { + + @Value('${grails.databinding.convertEmptyStringsToNull:true}') + Boolean convertEmptyStringsToNull + + @Value('${grails.databinding.trimStrings:true}') + Boolean trimStrings + + @Autowired + MappingContext grailsDomainClassMappingContext + + DomainProperty build(PersistentProperty persistentProperty) { + DomainPropertyImpl domainProperty = new DomainPropertyImpl(persistentProperty, grailsDomainClassMappingContext) + init(domainProperty) + domainProperty + } + + DomainProperty build(PersistentProperty rootProperty, PersistentProperty persistentProperty) { + DomainPropertyImpl domainProperty = new DomainPropertyImpl(rootProperty, persistentProperty, grailsDomainClassMappingContext) + init(domainProperty) + domainProperty + } + + private init(DomainPropertyImpl domainProperty) { + domainProperty.convertEmptyStringsToNull = convertEmptyStringsToNull + domainProperty.trimStrings = trimStrings + } +} diff --git a/src/main/groovy/org/grails/scaffolding/model/property/DomainPropertyImpl.groovy b/src/main/groovy/org/grails/scaffolding/model/property/DomainPropertyImpl.groovy new file mode 100644 index 00000000..b2742803 --- /dev/null +++ b/src/main/groovy/org/grails/scaffolding/model/property/DomainPropertyImpl.groovy @@ -0,0 +1,139 @@ +package org.grails.scaffolding.model.property + +import grails.gorm.validation.PersistentEntityValidator +import grails.util.GrailsNameUtils +import groovy.transform.CompileStatic +import org.grails.datastore.mapping.config.Property +import org.grails.datastore.mapping.model.MappingContext +import org.grails.datastore.mapping.model.PersistentEntity +import org.grails.datastore.mapping.model.PersistentProperty +import org.grails.datastore.mapping.model.types.Association +import org.grails.datastore.mapping.model.types.Basic +import org.springframework.validation.Validator +import static grails.gorm.validation.ConstrainedProperty.BLANK_CONSTRAINT + +/** + * @see {@link DomainProperty} + * @author James Kleeh + */ +@CompileStatic +class DomainPropertyImpl implements DomainProperty { + + @Delegate PersistentProperty persistentProperty + PersistentProperty rootProperty + PersistentEntity domainClass + Constrained constrained + String pathFromRoot + + protected Boolean convertEmptyStringsToNull + protected Boolean trimStrings + + DomainPropertyImpl(PersistentProperty persistentProperty, MappingContext mappingContext) { + this.persistentProperty = persistentProperty + this.domainClass = persistentProperty.owner + Validator validator = mappingContext.getEntityValidator(domainClass) + if (validator instanceof PersistentEntityValidator) { + this.constrained = new Constrained(((PersistentEntityValidator)validator).constrainedProperties.get(name)) + } + if (this.constrained?.isNull()) { + this.constrained = null + } + + this.pathFromRoot = persistentProperty.name + } + + DomainPropertyImpl(PersistentProperty rootProperty, PersistentProperty persistentProperty, MappingContext mappingContext) { + this(persistentProperty, mappingContext) + this.setRootProperty(rootProperty) + } + + void setRootProperty(PersistentProperty rootProperty) { + this.rootProperty = rootProperty + this.pathFromRoot = "${rootProperty.name}.${name}" + } + + Class getRootBeanType() { + (rootProperty ?: persistentProperty).owner.javaClass + } + + Class getBeanType() { + owner.javaClass + } + + Class getAssociatedType() { + if (persistentProperty instanceof Association) { + if (persistentProperty instanceof Basic) { + ((Basic)persistentProperty).componentType + } else { + associatedEntity.javaClass + } + } else { + null + } + } + + PersistentEntity getAssociatedEntity() { + ((Association)persistentProperty).associatedEntity + } + + boolean isRequired() { + if (type in [Boolean, boolean]) { + false + } else if (type == String) { + // if the property prohibits nulls and blanks are converted to nulls, then blanks will be prohibited even if a blank + // constraint does not exist + boolean hasBlankConstraint = constrained?.hasAppliedConstraint(BLANK_CONSTRAINT) + boolean blanksImplicityProhibited = !hasBlankConstraint && !constrained?.nullable && convertEmptyStringsToNull && trimStrings + !constrained?.nullable && (!constrained?.blank || blanksImplicityProhibited) + } else { + !constrained?.nullable + } + } + + List getLabelKeys() { + List labelKeys = [] + labelKeys.add("${GrailsNameUtils.getPropertyName(beanType.simpleName)}.${name}.label".toString()) + if (rootProperty) { + labelKeys.add("${GrailsNameUtils.getPropertyName(rootBeanType.simpleName)}.${pathFromRoot}.label".replaceAll(/\[(.+)\]/, '').toString()) + } + labelKeys.unique() + } + + String getDefaultLabel() { + GrailsNameUtils.getNaturalName(name) + } + + int compareTo(DomainProperty o2) { + + if (domainClass.mapping.identifier?.identifierName?.contains(name)) { + return -1 + } + if (domainClass.mapping.identifier?.identifierName?.contains(o2.name)) { + return 1 + } + + Constrained cp2 = o2.constrained + + if (constrained == null && cp2 == null) { + return name.compareTo(o2.name) + } + + if (constrained == null) { + return 1 + } + + if (cp2 == null) { + return -1 + } + + if (constrained.order > cp2.order) { + return 1 + } + + if (constrained.order < cp2.order) { + return -1 + } + + return 0 + } +} diff --git a/src/main/groovy/org/grails/scaffolding/registry/DomainInputRenderer.groovy b/src/main/groovy/org/grails/scaffolding/registry/DomainInputRenderer.groovy new file mode 100644 index 00000000..c896741b --- /dev/null +++ b/src/main/groovy/org/grails/scaffolding/registry/DomainInputRenderer.groovy @@ -0,0 +1,21 @@ +package org.grails.scaffolding.registry + +import org.grails.scaffolding.model.property.DomainProperty + +/** + * Used to render a single domain class property on a form + * + * @author James Kleeh + */ +interface DomainInputRenderer extends DomainRenderer { + + /** + * Defines how a given domain class property will be rendered in the context of a form + * + * @param defaultAttributes The default html element attributes + * @param property The domain property to be rendered + * @return The closure to be passed to an instance of {@link groovy.xml.MarkupBuilder} + */ + Closure renderInput(Map defaultAttributes, DomainProperty property) + +} diff --git a/src/main/groovy/org/grails/scaffolding/registry/DomainInputRendererRegistry.groovy b/src/main/groovy/org/grails/scaffolding/registry/DomainInputRendererRegistry.groovy new file mode 100644 index 00000000..68a7b6a0 --- /dev/null +++ b/src/main/groovy/org/grails/scaffolding/registry/DomainInputRendererRegistry.groovy @@ -0,0 +1,13 @@ +package org.grails.scaffolding.registry + +import groovy.transform.CompileStatic + +/** + * A registry of {@link DomainInputRenderer} instances + * + * @author James Kleeh + */ +@CompileStatic +class DomainInputRendererRegistry extends DomainRendererRegistry { + +} \ No newline at end of file diff --git a/src/main/groovy/org/grails/scaffolding/registry/DomainOutputRenderer.groovy b/src/main/groovy/org/grails/scaffolding/registry/DomainOutputRenderer.groovy new file mode 100644 index 00000000..db2ce0e2 --- /dev/null +++ b/src/main/groovy/org/grails/scaffolding/registry/DomainOutputRenderer.groovy @@ -0,0 +1,27 @@ +package org.grails.scaffolding.registry + +import org.grails.scaffolding.model.property.DomainProperty + +/** + * Used to render markup that represents how single domain class property will be displayed + * + * @author James Kleeh + */ +interface DomainOutputRenderer extends DomainRenderer { + + /** + * Defines how a given domain class property will be rendered in the context of a list of domains class instances + * + * @param property The domain property to be rendered + * @return The closure to be passed to an instance of {@link groovy.xml.MarkupBuilder} + */ + Closure renderListOutput(DomainProperty property) + + /** + * Defines how a given domain class property will be rendered in the context of a single domain class instance + * + * @param property The domain property to be rendered + * @return The closure to be passed to an instance of {@link groovy.xml.MarkupBuilder} + */ + Closure renderOutput(DomainProperty property) +} diff --git a/src/main/groovy/org/grails/scaffolding/registry/DomainOutputRendererRegistry.groovy b/src/main/groovy/org/grails/scaffolding/registry/DomainOutputRendererRegistry.groovy new file mode 100644 index 00000000..43f885ce --- /dev/null +++ b/src/main/groovy/org/grails/scaffolding/registry/DomainOutputRendererRegistry.groovy @@ -0,0 +1,13 @@ +package org.grails.scaffolding.registry + +import groovy.transform.CompileStatic + +/** + * A registry of {@link DomainOutputRenderer} instances + * + * @author James Kleeh + */ +@CompileStatic +class DomainOutputRendererRegistry extends DomainRendererRegistry { + +} diff --git a/src/main/groovy/org/grails/scaffolding/registry/DomainRenderer.groovy b/src/main/groovy/org/grails/scaffolding/registry/DomainRenderer.groovy new file mode 100644 index 00000000..bcb7729d --- /dev/null +++ b/src/main/groovy/org/grails/scaffolding/registry/DomainRenderer.groovy @@ -0,0 +1,20 @@ +package org.grails.scaffolding.registry + +import org.grails.scaffolding.model.property.DomainProperty + +/** + * Used to render markup for a domain class property + * + * @author James Kleeh + */ +interface DomainRenderer { + + /** + * Determines if the renderer supports rendering the given property + * + * @param property The domain property to be rendered + * @return Whether or not the property is supported + */ + boolean supports(DomainProperty property) + +} \ No newline at end of file diff --git a/src/main/groovy/org/grails/scaffolding/registry/DomainRendererRegisterer.groovy b/src/main/groovy/org/grails/scaffolding/registry/DomainRendererRegisterer.groovy new file mode 100644 index 00000000..584492bd --- /dev/null +++ b/src/main/groovy/org/grails/scaffolding/registry/DomainRendererRegisterer.groovy @@ -0,0 +1,48 @@ +package org.grails.scaffolding.registry + +import org.grails.scaffolding.registry.input.* +import org.grails.scaffolding.registry.output.DefaultOutputRenderer +import grails.web.mapping.LinkGenerator +import groovy.transform.CompileStatic +import org.springframework.beans.factory.annotation.Autowired +import jakarta.annotation.PostConstruct + +/** + * Bean for registering the default domain renderers + * + * @author James Kleeh + */ +@CompileStatic +class DomainRendererRegisterer { + + @Autowired + DomainInputRendererRegistry domainInputRendererRegistry + + @Autowired + DomainOutputRendererRegistry domainOutputRendererRegistry + + @Autowired + LinkGenerator grailsLinkGenerator + + @PostConstruct + void registerRenderers() { + domainInputRendererRegistry.registerDomainRenderer(new DefaultInputRenderer(), -3) + domainInputRendererRegistry.registerDomainRenderer(new UrlInputRenderer(), -1) + domainInputRendererRegistry.registerDomainRenderer(new TimeZoneInputRenderer(), -1) + domainInputRendererRegistry.registerDomainRenderer(new TimeInputRenderer(), -1) + domainInputRendererRegistry.registerDomainRenderer(new StringInputRenderer(), -2) + domainInputRendererRegistry.registerDomainRenderer(new TextareaInputRenderer(), -2) + domainInputRendererRegistry.registerDomainRenderer(new NumberInputRenderer(), -2) + domainInputRendererRegistry.registerDomainRenderer(new LocaleInputRenderer(), -1) + domainInputRendererRegistry.registerDomainRenderer(new InListInputRenderer(), -1) + domainInputRendererRegistry.registerDomainRenderer(new FileInputRenderer(), -1) + domainInputRendererRegistry.registerDomainRenderer(new EnumInputRenderer(), -1) + domainInputRendererRegistry.registerDomainRenderer(new DateInputRenderer(), -1) + domainInputRendererRegistry.registerDomainRenderer(new CurrencyInputRenderer(), -1) + domainInputRendererRegistry.registerDomainRenderer(new BooleanInputRenderer(), -1) + domainInputRendererRegistry.registerDomainRenderer(new BidirectionalToManyInputRenderer(grailsLinkGenerator), -1) + domainInputRendererRegistry.registerDomainRenderer(new AssociationInputRenderer(), -2) + + domainOutputRendererRegistry.registerDomainRenderer(new DefaultOutputRenderer(), -1) + } +} diff --git a/src/main/groovy/org/grails/scaffolding/registry/DomainRendererRegistry.groovy b/src/main/groovy/org/grails/scaffolding/registry/DomainRendererRegistry.groovy new file mode 100644 index 00000000..c860b2a0 --- /dev/null +++ b/src/main/groovy/org/grails/scaffolding/registry/DomainRendererRegistry.groovy @@ -0,0 +1,52 @@ +package org.grails.scaffolding.registry + +import org.grails.scaffolding.model.property.DomainProperty +import groovy.transform.CompileStatic + +import java.util.concurrent.atomic.AtomicInteger + +/** + * A registry of domain property renderers sorted by priority and order of addition + * + * @author James Kleeh + */ +@CompileStatic +abstract class DomainRendererRegistry { + + protected SortedSet domainRegistryEntries = new TreeSet(); + + protected final AtomicInteger RENDERER_SEQUENCE = new AtomicInteger(0); + + void registerDomainRenderer(T domainRenderer, Integer priority) { + domainRegistryEntries.add(new Entry(domainRenderer, priority)) + } + + public SortedSet getDomainRegistryEntries() { + this.domainRegistryEntries + } + + T get(DomainProperty domainProperty) { + for (Entry entry : domainRegistryEntries) { + if (entry.renderer.supports(domainProperty)) { + return entry.renderer + } + } + null + } + + private class Entry implements Comparable { + protected final T renderer + private final int priority; + private final int seq; + + Entry(T renderer, int priority) { + this.renderer = renderer + this.priority = priority + seq = RENDERER_SEQUENCE.incrementAndGet() + } + + public int compareTo(Entry entry) { + return priority == entry.priority ? entry.seq - seq : entry.priority - priority; + } + } +} diff --git a/src/main/groovy/org/grails/scaffolding/registry/input/AssociationInputRenderer.groovy b/src/main/groovy/org/grails/scaffolding/registry/input/AssociationInputRenderer.groovy new file mode 100644 index 00000000..3d225f6c --- /dev/null +++ b/src/main/groovy/org/grails/scaffolding/registry/input/AssociationInputRenderer.groovy @@ -0,0 +1,23 @@ +package org.grails.scaffolding.registry.input + +import org.grails.scaffolding.model.property.DomainProperty +import org.grails.scaffolding.registry.DomainInputRenderer +import org.grails.datastore.mapping.model.types.Association + +/** + * The default renderer for rendering associations + * + * @author James Kleeh + */ +class AssociationInputRenderer implements DomainInputRenderer { + + @Override + boolean supports(DomainProperty property) { + property.persistentProperty instanceof Association + } + + @Override + Closure renderInput(Map defaultAttributes, DomainProperty property) { + { -> } + } +} diff --git a/src/main/groovy/org/grails/scaffolding/registry/input/BidirectionalToManyInputRenderer.groovy b/src/main/groovy/org/grails/scaffolding/registry/input/BidirectionalToManyInputRenderer.groovy new file mode 100644 index 00000000..0ea2a08c --- /dev/null +++ b/src/main/groovy/org/grails/scaffolding/registry/input/BidirectionalToManyInputRenderer.groovy @@ -0,0 +1,47 @@ +package org.grails.scaffolding.registry.input + +import org.grails.scaffolding.model.property.DomainProperty +import org.grails.scaffolding.registry.DomainInputRenderer +import grails.util.GrailsNameUtils +import grails.web.mapping.LinkGenerator +import org.grails.datastore.mapping.model.PersistentProperty +import org.grails.datastore.mapping.model.types.ToMany + +/** + * The default renderer for rendering bidirectional to many associations + * + * @author James Kleeh + */ +class BidirectionalToManyInputRenderer implements DomainInputRenderer { + + protected LinkGenerator linkGenerator + + BidirectionalToManyInputRenderer(LinkGenerator linkGenerator) { + this.linkGenerator = linkGenerator + } + + @Override + boolean supports(DomainProperty property) { + PersistentProperty persistentProperty = property.persistentProperty + persistentProperty instanceof ToMany && persistentProperty.bidirectional + } + + protected String getPropertyName(DomainProperty property) { + GrailsNameUtils.getPropertyName(property.rootBeanType) + } + + protected String getAssociatedClassName(DomainProperty property) { + property.associatedType.simpleName + } + + @Override + Closure renderInput(Map defaultAttributes, DomainProperty property) { + final String objectName = "${getPropertyName(property)}.id" + defaultAttributes.remove('required') + defaultAttributes.remove('readonly') + defaultAttributes.href = linkGenerator.link(resource: property.associatedType, action: "create", params: [(objectName): ""]) + return { -> + a("Add ${getAssociatedClassName(property)}", defaultAttributes) + } + } +} diff --git a/src/main/groovy/org/grails/scaffolding/registry/input/BooleanInputRenderer.groovy b/src/main/groovy/org/grails/scaffolding/registry/input/BooleanInputRenderer.groovy new file mode 100644 index 00000000..1e9389f0 --- /dev/null +++ b/src/main/groovy/org/grails/scaffolding/registry/input/BooleanInputRenderer.groovy @@ -0,0 +1,25 @@ +package org.grails.scaffolding.registry.input + +import org.grails.scaffolding.model.property.DomainProperty +import org.grails.scaffolding.registry.DomainInputRenderer + +/** + * The default renderer for rendering boolean properties + * + * @author James Kleeh + */ +class BooleanInputRenderer implements DomainInputRenderer { + + @Override + boolean supports(DomainProperty domainProperty) { + domainProperty.type in [boolean, Boolean] + } + + @Override + Closure renderInput(Map standardAttributes, DomainProperty domainProperty) { + standardAttributes.type = "checkbox" + return { -> + input(standardAttributes) + } + } +} diff --git a/src/main/groovy/org/grails/scaffolding/registry/input/CurrencyInputRenderer.groovy b/src/main/groovy/org/grails/scaffolding/registry/input/CurrencyInputRenderer.groovy new file mode 100644 index 00000000..cfe4e2bf --- /dev/null +++ b/src/main/groovy/org/grails/scaffolding/registry/input/CurrencyInputRenderer.groovy @@ -0,0 +1,44 @@ +package org.grails.scaffolding.registry.input + +import org.grails.scaffolding.model.property.DomainProperty +import org.grails.scaffolding.registry.DomainInputRenderer +import groovy.transform.CompileStatic + +/** + * The default renderer for rendering {@link Currency} properties + * + * @author James Kleeh + */ +@CompileStatic +class CurrencyInputRenderer implements MapToSelectInputRenderer { + + String getOptionValue(Currency currency) { + currency.currencyCode + } + + String getOptionKey(Currency currency) { + currency.currencyCode + } + + protected List getDefaultCurrencyCodes() { + ['EUR', 'XCD', 'USD', 'XOF', 'NOK', 'AUD', + 'XAF', 'NZD', 'MAD', 'DKK', 'GBP', 'CHF', + 'XPF', 'ILS', 'ROL', 'TRL'] + } + + Map getOptions() { + defaultCurrencyCodes.collectEntries { + Currency currency = Currency.getInstance(it) + [(getOptionKey(currency)): getOptionValue(currency)] + } + } + + Currency getDefaultOption() { + Currency.getInstance(Locale.default) + } + + @Override + boolean supports(DomainProperty property) { + property.type in Currency + } +} diff --git a/src/main/groovy/org/grails/scaffolding/registry/input/DateInputRenderer.groovy b/src/main/groovy/org/grails/scaffolding/registry/input/DateInputRenderer.groovy new file mode 100644 index 00000000..6dc167e0 --- /dev/null +++ b/src/main/groovy/org/grails/scaffolding/registry/input/DateInputRenderer.groovy @@ -0,0 +1,26 @@ +package org.grails.scaffolding.registry.input + +import org.grails.scaffolding.model.property.DomainProperty +import org.grails.scaffolding.registry.DomainInputRenderer + +/** + * The default renderer for rendering date properties + * + * @author James Kleeh + */ +class DateInputRenderer implements DomainInputRenderer { + + @Override + boolean supports(DomainProperty property) { + property.type in [Date, Calendar, java.sql.Date] + } + + @Override + Closure renderInput(Map defaultAttributes, DomainProperty property) { + defaultAttributes.type = "date" + defaultAttributes.placeholder = "YYYY-MM-DD" + return { -> + input(defaultAttributes) + } + } +} diff --git a/src/main/groovy/org/grails/scaffolding/registry/input/DefaultInputRenderer.groovy b/src/main/groovy/org/grails/scaffolding/registry/input/DefaultInputRenderer.groovy new file mode 100644 index 00000000..dda16f83 --- /dev/null +++ b/src/main/groovy/org/grails/scaffolding/registry/input/DefaultInputRenderer.groovy @@ -0,0 +1,24 @@ +package org.grails.scaffolding.registry.input + +import org.grails.scaffolding.model.property.DomainProperty +import org.grails.scaffolding.registry.DomainInputRenderer + +/** + * The renderer chosen for outputting domain properties when no other + * renderers support the given property + * + * @author James Kleeh + */ +class DefaultInputRenderer implements DomainInputRenderer { + + @Override + boolean supports(DomainProperty property) { + true + } + + @Override + Closure renderInput(Map attributes, DomainProperty property) { + { -> } + } + +} diff --git a/src/main/groovy/org/grails/scaffolding/registry/input/EnumInputRenderer.groovy b/src/main/groovy/org/grails/scaffolding/registry/input/EnumInputRenderer.groovy new file mode 100644 index 00000000..26f1f540 --- /dev/null +++ b/src/main/groovy/org/grails/scaffolding/registry/input/EnumInputRenderer.groovy @@ -0,0 +1,40 @@ +package org.grails.scaffolding.registry.input + +import org.grails.scaffolding.model.property.DomainProperty +import org.grails.scaffolding.registry.DomainInputRenderer + +/** + * The default renderer for rendering enum properties + * + * @author James Kleeh + */ +class EnumInputRenderer implements DomainInputRenderer { + + protected List getEnumValues(DomainProperty property) { + List enumList = [] + List keys = property.type.values()*.name() + List values = property.type.values() + keys.eachWithIndex { k, i -> + enumList.add([id: k, name: values[i].toString()]) + } + enumList + } + + @Override + boolean supports(DomainProperty property) { + property.type.isEnum() + } + + @Override + Closure renderInput(Map defaultAttributes, DomainProperty property) { + List enumList = getEnumValues(property) + + return { -> + select(defaultAttributes) { + enumList.each { + option(it.name, [value: it.id]) + } + } + } + } +} diff --git a/src/main/groovy/org/grails/scaffolding/registry/input/FileInputRenderer.groovy b/src/main/groovy/org/grails/scaffolding/registry/input/FileInputRenderer.groovy new file mode 100644 index 00000000..903f8903 --- /dev/null +++ b/src/main/groovy/org/grails/scaffolding/registry/input/FileInputRenderer.groovy @@ -0,0 +1,27 @@ +package org.grails.scaffolding.registry.input + +import org.grails.scaffolding.model.property.DomainProperty +import org.grails.scaffolding.registry.DomainInputRenderer + +import java.sql.Blob + +/** + * The default renderer for rendering byte[] or Blob properties + * + * @author James Kleeh + */ +class FileInputRenderer implements DomainInputRenderer { + + @Override + boolean supports(DomainProperty property) { + property.type in [byte[], Byte[], Blob] + } + + @Override + Closure renderInput(Map defaultAttributes, DomainProperty property) { + defaultAttributes.type = "file" + return { -> + input(defaultAttributes) + } + } +} diff --git a/src/main/groovy/org/grails/scaffolding/registry/input/InListInputRenderer.groovy b/src/main/groovy/org/grails/scaffolding/registry/input/InListInputRenderer.groovy new file mode 100644 index 00000000..d8f6f803 --- /dev/null +++ b/src/main/groovy/org/grails/scaffolding/registry/input/InListInputRenderer.groovy @@ -0,0 +1,34 @@ +package org.grails.scaffolding.registry.input + +import groovy.transform.CompileStatic +import groovy.transform.TypeCheckingMode +import org.grails.scaffolding.model.property.DomainProperty +import org.grails.scaffolding.registry.DomainInputRenderer + +/** + * The default renderer for rendering properties with an inList constraint + * + * @author James Kleeh + */ +@CompileStatic +class InListInputRenderer implements DomainInputRenderer { + + @Override + boolean supports(DomainProperty domainProperty) { + domainProperty.constrained?.inList + } + + @Override + @CompileStatic(TypeCheckingMode.SKIP) + Closure renderInput(Map standardAttributes, DomainProperty domainProperty) { + List inList = domainProperty.constrained?.inList + + return { -> + select(standardAttributes) { + inList.each { + option(it, [value: it]) + } + } + } + } +} diff --git a/src/main/groovy/org/grails/scaffolding/registry/input/LocaleInputRenderer.groovy b/src/main/groovy/org/grails/scaffolding/registry/input/LocaleInputRenderer.groovy new file mode 100644 index 00000000..d4d56299 --- /dev/null +++ b/src/main/groovy/org/grails/scaffolding/registry/input/LocaleInputRenderer.groovy @@ -0,0 +1,42 @@ +package org.grails.scaffolding.registry.input + +import org.grails.scaffolding.model.property.DomainProperty +import org.grails.scaffolding.registry.DomainInputRenderer +import groovy.transform.CompileStatic + +/** + * The default renderer for rendering {@link Locale} properties + * + * @author James Kleeh + */ +@CompileStatic +class LocaleInputRenderer implements MapToSelectInputRenderer { + + String getOptionValue(Locale locale) { + locale.country ? "${locale.language}, ${locale.country}, ${locale.displayName}" : "${locale.language}, ${locale.displayName}" + } + + String getOptionKey(Locale locale) { + locale.country ? "${locale.language}_${locale.country}" : locale.language + } + + Map getOptions() { + Locale.availableLocales.collectEntries { + if (it.country || it.language) { + [(getOptionKey(it)): getOptionValue(it)] + } else { + [:] + } + } + } + + Locale getDefaultOption() { + Locale.default + } + + @Override + boolean supports(DomainProperty property) { + property.type in Locale + } + +} diff --git a/src/main/groovy/org/grails/scaffolding/registry/input/MapToSelectInputRenderer.groovy b/src/main/groovy/org/grails/scaffolding/registry/input/MapToSelectInputRenderer.groovy new file mode 100644 index 00000000..06dd7242 --- /dev/null +++ b/src/main/groovy/org/grails/scaffolding/registry/input/MapToSelectInputRenderer.groovy @@ -0,0 +1,60 @@ +package org.grails.scaffolding.registry.input + +import org.grails.scaffolding.model.property.DomainProperty +import org.grails.scaffolding.registry.DomainInputRenderer + +/** + * A class to easily render a select element based on a given set + * of options and a default option + * + * @author James Kleeh + * @param Any type + */ +trait MapToSelectInputRenderer implements DomainInputRenderer { + + /** + * Defines how a given should be displayed in a select element + * + * @param t An instance of T + * @return The inner text of an option element + */ + abstract String getOptionValue(T t) + + /** + * Defines how a given should be uniquely identified in a select element + * + * @param t An instance of T + * @return The value attribute of an option element + */ + abstract String getOptionKey(T t) + + /** + * @return The default to be selected in the select element + */ + abstract T getDefaultOption() + + /** + * Builds the options to be displayed + * + * @return The map of options where the key will be to the option value and value will be the option text + */ + abstract Map getOptions() + + /** @see DomainInputRenderer#renderInput() **/ + Closure renderInput(Map defaultAttributes, DomainProperty property) { + String selected = getOptionKey(defaultOption) + + return { -> + select(defaultAttributes) { + options.each { String key, String value -> + Map attrs = [value: key] + if (selected == key) { + attrs.selected = "" + } + option(value, attrs) + } + } + } + } + +} \ No newline at end of file diff --git a/src/main/groovy/org/grails/scaffolding/registry/input/NumberInputRenderer.groovy b/src/main/groovy/org/grails/scaffolding/registry/input/NumberInputRenderer.groovy new file mode 100644 index 00000000..f892a5ee --- /dev/null +++ b/src/main/groovy/org/grails/scaffolding/registry/input/NumberInputRenderer.groovy @@ -0,0 +1,55 @@ +package org.grails.scaffolding.registry.input + +import groovy.transform.CompileStatic +import groovy.transform.TypeCheckingMode +import org.grails.scaffolding.model.property.Constrained +import org.grails.scaffolding.model.property.DomainProperty +import org.grails.scaffolding.registry.DomainInputRenderer + +/** + * The default renderer for rendering {@link Number} or primitive properties + * + * @author James Kleeh + */ +@CompileStatic +class NumberInputRenderer implements DomainInputRenderer { + + @Override + boolean supports(DomainProperty domainProperty) { + Class type = domainProperty.type + type.isPrimitive() || type in Number + } + + @Override + @CompileStatic(TypeCheckingMode.SKIP) + Closure renderInput(Map attributes, DomainProperty property) { + Constrained constraints = property.constrained + Range range = constraints?.range + if (range) { + attributes.type = "range" + attributes.min = range.from + attributes.max = range.to + } else { + String typeName = property.type.simpleName.toLowerCase() + + attributes.type = "number" + + if(typeName in ['double', 'float', 'bigdecimal']) { + attributes.step = "any" + } + if (constraints?.scale != null) { + attributes.step = "0.${'0' * (constraints.scale - 1)}1" + } + if (constraints?.min != null) { + attributes.min = constraints.min + } + if (constraints?.max != null) { + attributes.max = constraints.max + } + } + + return { -> + input(attributes) + } + } +} diff --git a/src/main/groovy/org/grails/scaffolding/registry/input/StringInputRenderer.groovy b/src/main/groovy/org/grails/scaffolding/registry/input/StringInputRenderer.groovy new file mode 100644 index 00000000..3d0d0064 --- /dev/null +++ b/src/main/groovy/org/grails/scaffolding/registry/input/StringInputRenderer.groovy @@ -0,0 +1,47 @@ +package org.grails.scaffolding.registry.input + +import groovy.transform.CompileStatic +import groovy.transform.TypeCheckingMode +import org.grails.scaffolding.model.property.Constrained +import org.grails.scaffolding.model.property.DomainProperty +import org.grails.scaffolding.registry.DomainInputRenderer + +/** + * The default renderer for rendering {@link String} properties + * + * @author James Kleeh + */ +@CompileStatic +class StringInputRenderer implements DomainInputRenderer { + + @Override + boolean supports(DomainProperty domainProperty) { + domainProperty.type in [String, null] + } + + @Override + @CompileStatic(TypeCheckingMode.SKIP) + Closure renderInput(Map standardAttributes, DomainProperty domainProperty) { + Constrained constraints = domainProperty.constrained + if (constraints?.password) { + standardAttributes.type = "password" + } else if (constraints?.email) { + standardAttributes.type = "email" + } else if (constraints?.url) { + standardAttributes.type = "url" + } else { + standardAttributes.type = "text" + } + + if (constraints?.matches) { + standardAttributes.pattern = constraints.matches + } + if (constraints?.maxSize) { + standardAttributes.maxlength = constraints.maxSize + } + + return { -> + input(standardAttributes) + } + } +} diff --git a/src/main/groovy/org/grails/scaffolding/registry/input/TextareaInputRenderer.groovy b/src/main/groovy/org/grails/scaffolding/registry/input/TextareaInputRenderer.groovy new file mode 100644 index 00000000..4c78bf83 --- /dev/null +++ b/src/main/groovy/org/grails/scaffolding/registry/input/TextareaInputRenderer.groovy @@ -0,0 +1,32 @@ +package org.grails.scaffolding.registry.input + +import groovy.transform.CompileStatic +import groovy.transform.TypeCheckingMode +import org.grails.scaffolding.model.property.DomainProperty +import org.grails.scaffolding.registry.DomainInputRenderer + +/** + * The default renderer for rendering properties with the constraint {@code [widget: "textarea"]} + * + * @author James Kleeh + */ +@CompileStatic +class TextareaInputRenderer implements DomainInputRenderer { + + @Override + boolean supports(DomainProperty domainProperty) { + domainProperty.constrained?.widget == "textarea" + } + + @Override + @CompileStatic(TypeCheckingMode.SKIP) + Closure renderInput(Map defaultAttributes, DomainProperty domainProperty) { + Integer maxSize = domainProperty.constrained?.maxSize + if (maxSize) { + defaultAttributes.maxlength = maxSize + } + return { -> + textarea(defaultAttributes) + } + } +} diff --git a/src/main/groovy/org/grails/scaffolding/registry/input/TimeInputRenderer.groovy b/src/main/groovy/org/grails/scaffolding/registry/input/TimeInputRenderer.groovy new file mode 100644 index 00000000..3e9d3b52 --- /dev/null +++ b/src/main/groovy/org/grails/scaffolding/registry/input/TimeInputRenderer.groovy @@ -0,0 +1,25 @@ +package org.grails.scaffolding.registry.input + +import org.grails.scaffolding.model.property.DomainProperty +import org.grails.scaffolding.registry.DomainInputRenderer + +/** + * The default renderer for rendering {@link java.sql.Time} properties + * + * @author James Kleeh + */ +class TimeInputRenderer implements DomainInputRenderer { + + @Override + boolean supports(DomainProperty property) { + property.type in java.sql.Time + } + + @Override + Closure renderInput(Map defaultAttributes, DomainProperty property) { + defaultAttributes.type = "datetime-local" + return { -> + input(defaultAttributes) + } + } +} diff --git a/src/main/groovy/org/grails/scaffolding/registry/input/TimeZoneInputRenderer.groovy b/src/main/groovy/org/grails/scaffolding/registry/input/TimeZoneInputRenderer.groovy new file mode 100644 index 00000000..49a70267 --- /dev/null +++ b/src/main/groovy/org/grails/scaffolding/registry/input/TimeZoneInputRenderer.groovy @@ -0,0 +1,48 @@ +package org.grails.scaffolding.registry.input + +import org.grails.scaffolding.model.property.DomainProperty +import org.grails.scaffolding.registry.DomainInputRenderer +import groovy.transform.CompileStatic + +/** + * The default renderer for rendering {@link TimeZone} properties + * + * @author James Kleeh + */ +@CompileStatic +class TimeZoneInputRenderer implements MapToSelectInputRenderer { + + String getOptionValue(TimeZone timeZone) { + Date date = new Date() + String shortName = timeZone.getDisplayName(timeZone.inDaylightTime(date), TimeZone.SHORT) + String longName = timeZone.getDisplayName(timeZone.inDaylightTime(date), TimeZone.LONG) + + int offset = timeZone.rawOffset + BigDecimal hour = offset / (60 * 60 * 1000) + BigDecimal minute = offset / (60 * 1000) + double min = Math.abs(minute.toDouble()) % 60 + + "${shortName}, ${longName} ${hour}:${min} [${timeZone.ID}]" + } + + String getOptionKey(TimeZone timeZone) { + timeZone.ID + } + + Map getOptions() { + TimeZone.availableIDs.collectEntries { + TimeZone timeZone = TimeZone.getTimeZone(it) + [(getOptionKey(timeZone)): getOptionValue(timeZone)] + } + } + + TimeZone getDefaultOption() { + TimeZone.default + } + + @Override + boolean supports(DomainProperty property) { + property.type in TimeZone + } + +} diff --git a/src/main/groovy/org/grails/scaffolding/registry/input/UrlInputRenderer.groovy b/src/main/groovy/org/grails/scaffolding/registry/input/UrlInputRenderer.groovy new file mode 100644 index 00000000..a426b093 --- /dev/null +++ b/src/main/groovy/org/grails/scaffolding/registry/input/UrlInputRenderer.groovy @@ -0,0 +1,25 @@ +package org.grails.scaffolding.registry.input + +import org.grails.scaffolding.model.property.DomainProperty +import org.grails.scaffolding.registry.DomainInputRenderer + +/** + * The default renderer for rendering {@link URL} properties + * + * @author James Kleeh + */ +class UrlInputRenderer implements DomainInputRenderer { + + @Override + boolean supports(DomainProperty property) { + property.type in URL + } + + @Override + Closure renderInput(Map defaultAttributes, DomainProperty property) { + defaultAttributes.type = "url" + return { -> + input(defaultAttributes) + } + } +} diff --git a/src/main/groovy/org/grails/scaffolding/registry/output/DefaultOutputRenderer.groovy b/src/main/groovy/org/grails/scaffolding/registry/output/DefaultOutputRenderer.groovy new file mode 100644 index 00000000..4b29ba4d --- /dev/null +++ b/src/main/groovy/org/grails/scaffolding/registry/output/DefaultOutputRenderer.groovy @@ -0,0 +1,38 @@ +package org.grails.scaffolding.registry.output + +import org.grails.scaffolding.model.property.DomainProperty +import org.grails.scaffolding.registry.DomainOutputRenderer +import grails.util.GrailsNameUtils + +/** + * The renderer chosen for displaying domain properties when no other + * renderers support the given property + * + * @author James Kleeh + */ +class DefaultOutputRenderer implements DomainOutputRenderer { + + protected String buildPropertyPath(DomainProperty property) { + StringBuilder sb = new StringBuilder() + sb.append(GrailsNameUtils.getPropertyName(property.rootBeanType)).append('.') + sb.append(property.pathFromRoot) + sb.toString() + } + + @Override + boolean supports(DomainProperty property) { + true + } + + @Override + Closure renderListOutput(DomainProperty property) { + renderOutput(property) + } + + @Override + Closure renderOutput(DomainProperty property) { + { -> + span("\${${buildPropertyPath(property)}}") + } + } +} diff --git a/src/test/groovy/org/grails/scaffolding/ClosureCapture.groovy b/src/test/groovy/org/grails/scaffolding/ClosureCapture.groovy new file mode 100644 index 00000000..be7ad0ed --- /dev/null +++ b/src/test/groovy/org/grails/scaffolding/ClosureCapture.groovy @@ -0,0 +1,45 @@ +package org.grails.scaffolding + + +class ClosureCapture { + boolean stopOnException = true + private boolean hasException = false + private RootCall root = new RootCall(calls: []) + private RootCall current = root + List getCalls() { return root.calls } + def invokeMethod(String name, args) { + Call call = new Call(name: name, args: args, parent: current, calls: [], throwable: null) + current.calls << call + if(args && args[-1] instanceof Closure) { + RootCall previousCall = current + current = call + Closure c = args[-1] + def previousDelegate = c.delegate + c.delegate = this + try { + c.call() + } catch(Throwable t) { + call.throwable = t + hasException = true + if(stopOnException) { + throw t + } + } finally { + c.delegate = previousDelegate + } + current = previousCall + } + } + + private static class RootCall { + @Delegate + List calls + } + + private static class Call extends RootCall { + String name + def args + RootCall parent + Throwable throwable + } +} \ No newline at end of file diff --git a/src/test/groovy/org/grails/scaffolding/ClosureCaptureSpecification.groovy b/src/test/groovy/org/grails/scaffolding/ClosureCaptureSpecification.groovy new file mode 100644 index 00000000..52f576e9 --- /dev/null +++ b/src/test/groovy/org/grails/scaffolding/ClosureCaptureSpecification.groovy @@ -0,0 +1,18 @@ +package org.grails.scaffolding + +import spock.lang.Specification + +/** + * Created by Jim on 6/6/2016. + */ +abstract class ClosureCaptureSpecification extends Specification { + + protected ClosureCapture getClosureCapture(Closure closure) { + ClosureCapture closureCapture = new ClosureCapture() + closure.resolveStrategy = Closure.DELEGATE_FIRST + closure.delegate = closureCapture + closure.call() + closureCapture + } + +} diff --git a/src/test/groovy/org/grails/scaffolding/markup/ContextMarkupRendererSpec.groovy b/src/test/groovy/org/grails/scaffolding/markup/ContextMarkupRendererSpec.groovy new file mode 100644 index 00000000..f0c1b6ef --- /dev/null +++ b/src/test/groovy/org/grails/scaffolding/markup/ContextMarkupRendererSpec.groovy @@ -0,0 +1,224 @@ +package org.grails.scaffolding.markup + +import org.grails.scaffolding.ClosureCapture +import org.grails.scaffolding.ClosureCaptureSpecification +import org.grails.scaffolding.model.property.DomainProperty +import org.grails.datastore.mapping.model.PersistentEntity +import spock.lang.Ignore +import spock.lang.Shared +import spock.lang.Subject +import spock.lang.Specification + +@Subject(ContextMarkupRendererImpl) +class ContextMarkupRendererSpec extends ClosureCaptureSpecification { + + @Shared + ContextMarkupRendererImpl renderer + + void setup() { + renderer = new ContextMarkupRendererImpl() + } + + @Ignore + void "test listOutputContext"() { + given: + DomainProperty prop1 = Mock(DomainProperty) { + 1 * getDefaultLabel() >> "Prop 1" + 1 * getName() >> "prop1" + } + DomainProperty prop2 = Mock(DomainProperty) { + 1 * getDefaultLabel() >> "Prop 2" + 1 * getName() >> "prop2" + } + DomainProperty prop3 = Mock(DomainProperty) { + 1 * getDefaultLabel() >> "Prop 3" + 1 * getName() >> "prop3" + } + + when: + ClosureCapture closureCapture = getClosureCapture(renderer.listOutputContext(Mock(PersistentEntity), [prop1, prop2, prop3], { DomainProperty prop -> + prop.name + })) + + then: + closureCapture.calls[0].name == "table" + closureCapture.calls[0][0].name == "thead" + closureCapture.calls[0][0][0].name == "tr" + closureCapture.calls[0][0][0][0].name == "th" + closureCapture.calls[0][0][0][0].args[0] == "Prop 1" + closureCapture.calls[0][0][0][1].name == "th" + closureCapture.calls[0][0][0][1].args[0] == "Prop 2" + closureCapture.calls[0][0][0][2].name == "th" + closureCapture.calls[0][0][0][2].args[0] == "Prop 3" + closureCapture.calls[0][1].name == "tbody" + closureCapture.calls[0][1][0].name == "tr" + closureCapture.calls[0][1][0][0].name == "td" + closureCapture.calls[0][1][0][0].args[0] == "prop1" + closureCapture.calls[0][1][0][1].name == "td" + closureCapture.calls[0][1][0][1].args[0] == "prop2" + closureCapture.calls[0][1][0][2].name == "td" + closureCapture.calls[0][1][0][2].args[0] == "prop3" + } + + void "test inputContext (Domain)"() { + when: + ClosureCapture closureCapture = getClosureCapture(renderer.inputContext(Mock(PersistentEntity)) { -> + span("foo") + }) + + then: + closureCapture.calls[0].name == "fieldset" + closureCapture.calls[0].args[0] == ["class": "form"] + closureCapture.calls[0][0].name == "span" + closureCapture.calls[0][0].args[0] == "foo" + } + + @Ignore + void "test inputContext (Property) required"() { + given: + DomainProperty property = Mock(DomainProperty) { + 2 * isRequired() >> true + 1 * getPathFromRoot() >> "bar" + 1 * getLabelKeys() >> null + 1 * getDefaultLabel() >> "Bar" + } + + when: + ClosureCapture closureCapture = getClosureCapture(renderer.inputContext(property) { -> + input([type: "text"]) + }) + + then: + closureCapture.calls[0].name == "div" + closureCapture.calls[0].args[0] == ["class": "fieldcontain required"] + closureCapture.calls[0][0].name == "label" + closureCapture.calls[0][0].args[0] == ["for": "bar"] + closureCapture.calls[0][0].args[1] == "Bar" + closureCapture.calls[0][0][0].name == "span" + closureCapture.calls[0][0][0].args[0] == ["class": "required-indicator"] + closureCapture.calls[0][0][0].args[1] == "*" + closureCapture.calls[0][1].name == "input" + closureCapture.calls[0][1].args[0] == ["type": "text"] + } + + @Ignore + void "test inputContext (Property) not required"() { + given: + DomainProperty property = Mock(DomainProperty) { + 2 * isRequired() >> false + 1 * getPathFromRoot() >> "bar" + 1 * getLabelKeys() >> null + 1 * getDefaultLabel() >> "Bar" + } + + when: + ClosureCapture closureCapture = getClosureCapture(renderer.inputContext(property) { -> + input([type: "text"]) + }) + + then: + closureCapture.calls[0].name == "div" + closureCapture.calls[0].args[0] == ["class": "fieldcontain"] + closureCapture.calls[0][0].name == "label" + closureCapture.calls[0][0].args[0] == ["for": "bar"] + closureCapture.calls[0][0].args[1] == "Bar" + closureCapture.calls[0][1].name == "input" + closureCapture.calls[0][1].args[0] == ["type": "text"] + } + + void "test outputContext (Domain)"() { + given: + PersistentEntity domain = Mock(PersistentEntity) { + 1 * getDecapitalizedName() >> "foo" + } + + when: + ClosureCapture closureCapture = getClosureCapture(renderer.outputContext(domain) { -> + li("prop1") + li("prop2") + li("prop3") + }) + + then: + closureCapture.calls[0].name == "ol" + closureCapture.calls[0].args[0] == ["class": "property-list foo"] + closureCapture.calls[0][0].name == "li" + closureCapture.calls[0][0].args[0] == "prop1" + closureCapture.calls[0][1].name == "li" + closureCapture.calls[0][1].args[0] == "prop2" + closureCapture.calls[0][2].name == "li" + closureCapture.calls[0][2].args[0] == "prop3" + } + + @Ignore + void "test outputContext (Property)"() { + given: + DomainProperty property = Mock(DomainProperty) { + 2 * getPathFromRoot() >> "bar" + 1 * getLabelKeys() >> null + 1 * getDefaultLabel() >> "Bar" + } + + when: + ClosureCapture closureCapture = getClosureCapture(renderer.outputContext(property) { -> + span("x") + }) + + then: + closureCapture.calls[0].name == "li" + closureCapture.calls[0].args[0] == ["class": "fieldcontain"] + closureCapture.calls[0][0].name == "span" + closureCapture.calls[0][0].args[0] == ["id": "bar-label", class: "property-label"] + closureCapture.calls[0][0].args[1] == "Bar" + closureCapture.calls[0][1].name == "div" + closureCapture.calls[0][1].args[0] == ["class": "property-value", "aria-labelledby": "bar-label"] + closureCapture.calls[0][1][0].name == "span" + closureCapture.calls[0][1][0].args[0] == "x" + } + + @Ignore + void "test embeddedInputContext"() { + given: + DomainProperty property = Mock(DomainProperty) { + 1 * getType() >> TimeZone + 1 * getLabelKeys() >> null + 1 * getDefaultLabel() >> "Bar" + } + + when: + ClosureCapture closureCapture = getClosureCapture(renderer.embeddedInputContext(property) { -> + span("x") + }) + + then: + closureCapture.calls[0].name == "fieldset" + closureCapture.calls[0].args[0] == [class: "embedded timeZone"] + closureCapture.calls[0][0].name == "legend" + closureCapture.calls[0][0].args[0] == "Bar" + closureCapture.calls[0][1].name == "span" + closureCapture.calls[0][1].args[0] == "x" + } + + @Ignore + void "test embeddedOutputContext"() { + given: + DomainProperty property = Mock(DomainProperty) { + 1 * getType() >> TimeZone + 1 * getLabelKeys() >> null + 1 * getDefaultLabel() >> "Bar" + } + + when: + ClosureCapture closureCapture = getClosureCapture(renderer.embeddedOutputContext(property) { -> + span("x") + }) + + then: + closureCapture.calls[0].name == "fieldset" + closureCapture.calls[0].args[0] == [class: "embedded timeZone"] + closureCapture.calls[0][0].name == "legend" + closureCapture.calls[0][0].args[0] == "Bar" + closureCapture.calls[0][1].name == "span" + closureCapture.calls[0][1].args[0] == "x" + } +} diff --git a/src/test/groovy/org/grails/scaffolding/markup/DomainMarkupRendererSpec.groovy b/src/test/groovy/org/grails/scaffolding/markup/DomainMarkupRendererSpec.groovy new file mode 100644 index 00000000..b6cc5586 --- /dev/null +++ b/src/test/groovy/org/grails/scaffolding/markup/DomainMarkupRendererSpec.groovy @@ -0,0 +1,258 @@ +package org.grails.scaffolding.markup + +import grails.persistence.Entity +import org.grails.datastore.mapping.keyvalue.mapping.config.KeyValueMappingContext +import org.grails.datastore.mapping.model.MappingContext +import org.grails.scaffolding.model.DomainModelService +import org.grails.scaffolding.model.DomainModelServiceImpl +import org.grails.scaffolding.model.MocksDomain +import org.grails.scaffolding.model.property.DomainProperty +import org.grails.datastore.mapping.model.PersistentEntity +import org.grails.datastore.mapping.model.PersistentProperty +import org.grails.datastore.mapping.model.types.Embedded +import org.grails.scaffolding.model.property.DomainPropertyFactory +import org.grails.scaffolding.model.property.DomainPropertyFactoryImpl +import spock.lang.Shared +import spock.lang.Specification + +/** + * Created by Jim on 5/29/2016. + */ +class DomainMarkupRendererSpec extends Specification implements MocksDomain { + + @Shared + DomainMarkupRendererImpl renderer + + void setup() { + renderer = new DomainMarkupRendererImpl() + } + + void "test renderListOutput"() { + given: + PersistentEntity persistentEntity = Mock(PersistentEntity) + PersistentEntity embeddedEntity = Mock(PersistentEntity) + DomainProperty prop1 = Mock(DomainProperty) { + 1 * getName() >> "prop1" + 1 * getPersistentProperty() >> Mock(PersistentProperty) + } + DomainProperty prop2 = Mock(DomainProperty) { + 1 * getName() >> "prop2" + 1 * getPersistentProperty() >> Mock(PersistentProperty) + } + DomainProperty prop3 = Mock(DomainProperty) { + 0 * getName() >> "prop3" + 2 * getPersistentProperty() >> Mock(Embedded) { + 1 * getAssociatedEntity() >> embeddedEntity + } + } + DomainProperty prop4 = Mock(DomainProperty) { + 1 * getName() >> "prop4" + 1 * getPersistentProperty() >> Mock(PersistentProperty) + } + DomainProperty prop5 = Mock(DomainProperty) { + 1 * getName() >> "prop5" + 1 * getPersistentProperty() >> Mock(PersistentProperty) + } + DomainProperty prop6 = Mock(DomainProperty) { + 0 * getName() >> "prop6" + 1 * getPersistentProperty() >> Mock(PersistentProperty) + } + DomainProperty embeddedProp1 = Mock(DomainProperty) { + 1 * getName() >> "embeddedProp1" + 0 * getPersistentProperty() + } + DomainProperty embeddedProp2 = Mock(DomainProperty) { + 1 * getName() >> "embeddedProp2" + 0 * getPersistentProperty() + } + DomainProperty embeddedProp3 = Mock(DomainProperty) { + 1 * getName() >> "embeddedProp3" + 0 * getPersistentProperty() + } + List props = [prop1, prop2, prop3, prop4, prop5, prop6] + List embeddedProps = [embeddedProp1, embeddedProp2, embeddedProp3] + renderer.domainModelService = Mock(DomainModelService) { + 1 * getListOutputProperties(persistentEntity) >> props + 1 * getOutputProperties(embeddedEntity) >> embeddedProps + } + renderer.contextMarkupRenderer = Mock(ContextMarkupRenderer) { + 1 * listOutputContext(_ as PersistentEntity, [prop1, prop2, embeddedProp1, embeddedProp2, embeddedProp3, prop4, prop5], _ as Closure) >> { entity, properties, closure -> + return { -> + properties.each { DomainProperty prop -> + div(closure.call(prop)) + } + } + } + } + renderer.propertyMarkupRenderer = Mock(PropertyMarkupRenderer) { + 7 * renderListOutput(_ as DomainProperty) >> { DomainProperty prop -> + return { -> span(prop.name) } + } + } + + when: + String output = renderer.renderListOutput(persistentEntity) + + then: + output == ["prop1", "prop2", "embeddedProp1", "embeddedProp2", "embeddedProp3", "prop4", "prop5"].collect { + "

\n $it\n
" + }.join("\n") + } + + void "test renderListOutput (real domain)"() { + given: + MappingContext mappingContext = new KeyValueMappingContext("test") + PersistentEntity persistentEntity = mockDomainClass(mappingContext, MainDomain) + //mockDomainClass(mappingContext, EmbeddedDomain) + DomainPropertyFactory domainPropertyFactory = new DomainPropertyFactoryImpl(convertEmptyStringsToNull: true, trimStrings: true, grailsDomainClassMappingContext: mappingContext) + renderer.domainModelService = new DomainModelServiceImpl(domainPropertyFactory: domainPropertyFactory) + renderer.contextMarkupRenderer = Mock(ContextMarkupRenderer) { + 1 * listOutputContext(_ as PersistentEntity, _ as List, _ as Closure) >> { entity, properties, closure -> + return { -> + properties.each { DomainProperty prop -> + div(closure.call(prop)) + } + } + } + } + renderer.propertyMarkupRenderer = Mock(PropertyMarkupRenderer) { + 7 * renderListOutput(_ as DomainProperty) >> { DomainProperty prop -> + return { -> span(prop.name) } + } + } + + when: + String output = renderer.renderListOutput(persistentEntity) + + then: + output == ["id", "prop1", "prop2", "embeddedProp1", "embeddedProp2", "embeddedProp3", "prop4"].collect { + "
\n $it\n
" + }.join("\n") + } + + void "test renderForm"() { + given: + PersistentEntity domain = Mock(PersistentEntity) + PersistentEntity embedded = Mock(PersistentEntity) + DomainProperty prop1 = Mock(DomainProperty) { + 1 * getName() >> "prop1" + 1 * getPersistentProperty() >> Mock(PersistentProperty) + } + DomainProperty prop2 = Mock(DomainProperty) { + 2 * getPersistentProperty() >> Mock(Embedded) { + 1 * getAssociatedEntity() >> embedded + } + } + DomainProperty prop3 = Mock(DomainProperty) { + 1 * getName() >> "prop3" + } + renderer.domainModelService = Mock(DomainModelService) { + 1 * getInputProperties(domain) >> [prop1, prop2] + 1 * getInputProperties(embedded) >> [prop3] + } + renderer.contextMarkupRenderer = Mock(ContextMarkupRenderer) { + 2 * inputContext(_ as DomainProperty, _ as Closure) >> { DomainProperty prop, Closure c -> + return { -> + div(c) + } + } + 1 * inputContext(_ as PersistentEntity, _ as Closure) >> { PersistentEntity d, Closure c -> + return { -> + form(c) + } + } + 1 * embeddedInputContext(_ as DomainProperty, _ as Closure) >> { DomainProperty prop, Closure c -> + return { -> + fieldset(c) + } + } + } + renderer.propertyMarkupRenderer = Mock(PropertyMarkupRenderer) { + 2 * renderInput(_ as DomainProperty) >> { DomainProperty prop -> + return { -> span(prop.name) } + } + } + + when: + String output = renderer.renderInput(domain) + + then: + output == "
\n
\n prop1\n
\n
\n
\n prop3\n
\n
\n
" + } + + void "test renderOutput"() { + given: + PersistentEntity domain = Mock(PersistentEntity) + PersistentEntity embedded = Mock(PersistentEntity) + DomainProperty prop1 = Mock(DomainProperty) { + 1 * getName() >> "prop1" + 1 * getPersistentProperty() >> Mock(PersistentProperty) + } + DomainProperty prop2 = Mock(DomainProperty) { + 2 * getPersistentProperty() >> Mock(Embedded) { + 1 * getAssociatedEntity() >> embedded + } + } + DomainProperty prop3 = Mock(DomainProperty) { + 1 * getName() >> "prop3" + } + renderer.domainModelService = Mock(DomainModelService) { + 1 * getOutputProperties(domain) >> [prop1, prop2] + 1 * getOutputProperties(embedded) >> [prop3] + } + renderer.contextMarkupRenderer = Mock(ContextMarkupRenderer) { + 2 * outputContext(_ as DomainProperty, _ as Closure) >> { DomainProperty prop, Closure c -> + return { -> + div(c) + } + } + 1 * outputContext(_ as PersistentEntity, _ as Closure) >> { PersistentEntity d, Closure c -> + return { -> + form(c) + } + } + 1 * embeddedOutputContext(_ as DomainProperty, _ as Closure) >> { DomainProperty prop, Closure c -> + return { -> + fieldset(c) + } + } + } + renderer.propertyMarkupRenderer = Mock(PropertyMarkupRenderer) { + 2 * renderOutput(_ as DomainProperty) >> { DomainProperty prop -> + return { -> span(prop.name) } + } + } + + when: + String output = renderer.renderOutput(domain) + + then: + output == "
\n
\n prop1\n
\n
\n
\n prop3\n
\n
\n
" + } + + @Entity + class MainDomain { + String prop1 + String prop2 + EmbeddedDomain prop3 + String prop4 + String prop5 + String prop6 + + static embedded = ['prop3'] + static constraints = { + prop1(order: 1) + prop2(order: 2) + prop3(order: 3) + prop4(order: 4) + prop5(order: 5) + prop6(order: 6) + } + } + + class EmbeddedDomain { + String embeddedProp1 + String embeddedProp2 + String embeddedProp3 + } +} diff --git a/src/test/groovy/org/grails/scaffolding/markup/PropertyMarkupRendererSpec.groovy b/src/test/groovy/org/grails/scaffolding/markup/PropertyMarkupRendererSpec.groovy new file mode 100644 index 00000000..4b6bcc46 --- /dev/null +++ b/src/test/groovy/org/grails/scaffolding/markup/PropertyMarkupRendererSpec.groovy @@ -0,0 +1,118 @@ +package org.grails.scaffolding.markup + +import org.grails.scaffolding.model.property.Constrained +import org.grails.scaffolding.model.property.DomainProperty +import org.grails.scaffolding.registry.DomainInputRenderer +import org.grails.scaffolding.registry.DomainInputRendererRegistry +import org.grails.scaffolding.registry.DomainOutputRenderer +import org.grails.scaffolding.registry.DomainOutputRendererRegistry +import spock.lang.Shared +import spock.lang.Specification +import spock.lang.Subject + +/** + * Created by Jim on 6/7/2016. + */ +@Subject(PropertyMarkupRendererImpl) +class PropertyMarkupRendererSpec extends Specification { + + @Shared + PropertyMarkupRendererImpl renderer + + void setup() { + renderer = new PropertyMarkupRendererImpl() + } + + void "test renderListOutput"() { + given: + renderer.domainOutputRendererRegistry = Mock(DomainOutputRendererRegistry) + DomainProperty property = Mock(DomainProperty) + + when: + renderer.renderListOutput(property) + + then: + 1 * renderer.domainOutputRendererRegistry.get(property) >> Mock(DomainOutputRenderer) { + 1 * renderListOutput(property) + } + } + + void "test renderOutput"() { + given: + renderer.domainOutputRendererRegistry = Mock(DomainOutputRendererRegistry) + DomainProperty property = Mock(DomainProperty) + + when: + renderer.renderOutput(property) + + then: + 1 * renderer.domainOutputRendererRegistry.get(property) >> Mock(DomainOutputRenderer) { + 1 * renderOutput(property) + } + } + + void "test renderInput"() { + given: + renderer.domainInputRendererRegistry = Mock(DomainInputRendererRegistry) + DomainProperty property = Mock(DomainProperty) { + 1 * getPathFromRoot() >> "city" + 1 * isRequired() >> false + 1 * getConstrained() >> null + } + + when: + renderer.renderInput(property) + + then: + 1 * renderer.domainInputRendererRegistry.get(property) >> Mock(DomainInputRenderer) { + 1 * renderInput([name: "city", id: "city"], property) + } + } + + void "test getStandardAttributes"() { + given: + DomainProperty property = Mock(DomainProperty) { + 1 * getPathFromRoot() >> "city" + 1 * isRequired() >> false + 1 * getConstrained() >> null + } + + when: + Map attrs = renderer.getStandardAttributes(property) + + then: + attrs == [name: "city", id: "city"] + } + + void "test getStandardAttributes required property"() { + given: + DomainProperty property = Mock(DomainProperty) { + 1 * getPathFromRoot() >> "city" + 1 * isRequired() >> true + 1 * getConstrained() >> null + } + + when: + Map attrs = renderer.getStandardAttributes(property) + + then: + attrs == [name: "city", id: "city", required: null] + } + + void "test getStandardAttributes readonly property"() { + given: + DomainProperty property = Mock(DomainProperty) { + 1 * getPathFromRoot() >> "city" + 1 * isRequired() >> false + 2 * getConstrained() >> Mock(Constrained) { + 1 * isEditable() >> false + } + } + + when: + Map attrs = renderer.getStandardAttributes(property) + + then: + attrs == [name: "city", id: "city", readonly: null] + } +} diff --git a/src/test/groovy/org/grails/scaffolding/model/DomainModelServiceSpec.groovy b/src/test/groovy/org/grails/scaffolding/model/DomainModelServiceSpec.groovy new file mode 100644 index 00000000..7bbb0c1e --- /dev/null +++ b/src/test/groovy/org/grails/scaffolding/model/DomainModelServiceSpec.groovy @@ -0,0 +1,242 @@ +package org.grails.scaffolding.model + +import org.grails.datastore.mapping.config.Property +import org.grails.datastore.mapping.model.PropertyMapping +import org.grails.scaffolding.model.property.Constrained +import org.grails.scaffolding.model.property.DomainProperty +import org.grails.scaffolding.model.property.DomainPropertyFactory +import org.grails.scaffolding.model.property.DomainPropertyFactoryImpl +import org.grails.datastore.mapping.keyvalue.mapping.config.KeyValueMappingContext +import org.grails.datastore.mapping.model.MappingContext +import org.grails.datastore.mapping.model.PersistentEntity +import org.grails.datastore.mapping.model.PersistentProperty +import spock.lang.Shared +import spock.lang.Specification + +class DomainModelServiceSpec extends Specification implements MocksDomain { + + @Shared + DomainModelServiceImpl domainModelService + + @Shared + PersistentEntity domainClass + + void setup() { + domainModelService = new DomainModelServiceImpl() + domainClass = Mock(PersistentEntity) { + getJavaClass() >> ScaffoldedDomain + } + } + + void "test getInputProperties valid property"() { + given: + PersistentProperty bar = Mock() + DomainProperty domainProperty = Mock(DomainProperty) { + 1 * getConstrained() >> Mock(Constrained) { 1 * isDisplay() >> true } + 1 * getName() >> "bar" + 1 * getMapping() >> Mock(PropertyMapping) { + 1 * getMappedForm() >> Mock(Property) { + 1 * isDerived() >> false + } + } + } + domainModelService.domainPropertyFactory = Mock(DomainPropertyFactoryImpl) { + 1 * build(bar) >> domainProperty + } + 1 * domainClass.getPersistentProperties() >> [bar] + + when: + List properties = domainModelService.getInputProperties(domainClass).toList() + + then: "properties that are excluded in the scaffolded property aren't included" + properties.size() == 1 + properties[0] == domainProperty + } + + void "test getInputProperties derived"() { + given: + PersistentProperty bar = Mock() + DomainProperty domainProperty = Mock(DomainProperty) { + 1 * getConstrained() >> Mock(Constrained) { 1 * isDisplay() >> true } + 1 * getName() >> "bar" + 1 * getMapping() >> Mock(PropertyMapping) { + 1 * getMappedForm() >> Mock(Property) { + 1 * isDerived() >> true + } + } + } + domainModelService.domainPropertyFactory = Mock(DomainPropertyFactoryImpl) { + 1 * build(bar) >> domainProperty + } + 1 * domainClass.getPersistentProperties() >> [bar] + + when: + List properties = domainModelService.getInputProperties(domainClass).toList() + + then: "derived properties aren't included" + properties.size() == 0 + } + + + void "test getEditableProperties excluded by default"() { + given: + PersistentProperty persistentProperty1 = Mock(PersistentProperty) + PersistentProperty persistentProperty2 = Mock(PersistentProperty) + PersistentProperty persistentProperty3 = Mock(PersistentProperty) + DomainProperty dateCreated = Mock(DomainProperty) { + 1 * getName() >> "dateCreated" + } + DomainProperty lastUpdated = Mock(DomainProperty) { + 1 * getName() >> "lastUpdated" + } + DomainProperty version = Mock(DomainProperty) { + 1 * getName() >> "lastUpdated" + } + domainModelService.domainPropertyFactory = Mock(DomainPropertyFactoryImpl) { + 1 * build(persistentProperty1) >> dateCreated + 1 * build(persistentProperty2) >> lastUpdated + 1 * build(persistentProperty3) >> version + } + 1 * domainClass.getPersistentProperties() >> [persistentProperty1, persistentProperty2, persistentProperty3] + + when: + List properties = domainModelService.getInputProperties(domainClass).toList() + + then: "properties that are excluded by default are excluded" + properties.empty + } + + void "test getEditableProperties constraints display false"() { + given: + PersistentProperty bar = Mock() + DomainProperty domainProperty = Mock(DomainProperty) { + 1 * getName() >> "bar" + 1 * getConstrained() >> Mock(Constrained) { 1 * isDisplay() >> false } + } + domainModelService.domainPropertyFactory = Mock(DomainPropertyFactoryImpl) { + 1 * build(bar) >> domainProperty + } + 1 * domainClass.getPersistentProperties() >> [bar] + + when: + List properties = domainModelService.getInputProperties(domainClass).toList() + + then: "properties that are excluded in the scaffolded property aren't included" + properties.empty + } + + void "test getEditableProperties scaffold exclude"() { + given: + PersistentProperty foo = Mock() + DomainProperty domainProperty = Mock(DomainProperty) { + 1 * getName() >> "foo" + } + domainModelService.domainPropertyFactory = Mock(DomainPropertyFactoryImpl) { + 1 * build(foo) >> domainProperty + } + 1 * domainClass.getPersistentProperties() >> [foo] + + when: + List properties = domainModelService.getInputProperties(domainClass).toList() + + then: "properties that are excluded in the scaffolded property aren't included" + properties.empty + } + + void "test hasProperty"() { + given: + MappingContext mappingContext = new KeyValueMappingContext("test") + PersistentEntity persistentEntity = mockDomainClass(mappingContext, ScaffoldedDomain) + mockDomainClass(mappingContext, EmbeddedAssociate) + DomainPropertyFactory domainPropertyFactory = mockDomainPropertyFactory(mappingContext) + domainModelService.domainPropertyFactory = domainPropertyFactory + + expect: + domainModelService.hasInputProperty(persistentEntity) { DomainProperty p -> + p.name == "timeZone" + } + domainModelService.hasInputProperty(persistentEntity) { DomainProperty p -> + p.name == "locale" + } + !domainModelService.hasInputProperty(persistentEntity) { DomainProperty p -> + p.name == "not here" + } + } + + void "test getVisibleProperties"() { + given: + PersistentProperty persistentProperty1 = Mock(PersistentProperty) + PersistentProperty persistentProperty2 = Mock(PersistentProperty) + DomainProperty bar = Stub(DomainProperty) { + getName() >> "bar" + getConstrained() >> Mock(Constrained) { 1 * isDisplay() >> true } + } + DomainProperty version = Stub(DomainProperty) { + getName() >> "version" + } + domainModelService.domainPropertyFactory = Mock(DomainPropertyFactoryImpl) { + 1 * build(persistentProperty1) >> bar + 1 * build(persistentProperty2) >> version + } + 1 * domainClass.getPersistentProperties() >> [persistentProperty1, persistentProperty2] + + when: + List properties = domainModelService.getOutputProperties(domainClass).toList() + + then: "version is excluded" + properties.size() == 1 + properties[0].name == "bar" + } + + void "test getListOutputProperties"() { + given: + List persistentProperties = (1..10).collect { + Mock(PersistentProperty) + } + List domainProperties = (1..10).collect { num -> + Stub(DomainProperty) { + getName() >> num.toString() + getConstrained() >> Mock(Constrained) { 1 * isDisplay() >> true } + } + } + domainProperties.add(Stub(DomainProperty) { + getName() >> "version" + }) + PersistentProperty identity = Stub(PersistentProperty) + domainModelService.domainPropertyFactory = Mock(DomainPropertyFactoryImpl) { + 10 * build(_ as PersistentProperty) >>> domainProperties + 1 * build(identity) >> Stub(DomainProperty) { + getName() >> "id" + } + } + 1 * domainClass.getPersistentProperties() >> persistentProperties + 1 * domainClass.getIdentity() >> identity + + when: + List properties = domainModelService.getListOutputProperties(domainClass).toList() + + then: "Identity is added to the beginning of the list" + properties.size() == 11 + properties[0].name == "id" + properties[10].name == "10" + } + + class ScaffoldedDomain { + Long id + Long version + static scaffold = [exclude: 'foo'] + + EmbeddedAssociate embeddedAssociate + Locale locale + byte[] data + + static embedded = ['embeddedAssociate'] + } + + class EmbeddedAssociate { + Long id + Long version + TimeZone timeZone + Calendar cal + } +} diff --git a/src/test/groovy/org/grails/scaffolding/model/MocksDomain.groovy b/src/test/groovy/org/grails/scaffolding/model/MocksDomain.groovy new file mode 100644 index 00000000..07e4eaa8 --- /dev/null +++ b/src/test/groovy/org/grails/scaffolding/model/MocksDomain.groovy @@ -0,0 +1,45 @@ +package org.grails.scaffolding.model + +import grails.gorm.validation.PersistentEntityValidator +import groovy.transform.CompileStatic +import org.grails.datastore.gorm.validation.constraints.eval.DefaultConstraintEvaluator +import org.grails.datastore.gorm.validation.constraints.registry.DefaultValidatorRegistry +import org.grails.datastore.mapping.core.connections.ConnectionSourceSettings +import org.grails.datastore.mapping.model.MappingContext +import org.grails.datastore.mapping.model.PersistentEntity +import org.grails.scaffolding.model.property.DomainPropertyFactory +import org.grails.scaffolding.model.property.DomainPropertyFactoryImpl +import org.springframework.context.support.StaticMessageSource +import org.springframework.validation.Validator + +@CompileStatic +trait MocksDomain { + + PersistentEntity mockDomainClass(MappingContext mappingContext, Class clazz) { + PersistentEntity persistentEntity = mappingContext.addPersistentEntity(clazz) + mappingContext.addEntityValidator(persistentEntity, + new PersistentEntityValidator( + persistentEntity, + new StaticMessageSource(), + new DefaultConstraintEvaluator() + ) + ) + persistentEntity + } + + PersistentEntity mockDomainClassEntityValidator(MappingContext mappingContext, Class clazz) { + PersistentEntity persistentEntity = mappingContext.addPersistentEntity(clazz) + def registry = new DefaultValidatorRegistry(mappingContext, new ConnectionSourceSettings()) + Validator validator = registry.getValidator(persistentEntity) + mappingContext.addEntityValidator(persistentEntity, validator) + persistentEntity + } + + DomainPropertyFactory mockDomainPropertyFactory(MappingContext mappingContext) { + DomainPropertyFactory domainPropertyFactory = new DomainPropertyFactoryImpl() + domainPropertyFactory.trimStrings = true + domainPropertyFactory.convertEmptyStringsToNull = true + domainPropertyFactory.grailsDomainClassMappingContext = mappingContext + domainPropertyFactory + } +} \ No newline at end of file diff --git a/src/test/groovy/org/grails/scaffolding/model/property/DomainPropertySpec.groovy b/src/test/groovy/org/grails/scaffolding/model/property/DomainPropertySpec.groovy new file mode 100644 index 00000000..de665390 --- /dev/null +++ b/src/test/groovy/org/grails/scaffolding/model/property/DomainPropertySpec.groovy @@ -0,0 +1,223 @@ +package org.grails.scaffolding.model.property + +import org.grails.orm.hibernate.cfg.HibernateMappingContext +import org.grails.scaffolding.model.MocksDomain +import org.grails.datastore.mapping.keyvalue.mapping.config.KeyValueMappingContext +import org.grails.datastore.mapping.model.MappingContext +import org.grails.datastore.mapping.model.PersistentEntity +import org.grails.datastore.mapping.model.PersistentProperty +import org.grails.datastore.mapping.model.types.Embedded +import spock.lang.Shared +import spock.lang.Specification +import spock.lang.Subject +import spock.lang.Unroll + +/** + * Created by Jim on 6/7/2016. + */ +@Subject(DomainPropertyImpl) +class DomainPropertySpec extends Specification implements MocksDomain { + + @Shared + MappingContext mappingContext + + @Shared + PersistentEntity domainClass + + @Shared + PersistentProperty address + + @Shared + PersistentProperty name + + @Shared + PersistentProperty foos + + @Shared + Embedded props + + void setup() { + mappingContext = new KeyValueMappingContext("test") + domainClass = mockDomainClass(mappingContext, ScaffoldedDomain) + address = domainClass.getPropertyByName("address") + props = (Embedded)domainClass.getPropertyByName("props") + name = props.associatedEntity.getPropertyByName("name") + foos = domainClass.getPropertyByName("foos") + } + + void "test pathFromRoot"() { + given: + DomainProperty property + + when: + property = new DomainPropertyImpl(address, mappingContext) + + then: + property.pathFromRoot == "address" + + when: + property = new DomainPropertyImpl(props, name, mappingContext) + + then: + property.pathFromRoot == "props.name" + } + + void "test bean type"() { + given: + DomainProperty property + + when: + property = new DomainPropertyImpl(address, mappingContext) + + then: + property.rootBeanType == ScaffoldedDomain + property.beanType == ScaffoldedDomain + + when: + property = new DomainPropertyImpl(props, name, mappingContext) + + then: + property.rootBeanType == ScaffoldedDomain + property.beanType == EmbeddedClass + } + + void "test associated type"() { + given: + DomainProperty property + + when: + property = new DomainPropertyImpl(address, mappingContext) + + then: + property.associatedType == null + + when: + property = new DomainPropertyImpl(foos, mappingContext) + + then: + property.associatedType == String + } + + @Unroll + void "test isRequired #propertyName is required: #expected"() { + given: + DomainProperty property + + when: + property = new DomainPropertyImpl(domainClass.getPropertyByName(propertyName), mappingContext) + property.convertEmptyStringsToNull = convertEmpty + property.trimStrings = trimStrings + + then: + property.isRequired() == expected + + where: + propertyName | convertEmpty | trimStrings | expected + "testRequired1" | true | true | true + "testRequired1" | false | true | true + "testRequired1" | true | false | true + "testRequired2" | true | true | false + "testRequired2" | false | true | false + "testRequired2" | true | false | false + "testRequired3" | true | true | false + "testRequired3" | false | true | false + "testRequired3" | true | false | false + "testRequired4" | true | true | true + "testRequired4" | false | true | false + "testRequired4" | true | false | false + } + + void "test getLabelKeys"() { + given: + DomainProperty property + + when: + property = new DomainPropertyImpl(address, mappingContext) + + then: + property.labelKeys == ["scaffoldedDomain.address.label"] + + when: + property = new DomainPropertyImpl(props, name, mappingContext) + + then: + property.labelKeys == ["embeddedClass.name.label", "scaffoldedDomain.props.name.label"] + } + + void "test getDefaultLabel"() { + given: + DomainProperty property + + when: + property = new DomainPropertyImpl(Stub(PersistentProperty) { getName() >> "fooBar" }, mappingContext) + + then: + property.defaultLabel == "Foo Bar" + } + + void "test sort"() { + given: + Embedded property = (Embedded)mappingContext.addExternalPersistentEntity(ScaffoldedDomainEntity).getPropertyByName("props") + List properties = property.associatedEntity.persistentProperties.collect { + new DomainPropertyImpl(it, mappingContext) + } + properties.sort() + + expect: + properties[0].name == "firstName" + properties[1].name == "lastName" + properties.size() == 2 + } + + void "test sort w/ Hibernate embedded"() { + given: + List properties = new HibernateMappingContext().createEmbeddedEntity(EmbeddedClassEntity).persistentProperties.collect { + new DomainPropertyImpl(it, mappingContext) + } + properties.sort() + + expect: + properties[0].name == "firstName" + properties[1].name == "lastName" + properties.size() == 2 + } + + class ScaffoldedDomain { + Long id + Long version + String address + EmbeddedClass props + + String testRequired1 + String testRequired2 + String testRequired3 + String testRequired4 + + Set foos + static hasMany = [foos: String] + + static embedded = ['props'] + + static constraints = { + testRequired1(nullable: false, blank: false) + testRequired2(nullable: false, blank: true) + testRequired3(nullable: true, blank: false) + } + } + + class ScaffoldedDomainEntity { + Long id + Long version + EmbeddedClassEntity props + static embedded = ['props'] + } + + class EmbeddedClass { + String name + } + + class EmbeddedClassEntity { + String lastName + String firstName + } +} diff --git a/src/test/groovy/org/grails/scaffolding/model/property/EntityValidatorDomainPropertySpec.groovy b/src/test/groovy/org/grails/scaffolding/model/property/EntityValidatorDomainPropertySpec.groovy new file mode 100644 index 00000000..30a17aef --- /dev/null +++ b/src/test/groovy/org/grails/scaffolding/model/property/EntityValidatorDomainPropertySpec.groovy @@ -0,0 +1,103 @@ +package org.grails.scaffolding.model.property + +import org.grails.datastore.mapping.keyvalue.mapping.config.KeyValueMappingContext +import org.grails.datastore.mapping.model.MappingContext +import org.grails.datastore.mapping.model.PersistentEntity +import org.grails.datastore.mapping.model.PersistentProperty +import org.grails.datastore.mapping.model.types.Embedded +import org.grails.scaffolding.model.MocksDomain +import spock.lang.Shared +import spock.lang.Specification +import spock.lang.Unroll + +/** + * Created by jameskleeh on 5/25/17. + */ +class EntityValidatorDomainPropertySpec extends Specification implements MocksDomain { + + @Shared + MappingContext mappingContext + + @Shared + PersistentEntity domainClass + + @Shared + PersistentProperty address + + @Shared + PersistentProperty name + + @Shared + PersistentProperty foos + + @Shared + Embedded props + + void setup() { + mappingContext = new KeyValueMappingContext("test") + domainClass = mockDomainClassEntityValidator(mappingContext, ScaffoldedDomain) + address = domainClass.getPropertyByName("address") + props = (Embedded)domainClass.getPropertyByName("props") + name = props.associatedEntity.getPropertyByName("name") + foos = domainClass.getPropertyByName("foos") + } + + @Unroll + void "test isRequired #propertyName is required: #expected"() { + given: + DomainProperty property + + when: + property = new DomainPropertyImpl(domainClass.getPropertyByName(propertyName), mappingContext) + property.convertEmptyStringsToNull = convertEmpty + property.trimStrings = trimStrings + + then: + property.isRequired() == expected + + where: + propertyName | convertEmpty | trimStrings | expected + "testRequired1" | true | true | true + "testRequired1" | false | true | true + "testRequired1" | true | false | true + "testRequired2" | true | true | false + "testRequired2" | false | true | false + "testRequired2" | true | false | false + "testRequired3" | true | true | false + "testRequired3" | false | true | false + "testRequired3" | true | false | false + "testRequired4" | true | true | true + "testRequired4" | false | true | false + "testRequired4" | true | false | false + } + + class ScaffoldedDomain { + Long id + Long version + String address + EmbeddedClass props + + String testRequired1 + String testRequired2 + String testRequired3 + String testRequired4 + + Set foos + static hasMany = [foos: String] + + static embedded = ['props'] + + static constraints = { + testRequired1(nullable: false, blank: false) + testRequired2(nullable: false, blank: true) + testRequired3(nullable: true, blank: false) + } + } + + + class EmbeddedClass { + String name + } + + +} diff --git a/src/test/groovy/org/grails/scaffolding/registry/DomainRendererRegistererSpec.groovy b/src/test/groovy/org/grails/scaffolding/registry/DomainRendererRegistererSpec.groovy new file mode 100644 index 00000000..aef40a56 --- /dev/null +++ b/src/test/groovy/org/grails/scaffolding/registry/DomainRendererRegistererSpec.groovy @@ -0,0 +1,219 @@ +package org.grails.scaffolding.registry + +import org.grails.scaffolding.model.property.Constrained +import org.grails.scaffolding.model.property.DomainProperty +import org.grails.scaffolding.registry.input.* +import org.grails.datastore.mapping.model.types.OneToMany +import spock.lang.Shared +import spock.lang.Specification + +import java.sql.Time + +/** + * Created by Jim on 5/26/2016. + */ +class DomainRendererRegistererSpec extends Specification { + + @Shared + DomainInputRendererRegistry domainInputRendererRegistry + + void setup() { + domainInputRendererRegistry = new DomainInputRendererRegistry() + DomainOutputRendererRegistry domainOutputRendererRegistry = new DomainOutputRendererRegistry() + new DomainRendererRegisterer(domainInputRendererRegistry: domainInputRendererRegistry, domainOutputRendererRegistry: domainOutputRendererRegistry).registerRenderers() + } + + + void "test the InList renderer is returned for String"() { + given: + DomainProperty domainProperty = Stub(DomainProperty) { + getType() >> String + getConstrained() >> Stub(Constrained) { + getInList() >> ["foo"] + } + } + + expect: + domainInputRendererRegistry.get(domainProperty) instanceof InListInputRenderer + } + + void "test the Textarea renderer is returned"() { + given: + DomainProperty domainProperty = Stub(DomainProperty) { + getType() >> String + getConstrained() >> Stub(Constrained) { + getWidget() >> "textarea" + } + } + + expect: + domainInputRendererRegistry.get(domainProperty) instanceof TextareaInputRenderer + } + + void "test the String renderer is returned"() { + given: + DomainProperty domainProperty = Stub(DomainProperty) { + getType() >> String + getConstrained() >> Stub(Constrained) + } + + expect: + domainInputRendererRegistry.get(domainProperty) instanceof StringInputRenderer + } + + void "test the Boolean renderer is returned"() { + given: + DomainProperty domainProperty = Stub(DomainProperty) { + getType() >> Boolean + } + + expect: + domainInputRendererRegistry.get(domainProperty) instanceof BooleanInputRenderer + } + + void "test the InList renderer is returned for Number"() { + given: + DomainProperty domainProperty = Stub(DomainProperty) { + getType() >> Long + getConstrained() >> Stub(Constrained) { + getInList() >> [1L, 2L] + } + } + + expect: + domainInputRendererRegistry.get(domainProperty) instanceof InListInputRenderer + } + + void "test the Number renderer is returned"() { + given: + DomainProperty domainProperty = Stub(DomainProperty) { + getType() >> Long + } + + expect: + domainInputRendererRegistry.get(domainProperty) instanceof NumberInputRenderer + } + + void "test the URL renderer is returned"() { + given: + DomainProperty domainProperty = Stub(DomainProperty) { + getType() >> URL + } + + expect: + domainInputRendererRegistry.get(domainProperty) instanceof UrlInputRenderer + } + + enum Fruit {APPLE,ORANGE,BANANA,PEAR}; + + void "test the Enum renderer is returned"() { + given: + DomainProperty domainProperty = Stub(DomainProperty) { + getType() >> Fruit + } + + expect: + domainInputRendererRegistry.get(domainProperty) instanceof EnumInputRenderer + } + + void "test the Date renderer is returned"() { + given: + DomainProperty domainProperty = Stub(DomainProperty) { + getType() >> Calendar + } + + expect: + domainInputRendererRegistry.get(domainProperty) instanceof DateInputRenderer + } + + void "test the Time renderer is returned"() { + given: + DomainProperty domainProperty = Stub(DomainProperty) { + getType() >> Time + } + + expect: + domainInputRendererRegistry.get(domainProperty) instanceof TimeInputRenderer + } + + + void "test the File renderer is returned"() { + given: + DomainProperty domainProperty = Stub(DomainProperty) { + getType() >> byte[] + } + + expect: + domainInputRendererRegistry.get(domainProperty) instanceof FileInputRenderer + } + + void "test the TimeZone renderer is returned"() { + given: + DomainProperty domainProperty = Stub(DomainProperty) { + getType() >> TimeZone + } + + expect: + domainInputRendererRegistry.get(domainProperty) instanceof TimeZoneInputRenderer + } + + void "test the Currency renderer is returned"() { + given: + DomainProperty domainProperty = Stub(DomainProperty) { + getType() >> Currency + } + + expect: + domainInputRendererRegistry.get(domainProperty) instanceof CurrencyInputRenderer + } + + void "test the Locale renderer is returned"() { + given: + DomainProperty domainProperty = Stub(DomainProperty) { + getType() >> Locale + } + + expect: + domainInputRendererRegistry.get(domainProperty) instanceof LocaleInputRenderer + } + + void "test the Default renderer is returned"() { + given: + DomainProperty domainProperty = Stub(DomainProperty) { + getType() >> Specification + getConstrained() >> Stub(Constrained) { + getWidget() >> "" + } + } + + expect: + domainInputRendererRegistry.get(domainProperty) instanceof DefaultInputRenderer + } + + void "test the BiDirectionalToMany renderer is returned"() { + given: + DomainProperty domainProperty = Stub(DomainProperty) { + getPersistentProperty() >> Stub(OneToMany) { + isBidirectional() >> true + } + + } + + expect: + domainInputRendererRegistry.get(domainProperty) instanceof BidirectionalToManyInputRenderer + } + + void "test the Association renderer is returned"() { + given: + DomainProperty domainProperty = Stub(DomainProperty) { + getType() >> Set + getPersistentProperty() >> Stub(OneToMany) { + isBidirectional() >> false + } + } + + expect: + domainInputRendererRegistry.get(domainProperty) instanceof AssociationInputRenderer + } + +} diff --git a/src/test/groovy/org/grails/scaffolding/registry/DomainRendererRegistrySpec.groovy b/src/test/groovy/org/grails/scaffolding/registry/DomainRendererRegistrySpec.groovy new file mode 100644 index 00000000..26480e6e --- /dev/null +++ b/src/test/groovy/org/grails/scaffolding/registry/DomainRendererRegistrySpec.groovy @@ -0,0 +1,72 @@ +package org.grails.scaffolding.registry + +import org.grails.scaffolding.model.property.DomainProperty +import spock.lang.Shared +import spock.lang.Specification + +/** + * Created by Jim on 5/26/2016. + */ +class DomainRendererRegistrySpec extends Specification { + + @Shared + DomainOutputRendererRegistry registry + + void setup() { + registry = new DomainOutputRendererRegistry() + } + + void "test renderers are returned in order"() { + given: + DomainOutputRenderer levelOne = Stub(DomainOutputRenderer) { + supports(_ as DomainProperty) >> true + } + DomainOutputRenderer levelTwo = Stub(DomainOutputRenderer) { + supports(_ as DomainProperty) >> true + } + registry.registerDomainRenderer(levelOne, 1) + registry.registerDomainRenderer(levelTwo, 2) + + when: + DomainOutputRenderer resolved = registry.get(Mock(DomainProperty)) + + then: + resolved == levelTwo + } + + void "test the last renderer added will have priority over others with the same priority"() { + given: + DomainOutputRenderer levelOne = Stub(DomainOutputRenderer) { + supports(_ as DomainProperty) >> true + } + DomainOutputRenderer levelTwo = Stub(DomainOutputRenderer) { + supports(_ as DomainProperty) >> true + } + registry.registerDomainRenderer(levelOne, 1) + registry.registerDomainRenderer(levelTwo, 1) + + when: + DomainOutputRenderer resolved = registry.get(Mock(DomainProperty)) + + then: + resolved == levelTwo + } + + void "test only supported renderers are resolved"() { + given: + DomainOutputRenderer levelOne = Stub(DomainOutputRenderer) { + supports(_ as DomainProperty) >> true + } + DomainOutputRenderer levelTwo = Stub(DomainOutputRenderer) { + supports(_ as DomainProperty) >> false + } + registry.registerDomainRenderer(levelOne, 1) + registry.registerDomainRenderer(levelTwo, 2) + + when: + DomainOutputRenderer resolved = registry.get(Mock(DomainProperty)) + + then: + resolved == levelOne + } +} diff --git a/src/test/groovy/org/grails/scaffolding/registry/input/AssociationInputRendererSpec.groovy b/src/test/groovy/org/grails/scaffolding/registry/input/AssociationInputRendererSpec.groovy new file mode 100644 index 00000000..39a4ec0d --- /dev/null +++ b/src/test/groovy/org/grails/scaffolding/registry/input/AssociationInputRendererSpec.groovy @@ -0,0 +1,31 @@ +package org.grails.scaffolding.registry.input + +import org.grails.scaffolding.ClosureCaptureSpecification +import org.grails.scaffolding.model.property.DomainProperty +import org.grails.datastore.mapping.model.types.ToOne +import spock.lang.Shared +import spock.lang.Subject + +/** + * Created by Jim on 6/7/2016. + */ +@Subject(AssociationInputRenderer) +class AssociationInputRendererSpec extends ClosureCaptureSpecification { + + @Shared + AssociationInputRenderer renderer + + void setup() { + renderer = new AssociationInputRenderer() + } + + void "test supports"() { + when: + DomainProperty property = Mock(DomainProperty) { + 1 * getPersistentProperty() >> Mock(ToOne) + } + + then: + renderer.supports(property) + } +} diff --git a/src/test/groovy/org/grails/scaffolding/registry/input/BidirectionalToManyInputRendererSpec.groovy b/src/test/groovy/org/grails/scaffolding/registry/input/BidirectionalToManyInputRendererSpec.groovy new file mode 100644 index 00000000..5f75826f --- /dev/null +++ b/src/test/groovy/org/grails/scaffolding/registry/input/BidirectionalToManyInputRendererSpec.groovy @@ -0,0 +1,58 @@ +package org.grails.scaffolding.registry.input + +import org.grails.scaffolding.ClosureCapture +import org.grails.scaffolding.ClosureCaptureSpecification +import org.grails.scaffolding.model.property.DomainProperty +import grails.web.mapping.LinkGenerator +import org.grails.datastore.mapping.model.PersistentProperty +import org.grails.datastore.mapping.model.types.ToMany +import spock.lang.Ignore +import spock.lang.Shared +import spock.lang.Subject + +@Subject(BidirectionalToManyInputRenderer) +class BidirectionalToManyInputRendererSpec extends ClosureCaptureSpecification { + + @Shared + BidirectionalToManyInputRenderer renderer + + void setup() { + renderer = new BidirectionalToManyInputRenderer(Mock(LinkGenerator)) + } + + void "test supports"() { + given: + DomainProperty property + + when: + property = Mock(DomainProperty) { + 1 * getPersistentProperty() >> Mock(ToMany) { + 1 * isBidirectional() >> true + } + } + + then: + renderer.supports(property) + } + + @Ignore + void "test render"() { + given: + DomainProperty property + renderer.linkGenerator = Mock(LinkGenerator) { + 1 * link([resource: Calendar, action: "create", params: ["timeZone.id": ""]]) >> "http://www.google.com" + } + + when: + property = Mock(DomainProperty) { + 1 * getRootBeanType() >> TimeZone + 2 * getAssociatedType() >> Calendar + } + ClosureCapture closureCapture = getClosureCapture(renderer.renderInput([required: "", readonly: ""], property)) + + then: + closureCapture.calls[0].name == "a" + closureCapture.calls[0].args[0] == "Add Calendar" + closureCapture.calls[0].args[1] == [href: "http://www.google.com"] + } +} diff --git a/src/test/groovy/org/grails/scaffolding/registry/input/BooleanInputRendererSpec.groovy b/src/test/groovy/org/grails/scaffolding/registry/input/BooleanInputRendererSpec.groovy new file mode 100644 index 00000000..84da58ce --- /dev/null +++ b/src/test/groovy/org/grails/scaffolding/registry/input/BooleanInputRendererSpec.groovy @@ -0,0 +1,45 @@ +package org.grails.scaffolding.registry.input + +import org.grails.scaffolding.ClosureCapture +import org.grails.scaffolding.ClosureCaptureSpecification +import org.grails.scaffolding.model.property.DomainProperty +import spock.lang.Shared +import spock.lang.Subject + +@Subject(BooleanInputRenderer) +class BooleanInputRendererSpec extends ClosureCaptureSpecification { + + @Shared + BooleanInputRenderer renderer + + void setup() { + renderer = new BooleanInputRenderer() + } + + void "test supports"() { + given: + DomainProperty property + + when: + property = Mock(DomainProperty) { + 1 * getType() >> type + } + + then: + renderer.supports(property) + + where: + type | _ + boolean | _ + Boolean | _ + } + + void "test render"() { + when: + ClosureCapture closureCapture = getClosureCapture(renderer.renderInput([:], Mock(DomainProperty))) + + then: + closureCapture.calls[0].name == "input" + closureCapture.calls[0].args[0] == ["type": "checkbox"] + } +} diff --git a/src/test/groovy/org/grails/scaffolding/registry/input/CurrencyInputRendererSpec.groovy b/src/test/groovy/org/grails/scaffolding/registry/input/CurrencyInputRendererSpec.groovy new file mode 100644 index 00000000..6a0dad57 --- /dev/null +++ b/src/test/groovy/org/grails/scaffolding/registry/input/CurrencyInputRendererSpec.groovy @@ -0,0 +1,40 @@ +package org.grails.scaffolding.registry.input + +import org.grails.scaffolding.ClosureCaptureSpecification +import org.grails.scaffolding.model.property.DomainProperty +import spock.lang.Shared +import spock.lang.Specification +import spock.lang.Subject + +@Subject(CurrencyInputRenderer) +class CurrencyInputRendererSpec extends Specification { + + @Shared + CurrencyInputRenderer renderer + + void setup() { + renderer = new CurrencyInputRenderer() + } + + void "test supports"() { + given: + DomainProperty property + + when: + property = Mock(DomainProperty) { + 1 * getType() >> Currency + } + + then: + renderer.supports(property) + } + + void "test option key and value"() { + given: + Currency currency = Currency.getInstance("USD") + + expect: + renderer.getOptionKey(currency) == "USD" + renderer.getOptionValue(currency) == "USD" + } +} diff --git a/src/test/groovy/org/grails/scaffolding/registry/input/DateInputRendererSpec.groovy b/src/test/groovy/org/grails/scaffolding/registry/input/DateInputRendererSpec.groovy new file mode 100644 index 00000000..36383582 --- /dev/null +++ b/src/test/groovy/org/grails/scaffolding/registry/input/DateInputRendererSpec.groovy @@ -0,0 +1,46 @@ +package org.grails.scaffolding.registry.input + +import org.grails.scaffolding.ClosureCapture +import org.grails.scaffolding.ClosureCaptureSpecification +import org.grails.scaffolding.model.property.DomainProperty +import spock.lang.Shared +import spock.lang.Subject + +@Subject(DateInputRenderer) +class DateInputRendererSpec extends ClosureCaptureSpecification { + + @Shared + DateInputRenderer renderer + + void setup() { + renderer = new DateInputRenderer() + } + + void "test supports"() { + given: + DomainProperty property + + when: + property = Mock(DomainProperty) { + 1 * getType() >> type + } + + then: + renderer.supports(property) + + where: + type | _ + Date | _ + Calendar | _ + java.sql.Date | _ + } + + void "test render"() { + when: + ClosureCapture closureCapture = getClosureCapture(renderer.renderInput([:], Mock(DomainProperty))) + + then: + closureCapture.calls[0].name == "input" + closureCapture.calls[0].args[0] == ["type": "date", "placeholder": "YYYY-MM-DD"] + } +} diff --git a/src/test/groovy/org/grails/scaffolding/registry/input/EnumInputRendererSpec.groovy b/src/test/groovy/org/grails/scaffolding/registry/input/EnumInputRendererSpec.groovy new file mode 100644 index 00000000..412d9b0b --- /dev/null +++ b/src/test/groovy/org/grails/scaffolding/registry/input/EnumInputRendererSpec.groovy @@ -0,0 +1,83 @@ +package org.grails.scaffolding.registry.input + +import org.grails.scaffolding.ClosureCapture +import org.grails.scaffolding.ClosureCaptureSpecification +import org.grails.scaffolding.model.property.DomainProperty +import spock.lang.Shared +import spock.lang.Subject + +@Subject(EnumInputRenderer) +class EnumInputRendererSpec extends ClosureCaptureSpecification { + + @Shared + EnumInputRenderer renderer + + void setup() { + renderer = new EnumInputRenderer() + } + + void "test supports"() { + given: + DomainProperty property + + when: + property = Mock(DomainProperty) { + 1 * getType() >> Fruit + } + + then: + renderer.supports(property) + } + + void "test render"() { + given: + DomainProperty property + ClosureCapture closureCapture + + when: + property = Mock(DomainProperty) { + 2 * getType() >> Fruit + } + closureCapture = getClosureCapture(renderer.renderInput([:], property)) + + then: + closureCapture.calls[0].name == "select" + closureCapture.calls[0].args[0] == [:] + closureCapture.calls[0][0].name == "option" + closureCapture.calls[0][0].args[0] == "APPLE" + closureCapture.calls[0][0].args[1] == [value: "APPLE"] + closureCapture.calls[0][1].name == "option" + closureCapture.calls[0][1].args[0] == "ORANGE" + closureCapture.calls[0][1].args[1] == [value: "ORANGE"] + + when: + property = Mock(DomainProperty) { + 2 * getType() >> Car + } + closureCapture = getClosureCapture(renderer.renderInput([:], property)) + + then: + closureCapture.calls[0].name == "select" + closureCapture.calls[0].args[0] == [:] + closureCapture.calls[0][0].name == "option" + closureCapture.calls[0][0].args[0] == "Alfa Romeo" + closureCapture.calls[0][0].args[1] == [value: "ALFA_ROMEO"] + closureCapture.calls[0][1].name == "option" + closureCapture.calls[0][1].args[0] == "Subaru" + closureCapture.calls[0][1].args[1] == [value: "SUBARU"] + } + + enum Fruit { APPLE, ORANGE } + enum Car { + ALFA_ROMEO("Alfa Romeo"), + SUBARU("Subaru") + + private String val + Car(String val) { + this.val = val + } + String toString() { + val + } + } +} \ No newline at end of file diff --git a/src/test/groovy/org/grails/scaffolding/registry/input/FileInputRendererSpec.groovy b/src/test/groovy/org/grails/scaffolding/registry/input/FileInputRendererSpec.groovy new file mode 100644 index 00000000..9d491409 --- /dev/null +++ b/src/test/groovy/org/grails/scaffolding/registry/input/FileInputRendererSpec.groovy @@ -0,0 +1,49 @@ +package org.grails.scaffolding.registry.input + +import org.grails.scaffolding.ClosureCapture +import org.grails.scaffolding.ClosureCaptureSpecification +import org.grails.scaffolding.model.property.DomainProperty +import spock.lang.Shared +import spock.lang.Subject + +import java.sql.Blob + +@Subject(FileInputRenderer) +class FileInputRendererSpec extends ClosureCaptureSpecification { + + @Shared + FileInputRenderer renderer + + void setup() { + renderer = new FileInputRenderer() + } + + void "test supports"() { + given: + DomainProperty property + + when: + property = Mock(DomainProperty) { + 1 * getType() >> type + } + + then: + renderer.supports(property) + + where: + type | _ + byte[] | _ + Byte[] | _ + Blob | _ + } + + void "test render"() { + when: + ClosureCapture closureCapture = getClosureCapture(renderer.renderInput([:], Mock(DomainProperty))) + + then: + closureCapture.calls[0].name == "input" + closureCapture.calls[0].args[0] == ["type": "file"] + } + +} diff --git a/src/test/groovy/org/grails/scaffolding/registry/input/InListInputRendererSpec.groovy b/src/test/groovy/org/grails/scaffolding/registry/input/InListInputRendererSpec.groovy new file mode 100644 index 00000000..a37068dc --- /dev/null +++ b/src/test/groovy/org/grails/scaffolding/registry/input/InListInputRendererSpec.groovy @@ -0,0 +1,61 @@ +package org.grails.scaffolding.registry.input + +import org.grails.scaffolding.ClosureCapture +import org.grails.scaffolding.ClosureCaptureSpecification +import org.grails.scaffolding.model.property.DomainProperty +import org.grails.scaffolding.model.property.Constrained +import spock.lang.Shared +import spock.lang.Subject + +/** + * Created by Jim on 6/6/2016. + */ +@Subject(InListInputRenderer) +class InListInputRendererSpec extends ClosureCaptureSpecification { + + @Shared + InListInputRenderer renderer + + void setup() { + renderer = new InListInputRenderer() + } + + void "test supports"() { + given: + DomainProperty property + + when: + property = Mock(DomainProperty) { + 1 * getConstrained() >> Mock(Constrained) { + 1 * getInList() >> [1] + } + } + + then: + renderer.supports(property) + } + + void "test render"() { + given: + DomainProperty property + ClosureCapture closureCapture + + when: + property = Mock(DomainProperty) { + 1 * getConstrained() >> Mock(Constrained) { + 1 * getInList() >> [1, 2] + } + } + closureCapture = getClosureCapture(renderer.renderInput([:], property)) + + then: + closureCapture.calls[0].name == "select" + closureCapture.calls[0].args[0] == [:] + closureCapture.calls[0][0].name == "option" + closureCapture.calls[0][0].args[0] == 1 + closureCapture.calls[0][0].args[1] == ["value": 1] + closureCapture.calls[0][1].name == "option" + closureCapture.calls[0][1].args[0] == 2 + closureCapture.calls[0][1].args[1] == ["value": 2] + } +} diff --git a/src/test/groovy/org/grails/scaffolding/registry/input/LocaleInputRendererSpec.groovy b/src/test/groovy/org/grails/scaffolding/registry/input/LocaleInputRendererSpec.groovy new file mode 100644 index 00000000..e73834be --- /dev/null +++ b/src/test/groovy/org/grails/scaffolding/registry/input/LocaleInputRendererSpec.groovy @@ -0,0 +1,49 @@ +package org.grails.scaffolding.registry.input + +import org.grails.scaffolding.model.property.DomainProperty +import spock.lang.Shared +import spock.lang.Specification +import spock.lang.Subject + +@Subject(LocaleInputRenderer) +class LocaleInputRendererSpec extends Specification { + + @Shared + LocaleInputRenderer renderer + + void setup() { + renderer = new LocaleInputRenderer() + } + + void "test supports"() { + given: + DomainProperty property + + when: + property = Mock(DomainProperty) { + 1 * getType() >> Locale + } + + then: + renderer.supports(property) + } + + void "test option key and value"() { + given: + Locale locale + + when: + locale = Locale.US + + then: + renderer.getOptionKey(locale) == "en_US" + renderer.getOptionValue(locale) == "en, US, English (United States)" + + when: + locale = Locale.ENGLISH + + then: + renderer.getOptionKey(locale) == "en" + renderer.getOptionValue(locale) == "en, English" + } +} diff --git a/src/test/groovy/org/grails/scaffolding/registry/input/MapToSelectInputRendererSpec.groovy b/src/test/groovy/org/grails/scaffolding/registry/input/MapToSelectInputRendererSpec.groovy new file mode 100644 index 00000000..6e687673 --- /dev/null +++ b/src/test/groovy/org/grails/scaffolding/registry/input/MapToSelectInputRendererSpec.groovy @@ -0,0 +1,70 @@ +package org.grails.scaffolding.registry.input + +import org.grails.scaffolding.ClosureCapture +import org.grails.scaffolding.ClosureCaptureSpecification +import org.grails.scaffolding.model.property.DomainProperty +import org.grails.scaffolding.registry.DomainInputRenderer +import spock.lang.Shared +import spock.lang.Subject + +/** + * Created by Jim on 6/6/2016. + */ +@Subject(MapToSelectInputRenderer) +class MapToSelectInputRendererSpec extends ClosureCaptureSpecification { + + @Shared + MapToSelectInputRenderer renderer + + void setup() { + renderer = new Renderer() + } + + void "test render"() { + given: + ClosureCapture closureCapture + + when: + closureCapture = getClosureCapture(renderer.renderInput([:], Mock(DomainProperty))) + + then: + closureCapture.calls[0].name == "select" + closureCapture.calls[0].args[0] == [:] + closureCapture.calls[0][0].name == "option" + closureCapture.calls[0][0].args[0] == "A" + closureCapture.calls[0][0].args[1] == ["value": "a"] + closureCapture.calls[0][1].args[0] == "B" + closureCapture.calls[0][1].args[1] == ["value": "b"] + closureCapture.calls[0][2].args[0] == "Cat" + closureCapture.calls[0][2].args[1] == ["value": "cat", "selected": ""] + } + + + class Renderer implements MapToSelectInputRenderer { + @Override + String getOptionValue(String o) { + o.capitalize() + } + + @Override + String getOptionKey(String o) { + o.toLowerCase() + } + + @Override + String getDefaultOption() { + "cat" + } + + @Override + Map getOptions() { + ["a": "A", "b": "B", "cat": "Cat"] + } + + @Override + boolean supports(DomainProperty property) { + false + } + } + +} diff --git a/src/test/groovy/org/grails/scaffolding/registry/input/NumberInputRendererSpec.groovy b/src/test/groovy/org/grails/scaffolding/registry/input/NumberInputRendererSpec.groovy new file mode 100644 index 00000000..a6a802e0 --- /dev/null +++ b/src/test/groovy/org/grails/scaffolding/registry/input/NumberInputRendererSpec.groovy @@ -0,0 +1,127 @@ +package org.grails.scaffolding.registry.input + +import org.grails.scaffolding.ClosureCapture +import org.grails.scaffolding.ClosureCaptureSpecification +import org.grails.scaffolding.model.property.DomainProperty +import org.grails.scaffolding.model.property.Constrained +import spock.lang.Shared +import spock.lang.Subject + +/** + * Created by Jim on 6/6/2016. + */ +@Subject(NumberInputRenderer) +class NumberInputRendererSpec extends ClosureCaptureSpecification { + + @Shared + NumberInputRenderer renderer + + void setup() { + renderer = new NumberInputRenderer() + } + + void "test supports"() { + given: + DomainProperty property + + when: + property = Mock(DomainProperty) { + 1 * getType() >> type + } + + then: + renderer.supports(property) + + where: + type | _ + int | _ + long | _ + double | _ + Integer | _ + Long | _ + Double | _ + } + + void "test render"() { + given: + DomainProperty property + ClosureCapture closureCapture + + when: + property = Mock(DomainProperty) { + 1 * getConstrained() >> Mock(Constrained) { + 1 * getRange() >> (1..5) + } + } + closureCapture = getClosureCapture(renderer.renderInput([:], property)) + + then: + closureCapture.calls[0].name == "input" + closureCapture.calls[0].args[0] == ["type": "range", "min": 1, "max": 5] + + when: + property = Mock(DomainProperty) { + 1 * getType() >> Integer + 1 * getConstrained() >> Mock(Constrained) { + 1 * getRange() >> null + 1 * getScale() >> null + 1 * getMin() >> null + 1 * getMax() >> null + } + } + closureCapture = getClosureCapture(renderer.renderInput([:], property)) + + then: + closureCapture.calls[0].name == "input" + closureCapture.calls[0].args[0] == ["type": "number"] + + when: + property = Mock(DomainProperty) { + 1 * getType() >> Double + 1 * getConstrained() >> Mock(Constrained) { + 1 * getRange() >> null + 1 * getScale() >> null + 1 * getMin() >> null + 1 * getMax() >> null + } + } + closureCapture = getClosureCapture(renderer.renderInput([:], property)) + + then: + closureCapture.calls[0].name == "input" + closureCapture.calls[0].args[0] == ["type": "number", "step": "any"] + + when: + property = Mock(DomainProperty) { + 1 * getType() >> Integer + 1 * getConstrained() >> Mock(Constrained) { + 1 * getRange() >> null + 2 * getScale() >> 3 + 1 * getMin() >> null + 1 * getMax() >> null + } + } + closureCapture = getClosureCapture(renderer.renderInput([:], property)) + + then: + closureCapture.calls[0].name == "input" + closureCapture.calls[0].args[0] == ["type": "number", "step": "0.001"] + + when: + property = Mock(DomainProperty) { + 1 * getType() >> Integer + 1 * getConstrained() >> Mock(Constrained) { + 1 * getRange() >> null + 1 * getScale() >> null + 2 * getMin() >> 5 + 2 * getMax() >> 6 + } + } + closureCapture = getClosureCapture(renderer.renderInput([:], property)) + + then: + closureCapture.calls[0].name == "input" + closureCapture.calls[0].args[0] == ["type": "number", "min": 5, "max": 6] + + } +} diff --git a/src/test/groovy/org/grails/scaffolding/registry/input/StringInputRendererSpec.groovy b/src/test/groovy/org/grails/scaffolding/registry/input/StringInputRendererSpec.groovy new file mode 100644 index 00000000..f446fd97 --- /dev/null +++ b/src/test/groovy/org/grails/scaffolding/registry/input/StringInputRendererSpec.groovy @@ -0,0 +1,123 @@ +package org.grails.scaffolding.registry.input + +import org.grails.scaffolding.ClosureCapture +import org.grails.scaffolding.ClosureCaptureSpecification +import org.grails.scaffolding.model.property.DomainProperty +import org.grails.scaffolding.model.property.Constrained +import spock.lang.Shared +import spock.lang.Subject + +@Subject(StringInputRenderer) +class StringInputRendererSpec extends ClosureCaptureSpecification { + + @Shared + StringInputRenderer renderer + + void setup() { + renderer = new StringInputRenderer() + } + + void "test supports"() { + given: + DomainProperty prop + + when: + prop = Mock(DomainProperty) { + 1 * getType() >> String + } + + then: + renderer.supports(prop) + + when: + prop = Mock(DomainProperty) { + 1 * getType() >> null + } + + then: + renderer.supports(prop) + } + + void "test render"() { + given: + DomainProperty property + ClosureCapture closureCapture + + when: + property = Mock(DomainProperty) { + 1 * getConstrained() >> Mock(Constrained) { + 1 * isPassword() >> true + 1 * getMatches() >> null + 1 * getMaxSize() >> null + } + } + closureCapture = getClosureCapture(renderer.renderInput([:], property)) + + then: + closureCapture.calls[0].name == "input" + closureCapture.calls[0].args[0] == ["type": "password"] + + when: + property = Mock(DomainProperty) { + 1 * getConstrained() >> Mock(Constrained) { + 1 * isPassword() >> false + 1 * isEmail() >> true + 1 * getMatches() >> null + 1 * getMaxSize() >> null + } + } + closureCapture = getClosureCapture(renderer.renderInput([:], property)) + + then: + closureCapture.calls[0].name == "input" + closureCapture.calls[0].args[0] == ["type": "email"] + + when: + property = Mock(DomainProperty) { + 1 * getConstrained() >> Mock(Constrained) { + 1 * isPassword() >> false + 1 * isEmail() >> false + 1 * isUrl() >> true + 1 * getMatches() >> null + 1 * getMaxSize() >> null + } + } + closureCapture = getClosureCapture(renderer.renderInput([:], property)) + + then: + closureCapture.calls[0].name == "input" + closureCapture.calls[0].args[0] == ["type": "url"] + + when: + property = Mock(DomainProperty) { + 1 * getConstrained() >> Mock(Constrained) { + 1 * isPassword() >> false + 1 * isEmail() >> false + 1 * isUrl() >> false + 1 * getMatches() >> null + 1 * getMaxSize() >> null + } + } + closureCapture = getClosureCapture(renderer.renderInput([:], property)) + + then: + closureCapture.calls[0].name == "input" + closureCapture.calls[0].args[0] == ["type": "text"] + + when: + property = Mock(DomainProperty) { + 1 * getConstrained() >> Mock(Constrained) { + 1 * isPassword() >> false + 1 * isEmail() >> false + 1 * isUrl() >> false + 2 * getMatches() >> "abc" + 2 * getMaxSize() >> 20 + } + } + closureCapture = getClosureCapture(renderer.renderInput([:], property)) + + then: + closureCapture.calls[0].name == "input" + closureCapture.calls[0].args[0] == ["type": "text", "pattern": "abc", "maxlength": 20] + } +} \ No newline at end of file diff --git a/src/test/groovy/org/grails/scaffolding/registry/input/TextareaInputRendererSpec.groovy b/src/test/groovy/org/grails/scaffolding/registry/input/TextareaInputRendererSpec.groovy new file mode 100644 index 00000000..3d11e96a --- /dev/null +++ b/src/test/groovy/org/grails/scaffolding/registry/input/TextareaInputRendererSpec.groovy @@ -0,0 +1,48 @@ +package org.grails.scaffolding.registry.input + +import org.grails.scaffolding.ClosureCapture +import org.grails.scaffolding.ClosureCaptureSpecification +import org.grails.scaffolding.model.property.DomainProperty +import org.grails.scaffolding.model.property.Constrained +import spock.lang.Shared +import spock.lang.Subject + +@Subject(TextareaInputRenderer) +class TextareaInputRendererSpec extends ClosureCaptureSpecification { + + + @Shared + TextareaInputRenderer renderer + + void setup() { + renderer = new TextareaInputRenderer() + } + + void "test supports"() { + given: + DomainProperty prop = Mock(DomainProperty) { + 1 * getConstrained() >> Mock(Constrained) { + 1 * getWidget() >> "textarea" + } + } + + expect: + renderer.supports(prop) + } + + void "test render"() { + given: + DomainProperty property = Mock(DomainProperty) { + 1 * getConstrained() >> Mock(Constrained) { + 1 * getMaxSize() >> 20 + } + } + + when: + ClosureCapture closureCapture = getClosureCapture(renderer.renderInput([:], property)) + + then: + closureCapture.calls[0].name == "textarea" + closureCapture.calls[0].args[0] == ["maxlength": 20] + } +} diff --git a/src/test/groovy/org/grails/scaffolding/registry/input/TimeInputRendererSpec.groovy b/src/test/groovy/org/grails/scaffolding/registry/input/TimeInputRendererSpec.groovy new file mode 100644 index 00000000..c8c47b69 --- /dev/null +++ b/src/test/groovy/org/grails/scaffolding/registry/input/TimeInputRendererSpec.groovy @@ -0,0 +1,40 @@ +package org.grails.scaffolding.registry.input + +import org.grails.scaffolding.ClosureCapture +import org.grails.scaffolding.ClosureCaptureSpecification +import org.grails.scaffolding.model.property.DomainProperty +import spock.lang.Shared +import spock.lang.Subject + +/** + * Created by Jim on 6/6/2016. + */ +@Subject(TimeInputRenderer) +class TimeInputRendererSpec extends ClosureCaptureSpecification { + + @Shared + TimeInputRenderer renderer + + void setup() { + renderer = new TimeInputRenderer() + } + + void "test supports"() { + given: + DomainProperty prop = Mock(DomainProperty) { + 1 * getType() >> java.sql.Time + } + + expect: + renderer.supports(prop) + } + + void "test render"() { + when: + ClosureCapture closureCapture = getClosureCapture(renderer.renderInput([:], Mock(DomainProperty))) + + then: + closureCapture.calls[0].name == "input" + closureCapture.calls[0].args[0] == ["type": "datetime-local"] + } +} diff --git a/src/test/groovy/org/grails/scaffolding/registry/input/TimeZoneInputRendererSpec.groovy b/src/test/groovy/org/grails/scaffolding/registry/input/TimeZoneInputRendererSpec.groovy new file mode 100644 index 00000000..70b3be39 --- /dev/null +++ b/src/test/groovy/org/grails/scaffolding/registry/input/TimeZoneInputRendererSpec.groovy @@ -0,0 +1,39 @@ +package org.grails.scaffolding.registry.input + +import org.grails.scaffolding.model.property.DomainProperty +import spock.lang.Shared +import spock.lang.Specification +import spock.lang.Subject + +@Subject(TimeZoneInputRenderer) +class TimeZoneInputRendererSpec extends Specification { + + @Shared + TimeZoneInputRenderer renderer + + void setup() { + renderer = new TimeZoneInputRenderer() + } + + void "test supports"() { + given: + DomainProperty property + + when: + property = Mock(DomainProperty) { + 1 * getType() >> TimeZone + } + + then: + renderer.supports(property) + } + + void "test option key and value"() { + given: + TimeZone timeZone = TimeZone.getTimeZone("America/New_York") + + expect: + renderer.getOptionKey(timeZone) == "America/New_York" +// renderer.getOptionValue(timeZone) == "EDT, Eastern Daylight Time -5:0.0 [America/New_York]" + } +} diff --git a/src/test/groovy/org/grails/scaffolding/registry/input/UrlInputRendererSpec.groovy b/src/test/groovy/org/grails/scaffolding/registry/input/UrlInputRendererSpec.groovy new file mode 100644 index 00000000..1372ac0b --- /dev/null +++ b/src/test/groovy/org/grails/scaffolding/registry/input/UrlInputRendererSpec.groovy @@ -0,0 +1,38 @@ +package org.grails.scaffolding.registry.input + +import org.grails.scaffolding.ClosureCapture +import org.grails.scaffolding.ClosureCaptureSpecification +import org.grails.scaffolding.model.property.DomainProperty +import spock.lang.Shared +import spock.lang.Specification +import spock.lang.Subject + +@Subject(UrlInputRenderer) +class UrlInputRendererSpec extends ClosureCaptureSpecification { + + @Shared + UrlInputRenderer renderer + + void setup() { + renderer = new UrlInputRenderer() + } + + void "test supports"() { + given: + DomainProperty prop = Mock(DomainProperty) { + 1 * getType() >> URL + } + + expect: + renderer.supports(prop) + } + + void "test render"() { + when: + ClosureCapture closureCapture = getClosureCapture(renderer.renderInput([:], Mock(DomainProperty))) + + then: + closureCapture.calls[0].name == "input" + closureCapture.calls[0].args[0] == ["type": "url"] + } +} diff --git a/src/test/groovy/org/grails/scaffolding/registry/output/DefaultDomainOutputRendererSpec.groovy b/src/test/groovy/org/grails/scaffolding/registry/output/DefaultDomainOutputRendererSpec.groovy new file mode 100644 index 00000000..ebd4edbc --- /dev/null +++ b/src/test/groovy/org/grails/scaffolding/registry/output/DefaultDomainOutputRendererSpec.groovy @@ -0,0 +1,56 @@ +package org.grails.scaffolding.registry.output + +import org.grails.scaffolding.ClosureCapture +import org.grails.scaffolding.ClosureCaptureSpecification +import org.grails.scaffolding.model.property.DomainProperty +import spock.lang.Ignore +import spock.lang.Shared +import spock.lang.Subject + +/** + * Created by Jim on 6/7/2016. + */ +@Subject(DefaultOutputRenderer) +class DefaultDomainOutputRendererSpec extends ClosureCaptureSpecification { + + @Shared + DefaultOutputRenderer renderer + + void setup() { + renderer = new DefaultOutputRenderer() + } + + @Ignore + void "test render"() { + given: + DomainProperty property + + when: + property = Mock(DomainProperty) { + 1 * getRootBeanType() >> Calendar + 1 * getPathFromRoot() >> "time" + } + ClosureCapture closureCapture = getClosureCapture(renderer.renderOutput(property)) + + then: + closureCapture.calls[0].name == "span" + closureCapture.calls[0].args[0] == "\${calendar.time}" + } + + @Ignore + void "test render list"() { + given: + DomainProperty property + + when: + property = Mock(DomainProperty) { + 1 * getRootBeanType() >> Calendar + 1 * getPathFromRoot() >> "time" + } + ClosureCapture closureCapture = getClosureCapture(renderer.renderOutput(property)) + + then: + closureCapture.calls[0].name == "span" + closureCapture.calls[0].args[0] == "\${calendar.time}" + } +}