Browse Source

#592 create new doc

Shinsuke Sugaya 9 năm trước cách đây
mục cha
commit
a2cee1cd29

+ 38 - 27
src/main/java/org/codelibs/fess/app/web/admin/searchlist/AdminSearchlistAction.java

@@ -15,6 +15,7 @@
  */
 package org.codelibs.fess.app.web.admin.searchlist;
 
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
@@ -22,7 +23,6 @@ import javax.annotation.Resource;
 import javax.servlet.http.HttpServletRequest;
 
 import org.codelibs.core.lang.StringUtil;
-import org.codelibs.core.misc.Pair;
 import org.codelibs.fess.Constants;
 import org.codelibs.fess.app.service.SearchService;
 import org.codelibs.fess.app.web.CrudMode;
@@ -216,14 +216,14 @@ public class AdminSearchlistAction extends FessAdminAction {
     }
 
     @Execute
-    public HtmlResponse createnew() {
+    public HtmlResponse createnew(final CreateForm form) {
         saveToken();
-        return asHtml(path_AdminSearchlist_AdminSearchlistEditJsp).useForm(CreateForm.class, op -> {
-            op.setup(form -> {
-                form.initialize();
-                form.crudMode = CrudMode.CREATE;
-            });
+        form.initialize();
+        form.crudMode = CrudMode.CREATE;
+        getDoc(form).ifPresent(entity -> {
+            form.doc = fessConfig.convertToEditableDoc(entity);
         });
+        return asEditHtml();
     }
 
     @Execute
@@ -231,7 +231,7 @@ public class AdminSearchlistAction extends FessAdminAction {
         validate(form, messages -> {}, () -> asListHtml());
         final String docId = form.docId;
         getDoc(form).ifPresent(entity -> {
-            form.doc = entity;
+            form.doc = fessConfig.convertToEditableDoc(entity);
             form.id = (String) entity.remove(fessConfig.getIndexFieldId());
             form.version = (Long) entity.remove(fessConfig.getIndexFieldVersion());
         }).orElse(() -> {
@@ -245,18 +245,28 @@ public class AdminSearchlistAction extends FessAdminAction {
     public HtmlResponse create(final CreateForm form) {
         verifyCrudMode(form.crudMode, CrudMode.CREATE);
         validate(form, messages -> {}, () -> asEditHtml());
-        // TODO verify
+        if (!fessConfig.hasIndexRequiredFields(form.doc)) {
+            throwValidationError(messages -> messages.addErrorsCrudFailedToCreateInstance(GLOBAL), () -> asEditHtml());
+        }
         verifyToken(() -> asEditHtml());
-        getDoc(form).ifPresent(entity -> {
-            try {
-                // TODO save
-                saveInfo(messages -> messages.addSuccessCrudCreateCrudTable(GLOBAL));
-            } catch (final Exception e) {
-                logger.error("Failed to add " + entity, e);
-                throwValidationError(messages -> messages.addErrorsCrudFailedToCreateCrudTable(GLOBAL, buildThrowableMessage(e)),
-                        () -> asEditHtml());
-            }
-        }).orElse(() -> {
+        getDoc(form).ifPresent(
+                entity -> {
+                    try {
+                        entity.putAll(fessConfig.convertToStorableDoc(form.doc));
+
+                        final String newId = ComponentUtil.getCrawlingInfoHelper().generateId(entity);
+                        entity.put(fessConfig.getIndexFieldId(), newId);
+
+                        final String index = fessConfig.getIndexDocumentUpdateIndex();
+                        final String type = fessConfig.getIndexDocumentType();
+                        fessEsClient.store(index, type, entity);
+                        saveInfo(messages -> messages.addSuccessCrudCreateCrudTable(GLOBAL));
+                    } catch (final Exception e) {
+                        logger.error("Failed to add " + entity, e);
+                        throwValidationError(messages -> messages.addErrorsCrudFailedToCreateCrudTable(GLOBAL, buildThrowableMessage(e)),
+                                () -> asEditHtml());
+                    }
+                }).orElse(() -> {
             throwValidationError(messages -> messages.addErrorsCrudFailedToCreateInstance(GLOBAL), () -> asEditHtml());
         });
         return redirect(getClass());
@@ -266,25 +276,25 @@ public class AdminSearchlistAction extends FessAdminAction {
     public HtmlResponse update(final EditForm form) {
         verifyCrudMode(form.crudMode, CrudMode.EDIT);
         validate(form, messages -> {}, () -> asEditHtml());
-        // TODO verify
+        if (!fessConfig.hasIndexRequiredFields(form.doc)) {
+            throwValidationError(messages -> messages.addErrorsCrudCouldNotFindCrudTable(GLOBAL, form.docId), () -> asEditHtml());
+        }
         verifyToken(() -> asEditHtml());
         getDoc(form).ifPresent(
                 entity -> {
                     try {
-                        form.doc.entrySet().stream().map(e -> {
-                            // TODO converter
-                                return new Pair<>(e.getKey(), e.getValue());
-                            }).forEach(p -> entity.put(p.getFirst(), p.getSecond()));
+                        entity.putAll(fessConfig.convertToStorableDoc(form.doc));
 
                         final String newId = ComponentUtil.getCrawlingInfoHelper().generateId(entity);
                         String oldId = null;
                         if (newId.equals(form.id)) {
                             entity.put(fessConfig.getIndexFieldId(), form.id);
+                            entity.put(fessConfig.getIndexFieldVersion(), form.version);
                         } else {
                             oldId = form.id;
                             entity.put(fessConfig.getIndexFieldId(), newId);
+                            entity.remove(fessConfig.getIndexFieldVersion());
                         }
-                        entity.put(fessConfig.getIndexFieldVersion(), form.version);
 
                         final String index = fessConfig.getIndexDocumentUpdateIndex();
                         final String type = fessConfig.getIndexDocumentType();
@@ -318,8 +328,9 @@ public class AdminSearchlistAction extends FessAdminAction {
     protected OptionalEntity<Map<String, Object>> getDoc(final CreateForm form) {
         switch (form.crudMode) {
         case CrudMode.CREATE:
-            // TODO
-            return OptionalEntity.empty();
+            Map<String, Object> entity = new HashMap<>();
+            entity.put(fessConfig.getIndexFieldDocId(), systemHelper.generateDocId(entity));
+            return OptionalEntity.of(entity);
         case CrudMode.EDIT:
             if (form instanceof EditForm) {
                 final String docId = ((EditForm) form).docId;

+ 6 - 3
src/main/java/org/codelibs/fess/es/client/FessEsClient.java

@@ -966,9 +966,12 @@ public class FessEsClient implements Client {
                                 .actionGet(fessConfig.getIndexIndexTimeout());
             } else {
                 // create or update
-                response =
-                        client.prepareIndex(index, type, id).setSource(source).setRefresh(true).setOpType(OpType.INDEX).setVersion(version)
-                                .execute().actionGet(fessConfig.getIndexIndexTimeout());
+                IndexRequestBuilder builder =
+                        client.prepareIndex(index, type, id).setSource(source).setRefresh(true).setOpType(OpType.INDEX);
+                if (version != null && version.longValue() > 0) {
+                    builder.setVersion(version);
+                }
+                response = builder.execute().actionGet(fessConfig.getIndexIndexTimeout());
             }
             return response.isCreated();
         } catch (final ElasticsearchException e) {

+ 123 - 0
src/main/java/org/codelibs/fess/mylasta/direction/FessConfig.java

@@ -352,6 +352,27 @@ public interface FessConfig extends FessEnv, org.codelibs.fess.mylasta.direction
     /** The key of the configuration. e.g. doc */
     String INDEX_DOCUMENT_TYPE = "index.document.type";
 
+    /** The key of the configuration. e.g. lang,role,label,anchor */
+    String INDEX_ADMIN_ARRAY_FIELDS = "index.admin.array.fields";
+
+    /** The key of the configuration. e.g. expires,created,timestamp,last_modified */
+    String INDEX_ADMIN_DATE_FIELDS = "index.admin.date.fields";
+
+    /** The key of the configuration. e.g.  */
+    String INDEX_ADMIN_INTEGER_FIELDS = "index.admin.integer.fields";
+
+    /** The key of the configuration. e.g. favorite_count,click_count */
+    String INDEX_ADMIN_LONG_FIELDS = "index.admin.long.fields";
+
+    /** The key of the configuration. e.g. boost */
+    String INDEX_ADMIN_FLOAT_FIELDS = "index.admin.float.fields";
+
+    /** The key of the configuration. e.g.  */
+    String INDEX_ADMIN_DOUBLE_FIELDS = "index.admin.double.fields";
+
+    /** The key of the configuration. e.g. doc_id,url,title,role */
+    String INDEX_ADMIN_REQUIRED_FIELDS = "index.admin.required.fields";
+
     /** The key of the configuration. e.g. 3m */
     String INDEX_SEARCH_TIMEOUT = "index.search.timeout";
 
@@ -1988,6 +2009,72 @@ public interface FessConfig extends FessEnv, org.codelibs.fess.mylasta.direction
      */
     String getIndexDocumentType();
 
+    /**
+     * Get the value for the key 'index.admin.array.fields'. <br>
+     * The value is, e.g. lang,role,label,anchor <br>
+     * comment: doc management
+     * @return The value of found property. (NotNull: if not found, exception but basically no way)
+     */
+    String getIndexAdminArrayFields();
+
+    /**
+     * Get the value for the key 'index.admin.date.fields'. <br>
+     * The value is, e.g. expires,created,timestamp,last_modified <br>
+     * @return The value of found property. (NotNull: if not found, exception but basically no way)
+     */
+    String getIndexAdminDateFields();
+
+    /**
+     * Get the value for the key 'index.admin.integer.fields'. <br>
+     * The value is, e.g.  <br>
+     * @return The value of found property. (NotNull: if not found, exception but basically no way)
+     */
+    String getIndexAdminIntegerFields();
+
+    /**
+     * Get the value for the key 'index.admin.integer.fields' as {@link Integer}. <br>
+     * The value is, e.g.  <br>
+     * @return The value of found property. (NotNull: if not found, exception but basically no way)
+     * @throws NumberFormatException When the property is not integer.
+     */
+    Integer getIndexAdminIntegerFieldsAsInteger();
+
+    /**
+     * Get the value for the key 'index.admin.long.fields'. <br>
+     * The value is, e.g. favorite_count,click_count <br>
+     * @return The value of found property. (NotNull: if not found, exception but basically no way)
+     */
+    String getIndexAdminLongFields();
+
+    /**
+     * Get the value for the key 'index.admin.float.fields'. <br>
+     * The value is, e.g. boost <br>
+     * @return The value of found property. (NotNull: if not found, exception but basically no way)
+     */
+    String getIndexAdminFloatFields();
+
+    /**
+     * Get the value for the key 'index.admin.double.fields'. <br>
+     * The value is, e.g.  <br>
+     * @return The value of found property. (NotNull: if not found, exception but basically no way)
+     */
+    String getIndexAdminDoubleFields();
+
+    /**
+     * Get the value for the key 'index.admin.double.fields' as {@link Integer}. <br>
+     * The value is, e.g.  <br>
+     * @return The value of found property. (NotNull: if not found, exception but basically no way)
+     * @throws NumberFormatException When the property is not integer.
+     */
+    Integer getIndexAdminDoubleFieldsAsInteger();
+
+    /**
+     * Get the value for the key 'index.admin.required.fields'. <br>
+     * The value is, e.g. doc_id,url,title,role <br>
+     * @return The value of found property. (NotNull: if not found, exception but basically no way)
+     */
+    String getIndexAdminRequiredFields();
+
     /**
      * Get the value for the key 'index.search.timeout'. <br>
      * The value is, e.g. 3m <br>
@@ -4540,6 +4627,42 @@ public interface FessConfig extends FessEnv, org.codelibs.fess.mylasta.direction
             return get(FessConfig.INDEX_DOCUMENT_TYPE);
         }
 
+        public String getIndexAdminArrayFields() {
+            return get(FessConfig.INDEX_ADMIN_ARRAY_FIELDS);
+        }
+
+        public String getIndexAdminDateFields() {
+            return get(FessConfig.INDEX_ADMIN_DATE_FIELDS);
+        }
+
+        public String getIndexAdminIntegerFields() {
+            return get(FessConfig.INDEX_ADMIN_INTEGER_FIELDS);
+        }
+
+        public Integer getIndexAdminIntegerFieldsAsInteger() {
+            return getAsInteger(FessConfig.INDEX_ADMIN_INTEGER_FIELDS);
+        }
+
+        public String getIndexAdminLongFields() {
+            return get(FessConfig.INDEX_ADMIN_LONG_FIELDS);
+        }
+
+        public String getIndexAdminFloatFields() {
+            return get(FessConfig.INDEX_ADMIN_FLOAT_FIELDS);
+        }
+
+        public String getIndexAdminDoubleFields() {
+            return get(FessConfig.INDEX_ADMIN_DOUBLE_FIELDS);
+        }
+
+        public Integer getIndexAdminDoubleFieldsAsInteger() {
+            return getAsInteger(FessConfig.INDEX_ADMIN_DOUBLE_FIELDS);
+        }
+
+        public String getIndexAdminRequiredFields() {
+            return get(FessConfig.INDEX_ADMIN_REQUIRED_FIELDS);
+        }
+
         public String getIndexSearchTimeout() {
             return get(FessConfig.INDEX_SEARCH_TIMEOUT);
         }

+ 182 - 0
src/main/java/org/codelibs/fess/mylasta/direction/FessProp.java

@@ -18,6 +18,7 @@ package org.codelibs.fess.mylasta.direction;
 import static org.codelibs.core.stream.StreamUtil.stream;
 
 import java.util.Collections;
+import java.util.Date;
 import java.util.Enumeration;
 import java.util.HashSet;
 import java.util.List;
@@ -39,6 +40,7 @@ import org.codelibs.core.misc.Tuple3;
 import org.codelibs.fess.Constants;
 import org.codelibs.fess.helper.PermissionHelper;
 import org.codelibs.fess.mylasta.action.FessUserBean;
+import org.codelibs.fess.taglib.FessFunctions;
 import org.codelibs.fess.util.ComponentUtil;
 import org.dbflute.optional.OptionalThing;
 import org.elasticsearch.action.search.SearchRequestBuilder;
@@ -48,6 +50,18 @@ import org.lastaflute.web.util.LaRequestUtil;
 
 public interface FessProp {
 
+    public static final String INDEX_ADMIN_ARRAY_FIELD_SET = "indexAdminArrayFieldSet";
+
+    public static final String INDEX_ADMIN_DATE_FIELD_SET = "indexAdminDateFieldSet";
+
+    public static final String INDEX_ADMIN_INTEGER_FIELD_SET = "indexAdminIntegerFieldSet";
+
+    public static final String INDEX_ADMIN_LONG_FIELD_SET = "indexAdminLongFieldSet";
+
+    public static final String INDEX_ADMIN_FLOAT_FIELD_SET = "indexAdminFloatFieldSet";
+
+    public static final String INDEX_ADMIN_DOUBLE_FIELD_SET = "indexAdminDoubleFieldSet";
+
     public static final String OIC_DEFAULT_ROLES = "oicDefaultRoles";
 
     public static final String OIC_DEFAULT_GROUPS = "oicDefaultGroups";
@@ -1034,4 +1048,172 @@ public interface FessProp {
         }
         return array;
     }
+
+    String getIndexAdminArrayFields();
+
+    public default Set<String> getIndexAdminArrayFieldSet() {
+        @SuppressWarnings("unchecked")
+        Set<String> fieldSet = (Set<String>) propMap.get(INDEX_ADMIN_ARRAY_FIELD_SET);
+        if (fieldSet == null) {
+            fieldSet =
+                    stream(getIndexAdminArrayFields().split(",")).get(
+                            stream -> stream.filter(StringUtil::isNotBlank).map(s -> s.trim()).collect(Collectors.toSet()));
+            propMap.put(INDEX_ADMIN_ARRAY_FIELD_SET, fieldSet);
+        }
+        return fieldSet;
+    }
+
+    String getIndexAdminDateFields();
+
+    public default Set<String> getIndexAdminDateFieldSet() {
+        @SuppressWarnings("unchecked")
+        Set<String> fieldSet = (Set<String>) propMap.get(INDEX_ADMIN_DATE_FIELD_SET);
+        if (fieldSet == null) {
+            fieldSet =
+                    stream(getIndexAdminDateFields().split(",")).get(
+                            stream -> stream.filter(StringUtil::isNotBlank).map(s -> s.trim()).collect(Collectors.toSet()));
+            propMap.put(INDEX_ADMIN_DATE_FIELD_SET, fieldSet);
+        }
+        return fieldSet;
+    }
+
+    String getIndexAdminIntegerFields();
+
+    public default Set<String> getIndexAdminIntegerFieldSet() {
+        @SuppressWarnings("unchecked")
+        Set<String> fieldSet = (Set<String>) propMap.get(INDEX_ADMIN_INTEGER_FIELD_SET);
+        if (fieldSet == null) {
+            fieldSet =
+                    stream(getIndexAdminIntegerFields().split(",")).get(
+                            stream -> stream.filter(StringUtil::isNotBlank).map(s -> s.trim()).collect(Collectors.toSet()));
+            propMap.put(INDEX_ADMIN_INTEGER_FIELD_SET, fieldSet);
+        }
+        return fieldSet;
+    }
+
+    String getIndexAdminLongFields();
+
+    public default Set<String> getIndexAdminLongFieldSet() {
+        @SuppressWarnings("unchecked")
+        Set<String> fieldSet = (Set<String>) propMap.get(INDEX_ADMIN_LONG_FIELD_SET);
+        if (fieldSet == null) {
+            fieldSet =
+                    stream(getIndexAdminLongFields().split(",")).get(
+                            stream -> stream.filter(StringUtil::isNotBlank).map(s -> s.trim()).collect(Collectors.toSet()));
+            propMap.put(INDEX_ADMIN_LONG_FIELD_SET, fieldSet);
+        }
+        return fieldSet;
+    }
+
+    String getIndexAdminFloatFields();
+
+    public default Set<String> getIndexAdminFloatFieldSet() {
+        @SuppressWarnings("unchecked")
+        Set<String> fieldSet = (Set<String>) propMap.get(INDEX_ADMIN_FLOAT_FIELD_SET);
+        if (fieldSet == null) {
+            fieldSet =
+                    stream(getIndexAdminFloatFields().split(",")).get(
+                            stream -> stream.filter(StringUtil::isNotBlank).map(s -> s.trim()).collect(Collectors.toSet()));
+            propMap.put(INDEX_ADMIN_FLOAT_FIELD_SET, fieldSet);
+        }
+        return fieldSet;
+    }
+
+    String getIndexAdminDoubleFields();
+
+    public default Set<String> getIndexAdminDoubleFieldSet() {
+        @SuppressWarnings("unchecked")
+        Set<String> fieldSet = (Set<String>) propMap.get(INDEX_ADMIN_DOUBLE_FIELD_SET);
+        if (fieldSet == null) {
+            fieldSet =
+                    stream(getIndexAdminDoubleFields().split(",")).get(
+                            stream -> stream.filter(StringUtil::isNotBlank).map(s -> s.trim()).collect(Collectors.toSet()));
+            propMap.put(INDEX_ADMIN_DOUBLE_FIELD_SET, fieldSet);
+        }
+        return fieldSet;
+    }
+
+    public default Map<String, Object> convertToEditableDoc(final Map<String, Object> source) {
+
+        final Set<String> arrayFieldSet = getIndexAdminArrayFieldSet();
+        final Set<String> dateFieldSet = getIndexAdminDateFieldSet();
+        final Set<String> integerFieldSet = getIndexAdminIntegerFieldSet();
+        final Set<String> longFieldSet = getIndexAdminLongFieldSet();
+        final Set<String> floatFieldSet = getIndexAdminFloatFieldSet();
+        final Set<String> doubleFieldSet = getIndexAdminDoubleFieldSet();
+
+        return source.entrySet().stream().map(e -> {
+            final String key = e.getKey();
+            Object value = e.getValue();
+            if (value instanceof String || value == null) {
+                return new Pair<String, Object>(key, value);
+            }
+            if (arrayFieldSet.contains(key)) {
+                if (value instanceof String[]) {
+                    value = stream((String[]) value).get(stream -> stream.collect(Collectors.joining("\n")));
+                } else if (value instanceof List) {
+                    @SuppressWarnings("unchecked")
+                    final List<String> list = (List<String>) value;
+                    value = list.stream().collect(Collectors.joining("\n"));
+                }
+            } else if (dateFieldSet.contains(key)) {
+                value = FessFunctions.formatDate((Date) value);
+            } else if (integerFieldSet.contains(key)) {
+                value = value.toString();
+            } else if (longFieldSet.contains(key)) {
+                value = value.toString();
+            } else if (floatFieldSet.contains(key)) {
+                value = value.toString();
+            } else if (doubleFieldSet.contains(key)) {
+                value = value.toString();
+            }
+            return new Pair<String, Object>(key, value);
+        }).collect(Collectors.toMap(Pair::getFirst, Pair::getSecond));
+    }
+
+    public default Map<String, Object> convertToStorableDoc(final Map<String, Object> source) {
+
+        final Set<String> arrayFieldSet = getIndexAdminArrayFieldSet();
+        final Set<String> dateFieldSet = getIndexAdminDateFieldSet();
+        final Set<String> integerFieldSet = getIndexAdminIntegerFieldSet();
+        final Set<String> longFieldSet = getIndexAdminLongFieldSet();
+        final Set<String> floatFieldSet = getIndexAdminFloatFieldSet();
+        final Set<String> doubleFieldSet = getIndexAdminDoubleFieldSet();
+
+        return source
+                .entrySet()
+                .stream()
+                .map(e -> {
+                    final String key = e.getKey();
+                    Object value = e.getValue();
+                    if (value == null) {
+                        return new Pair<String, Object>(key, value);
+                    }
+                    if (arrayFieldSet.contains(key)) {
+                        value =
+                                stream(value.toString().split("\n")).get(
+                                        stream -> stream.filter(StringUtil::isNotBlank).map(s -> s.trim()).collect(Collectors.toList()));
+                    } else if (dateFieldSet.contains(key)) {
+                        // TODO time zone
+                        value = FessFunctions.parseDate(value.toString());
+                    } else if (integerFieldSet.contains(key)) {
+                        value = Integer.parseInt(value.toString());
+                    } else if (longFieldSet.contains(key)) {
+                        value = Long.parseLong(value.toString());
+                    } else if (floatFieldSet.contains(key)) {
+                        value = Float.parseFloat(value.toString());
+                    } else if (doubleFieldSet.contains(key)) {
+                        value = Double.parseDouble(value.toString());
+                    }
+                    return new Pair<String, Object>(key, value);
+                }).collect(Collectors.toMap(Pair::getFirst, Pair::getSecond));
+    }
+
+    String getIndexAdminRequiredFields();
+
+    public default boolean hasIndexRequiredFields(final Map<String, Object> source) {
+        return stream(getIndexAdminRequiredFields().split(",")).get(
+                stream -> stream.filter(StringUtil::isNotBlank).map(s -> s.trim()).allMatch(s -> source.containsKey(s)));
+    }
+
 }

+ 9 - 0
src/main/resources/fess_config.properties

@@ -171,6 +171,15 @@ index.document.search.index=fess
 index.document.update.index=fess
 index.document.type=doc
 
+# doc management
+index.admin.array.fields=lang,role,label,anchor
+index.admin.date.fields=expires,created,timestamp,last_modified
+index.admin.integer.fields=
+index.admin.long.fields=favorite_count,click_count
+index.admin.float.fields=boost
+index.admin.double.fields=
+index.admin.required.fields=doc_id,url,title,role
+
 # timeout
 index.search.timeout=3m
 index.scroll.search.timeout.timeout=3m

+ 6 - 0
src/main/webapp/WEB-INF/view/admin/searchlist/admin_searchlist.jsp

@@ -33,6 +33,12 @@
 								<h3 class="box-title">
 									<la:message key="labels.search_list_configuration" />
 								</h3>
+								<div class="btn-group pull-right">
+									<la:link href="/admin/searchlist/createnew?q=${f:u(q)}" styleClass="btn btn-success btn-xs">
+										<i class="fa fa-plus"></i>
+										<la:message key="labels.crud_link_create" />
+									</la:link>
+								</div>
 							</div>
 							<%-- Box Body --%>
 							<div class="box-body">

+ 21 - 0
src/main/webapp/WEB-INF/view/admin/searchlist/admin_searchlist_edit.jsp

@@ -78,6 +78,20 @@
 										</la:info>
 										<la:errors property="_global" />
 									</div>
+									<div class="form-group">
+										<label for="doc_id" class="col-sm-3 control-label">doc_id</label>
+										<div class="col-sm-9">
+											<la:errors property="doc.doc_id" />
+											<la:text property="doc.doc_id" styleClass="form-control" />
+										</div>
+									</div>
+									<div class="form-group">
+										<label for="url" class="col-sm-3 control-label">url</label>
+										<div class="col-sm-9">
+											<la:errors property="doc.url" />
+											<la:text property="doc.url" styleClass="form-control" />
+										</div>
+									</div>
 									<div class="form-group">
 										<label for="title" class="col-sm-3 control-label">title</label>
 										<div class="col-sm-9">
@@ -85,6 +99,13 @@
 											<la:text property="doc.title" styleClass="form-control" />
 										</div>
 									</div>
+									<div class="form-group">
+										<label for="role" class="col-sm-3 control-label">role</label>
+										<div class="col-sm-9">
+											<la:errors property="doc.role" />
+											<la:textarea property="doc.role" styleClass="form-control" />
+										</div>
+									</div>
 								</div>
 								<!-- /.box-body -->
 								<div class="box-footer">