diff --git a/sci-types-project/src/main/java/org/scientificcms/contenttypes/sciproject/SciProject.java b/sci-types-project/src/main/java/org/scientificcms/contenttypes/sciproject/SciProject.java
index fbf2e1c..352418f 100644
--- a/sci-types-project/src/main/java/org/scientificcms/contenttypes/sciproject/SciProject.java
+++ b/sci-types-project/src/main/java/org/scientificcms/contenttypes/sciproject/SciProject.java
@@ -10,8 +10,6 @@ import javax.persistence.Entity;
import org.hibernate.envers.Audited;
import org.libreccm.l10n.LocalizedString;
import org.librecms.contentsection.ContentItem;
-import org.librecms.contenttypes.AuthoringKit;
-import org.librecms.contenttypes.AuthoringStep;
import org.librecms.contenttypes.ContentTypeDescription;
import java.io.Serializable;
diff --git a/sci-types-project/src/main/java/org/scientificcms/contenttypes/sciproject/ui/SciProjectContactModel.java b/sci-types-project/src/main/java/org/scientificcms/contenttypes/sciproject/ui/SciProjectContactModel.java
new file mode 100644
index 0000000..d7e48bc
--- /dev/null
+++ b/sci-types-project/src/main/java/org/scientificcms/contenttypes/sciproject/ui/SciProjectContactModel.java
@@ -0,0 +1,70 @@
+package org.scientificcms.contenttypes.sciproject.ui;
+
+import org.librecms.assets.ContactableEntity;
+import org.scientificcms.contenttypes.sciproject.Contact;
+
+/**
+ * DTO providing the information about a {@link Contact} of a {@link SciProject}
+ * in an form that easy usable from a MVC template.
+ *
+ * @author Jens Pelzetter
+ */
+public class SciProjectContactModel {
+
+ /**
+ * The ID of the {@link Contact} represented by this object.
+ */
+ private long contactId;
+
+ /**
+ * The type of the contact.
+ *
+ * @see Contact#contactType
+ */
+ private String contactType;
+
+ /**
+ * Order of contact.
+ */
+ private long order;
+
+ /**
+ * The title of the {@link ContactableEntity}
+ *
+ * @see Contact#contactable
+ */
+ private String contactable;
+
+ public long getContactId() {
+ return contactId;
+ }
+
+ public void setContactId(final long contactId) {
+ this.contactId = contactId;
+ }
+
+ public String getContactType() {
+ return contactType;
+ }
+
+ public void setContactType(final String contactType) {
+ this.contactType = contactType;
+ }
+
+ public long getOrder() {
+ return order;
+ }
+
+ public void setOrder(final long order) {
+ this.order = order;
+ }
+
+ public String getContactable() {
+ return contactable;
+ }
+
+ public void setContactable(final String contactable) {
+ this.contactable = contactable;
+ }
+
+}
diff --git a/sci-types-project/src/main/java/org/scientificcms/contenttypes/sciproject/ui/SciProjectDescriptionContactsModel.java b/sci-types-project/src/main/java/org/scientificcms/contenttypes/sciproject/ui/SciProjectDescriptionContactsModel.java
new file mode 100644
index 0000000..6c8ee42
--- /dev/null
+++ b/sci-types-project/src/main/java/org/scientificcms/contenttypes/sciproject/ui/SciProjectDescriptionContactsModel.java
@@ -0,0 +1,38 @@
+package org.scientificcms.contenttypes.sciproject.ui;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import javax.enterprise.context.RequestScoped;
+import javax.inject.Named;
+
+/**
+ *
+ * @author Jens Pelzetter
+ */
+@RequestScoped
+@Named("SciProjectDescriptionContacts")
+public class SciProjectDescriptionContactsModel {
+
+ private boolean canEdit;
+
+ private List contacts;
+
+ public boolean getCanEdit() {
+ return canEdit;
+ }
+
+ public List getContacts() {
+ return Collections.unmodifiableList(contacts);
+ }
+
+ protected void setContacts(final List contacts) {
+ this.contacts = new ArrayList<>(contacts);
+ }
+
+ protected void setCanEdit(final boolean canEdit) {
+ this.canEdit = canEdit;
+ }
+
+}
diff --git a/sci-types-project/src/main/java/org/scientificcms/contenttypes/sciproject/ui/SciProjectDescriptionMembersModel.java b/sci-types-project/src/main/java/org/scientificcms/contenttypes/sciproject/ui/SciProjectDescriptionMembersModel.java
new file mode 100644
index 0000000..8bd188c
--- /dev/null
+++ b/sci-types-project/src/main/java/org/scientificcms/contenttypes/sciproject/ui/SciProjectDescriptionMembersModel.java
@@ -0,0 +1,42 @@
+/*
+ * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
+ * Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template
+ */
+package org.scientificcms.contenttypes.sciproject.ui;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import javax.enterprise.context.RequestScoped;
+import javax.inject.Named;
+
+/**
+ *
+ * @author Jens Pelzetter
+ */
+@RequestScoped
+@Named("SciProjectDescriptionMembers")
+public class SciProjectDescriptionMembersModel {
+
+ private boolean canEdit;
+
+ private List members;
+
+ public boolean getCanEdit() {
+ return canEdit;
+ }
+
+ public List getMembers() {
+ return Collections.unmodifiableList(members);
+ }
+
+ protected void setMembers(final List members) {
+ this.members = new ArrayList<>(members);
+ }
+
+ protected void setCanEdit(final boolean canEdit) {
+ this.canEdit = canEdit;
+ }
+
+}
diff --git a/sci-types-project/src/main/java/org/scientificcms/contenttypes/sciproject/ui/SciProjectDescriptionModel.java b/sci-types-project/src/main/java/org/scientificcms/contenttypes/sciproject/ui/SciProjectDescriptionModel.java
new file mode 100644
index 0000000..7b788b4
--- /dev/null
+++ b/sci-types-project/src/main/java/org/scientificcms/contenttypes/sciproject/ui/SciProjectDescriptionModel.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2022 LibreCCM Foundation.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301 USA
+ */
+package org.scientificcms.contenttypes.sciproject.ui;
+
+import org.librecms.ui.contentsections.documents.CmsEditorLocaleVariantRow;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+import javax.enterprise.context.RequestScoped;
+import javax.inject.Named;
+
+/**
+ *
+ * @author Jens Pelzetter
+ */
+@RequestScoped
+@Named("SciProjectDescriptionModel")
+public class SciProjectDescriptionModel {
+
+ private boolean canEdit;
+
+ private Map descriptionValues;
+
+ private List variants;
+
+ private List unusedLocales;
+
+ private String selectedLocale;
+
+ public Map getDescriptionValues() {
+ return Optional
+ .ofNullable(descriptionValues)
+ .map(Collections::unmodifiableMap)
+ .orElse(Collections.emptyMap());
+ }
+
+ protected void setDescriptionValues(
+ final Map descriptionValues
+ ) {
+ this.descriptionValues = Optional
+ .ofNullable(descriptionValues)
+ .map(values -> new HashMap<>(values))
+ .map(values -> (Map) values)
+ .orElse(Collections.emptyMap());
+ }
+
+ public List getVariants() {
+ return Optional
+ .ofNullable(variants)
+ .map(Collections::unmodifiableList)
+ .orElse(Collections.emptyList());
+ }
+
+ protected void setVariants(final List variants) {
+ this.variants = Optional
+ .ofNullable(variants)
+ .map(list -> new ArrayList<>(list))
+ .map(list -> (List) list)
+ .orElse(Collections.emptyList());
+ }
+
+ public List getUnusedLocales() {
+ return Optional
+ .ofNullable(unusedLocales)
+ .map(Collections::unmodifiableList)
+ .orElse(Collections.emptyList());
+ }
+
+ protected void setUnusedLocales(final List unusedLocales) {
+ this.unusedLocales = Optional
+ .ofNullable(unusedLocales)
+ .map(list -> new ArrayList<>(list))
+ .map(list -> (List) list)
+ .orElse(Collections.emptyList());
+ }
+
+ public String getSelectedLocale() {
+ return selectedLocale;
+ }
+
+ protected void setSelectedLocale(final String selectedLocale) {
+ this.selectedLocale = selectedLocale;
+ }
+
+ public boolean getCanEdit() {
+ return canEdit;
+ }
+
+ protected void setCanEdit(final boolean canEdit) {
+ this.canEdit = canEdit;
+ }
+
+}
diff --git a/sci-types-project/src/main/java/org/scientificcms/contenttypes/sciproject/ui/SciProjectDescriptionStep.java b/sci-types-project/src/main/java/org/scientificcms/contenttypes/sciproject/ui/SciProjectDescriptionStep.java
new file mode 100644
index 0000000..d1f53ae
--- /dev/null
+++ b/sci-types-project/src/main/java/org/scientificcms/contenttypes/sciproject/ui/SciProjectDescriptionStep.java
@@ -0,0 +1,610 @@
+/*
+ * Copyright (C) 2022 LibreCCM Foundation.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301 USA
+ */
+package org.scientificcms.contenttypes.sciproject.ui;
+
+import org.libreccm.l10n.GlobalizationHelper;
+import org.librecms.assets.ContactableEntity;
+import org.librecms.assets.ContactableEntityRepository;
+import org.librecms.assets.Person;
+import org.librecms.contentsection.AssetRepository;
+import org.librecms.contentsection.ContentItemRepository;
+import org.librecms.ui.contentsections.ContentSectionNotFoundException;
+import org.librecms.ui.contentsections.ItemPermissionChecker;
+import org.librecms.ui.contentsections.documents.AbstractMvcAuthoringStep;
+import org.librecms.ui.contentsections.documents.CmsEditorUtil;
+import org.librecms.ui.contentsections.documents.DocumentNotFoundException;
+import org.librecms.ui.contentsections.documents.DocumentUi;
+import org.librecms.ui.contentsections.documents.MvcAuthoringStepDef;
+import org.librecms.ui.contentsections.documents.MvcAuthoringSteps;
+import org.scientificcms.contenttypes.sciproject.Contact;
+import org.scientificcms.contenttypes.sciproject.Membership;
+import org.scientificcms.contenttypes.sciproject.MembershipStatus;
+import org.scientificcms.contenttypes.sciproject.SciProject;
+import org.scientificcms.contenttypes.sciproject.SciProjectMananger;
+
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import javax.enterprise.context.RequestScoped;
+import javax.inject.Inject;
+import javax.mvc.Controller;
+import javax.mvc.Models;
+import javax.transaction.Transactional;
+import javax.ws.rs.FormParam;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+
+/**
+ *
+ * @author Jens Pelzetter
+ */
+@RequestScoped
+@Path(MvcAuthoringSteps.PATH_PREFIX + "sciproject-description")
+@Controller
+@MvcAuthoringStepDef(
+ bundle = SciProjectStepsConstants.BUNDLE,
+ descriptionKey = "authoringsteps.projectdescription.description",
+ labelKey = "authoringsteps.projectdescription.label",
+ supportedDocumentType = SciProject.class
+)
+public class SciProjectDescriptionStep extends AbstractMvcAuthoringStep {
+
+ @Inject
+ private SciProjectMessageBundle sciProjectMessageBundle;
+
+ @Inject
+ private AssetRepository assetRepo;
+
+ @Inject
+ private ContactableEntityRepository contactableRepo;
+
+ @Inject
+ private ContentItemRepository itemRepo;
+
+ @Inject
+ private DocumentUi documentUi;
+
+ @Inject
+ private GlobalizationHelper globalizationHelper;
+
+ @Inject
+ private ItemPermissionChecker itemPermissionChecker;
+
+ @Inject
+ private Models models;
+
+ @Inject
+ private SciProjectMananger projectManager;
+
+ @Inject
+ private SciProjectDescriptionModel descriptionModel;
+
+ @Inject
+ private SciProjectDescriptionContactsModel contactsModel;
+
+ @Inject
+ private SciProjectDescriptionMembersModel membersModel;
+
+ @Override
+ public Class getStepClass() {
+ return SciProjectDescriptionStep.class;
+ }
+
+ @GET
+ @Path("/")
+ @Transactional(Transactional.TxType.REQUIRED)
+ public String showStep(
+ @PathParam(MvcAuthoringSteps.SECTION_IDENTIFIER_PATH_PARAM)
+ final String sectionIdentifier,
+ @PathParam(MvcAuthoringSteps.DOCUMENT_PATH_PATH_PARAM_NAME)
+ final String documentPath
+ ) {
+ try {
+ init();
+ } catch (ContentSectionNotFoundException ex) {
+ return ex.showErrorMessage();
+ } catch (DocumentNotFoundException ex) {
+ return ex.showErrorMessage();
+ }
+
+ if (itemPermissionChecker.canEditItem(getProject())) {
+ return "org/scientificcms/contenttypes/sciproject/ui/sciproject-description.xhtml";
+ } else {
+ return documentUi.showAccessDenied(
+ getContentSection(),
+ getProject(),
+ sciProjectMessageBundle.getMessage("event.edit.denied")
+ );
+ }
+ }
+
+ @GET
+ @Path("/description/view/{locale}")
+ @Transactional(Transactional.TxType.REQUIRED)
+ public String viewDescription(
+ @PathParam(MvcAuthoringSteps.SECTION_IDENTIFIER_PATH_PARAM)
+ final String sectionIdentifier,
+ @PathParam(MvcAuthoringSteps.DOCUMENT_PATH_PATH_PARAM_NAME)
+ final String documentPath,
+ @PathParam("locale") final String localeParam
+ ) {
+ try {
+ init();
+ } catch (ContentSectionNotFoundException ex) {
+ return ex.showErrorMessage();
+ } catch (DocumentNotFoundException ex) {
+ return ex.showErrorMessage();
+ }
+
+ if (itemPermissionChecker.canEditItem(getProject())) {
+ descriptionModel.setSelectedLocale(new Locale(localeParam)
+ .toString());
+
+ return "org/scientificcms/contenttypes/sciproject/ui/sciproject-description/view.xhtml";
+ } else {
+ return documentUi.showAccessDenied(
+ getContentSection(),
+ getProject(),
+ sciProjectMessageBundle.getMessage("event.edit.denied")
+ );
+ }
+ }
+
+ @POST
+ @Path("/description/add")
+ @Transactional(Transactional.TxType.REQUIRED)
+ public String addDescriptionValue(
+ @PathParam(MvcAuthoringSteps.SECTION_IDENTIFIER_PATH_PARAM)
+ final String sectionIdentifier,
+ @PathParam(MvcAuthoringSteps.DOCUMENT_PATH_PATH_PARAM_NAME)
+ final String documentPath,
+ @FormParam("locale") final String localeParam
+ ) {
+ try {
+ init();
+ } catch (ContentSectionNotFoundException ex) {
+ return ex.showErrorMessage();
+ } catch (DocumentNotFoundException ex) {
+ return ex.showErrorMessage();
+ }
+
+ if (itemPermissionChecker.canEditItem(getProject())) {
+ final String value;
+ if (getProject().getProjectDescription().getAvailableLocales()
+ .isEmpty()) {
+ value = "";
+ } else {
+ value = globalizationHelper.getValueFromLocalizedString(
+ getProject().getProjectDescription()
+ );
+ }
+ final Locale locale = new Locale(localeParam);
+ getProject().getProjectDescription().putValue(locale, value);
+ itemRepo.save(getProject());
+
+ return buildRedirectPathForStep();
+ } else {
+ return documentUi.showAccessDenied(
+ getContentSection(),
+ getProject(),
+ sciProjectMessageBundle.getMessage("event.edit.denied")
+ );
+ }
+ }
+
+ @GET
+ @Path("/description/edit/{locale}")
+ @Transactional(Transactional.TxType.REQUIRED)
+ public String editDescriptionValue(
+ @PathParam(MvcAuthoringSteps.SECTION_IDENTIFIER_PATH_PARAM)
+ final String sectionIdentifier,
+ @PathParam(MvcAuthoringSteps.DOCUMENT_PATH_PATH_PARAM_NAME)
+ final String documentPath,
+ @PathParam("locale") final String localeParam
+ ) {
+ try {
+ init();
+ } catch (ContentSectionNotFoundException ex) {
+ return ex.showErrorMessage();
+ } catch (DocumentNotFoundException ex) {
+ return ex.showErrorMessage();
+ }
+
+ if (itemPermissionChecker.canEditItem(getProject())) {
+ descriptionModel.setSelectedLocale(
+ new Locale(localeParam).toString()
+ );
+
+ return "org/scientificcms/contenttypes/sciproject/ui/sciproject-description/edit.xhtml";
+ } else {
+ return documentUi.showAccessDenied(
+ getContentSection(),
+ getProject(),
+ sciProjectMessageBundle.getMessage("event.edit.denied")
+ );
+ }
+ }
+
+ @POST
+ @Path("/description/edit/{locale}")
+ @Transactional(Transactional.TxType.REQUIRED)
+ public String editDescriptionValue(
+ @PathParam(MvcAuthoringSteps.SECTION_IDENTIFIER_PATH_PARAM)
+ final String sectionIdentifier,
+ @PathParam(MvcAuthoringSteps.DOCUMENT_PATH_PATH_PARAM_NAME)
+ final String documentPath,
+ @PathParam("locale") final String localeParam,
+ @FormParam("value") final String value
+ ) {
+ try {
+ init();
+ } catch (ContentSectionNotFoundException ex) {
+ return ex.showErrorMessage();
+ } catch (DocumentNotFoundException ex) {
+ return ex.showErrorMessage();
+ }
+
+ if (itemPermissionChecker.canEditItem(getProject())) {
+ final Locale locale = new Locale(localeParam);
+ getProject().getProjectDescription().putValue(locale, value);
+ itemRepo.save(getProject());
+
+ return buildRedirectPathForStep();
+ } else {
+ return documentUi.showAccessDenied(
+ getContentSection(),
+ getProject(),
+ sciProjectMessageBundle.getMessage("event.edit.denied")
+ );
+ }
+ }
+
+ @POST
+ @Path("/description/remove/{locale}")
+ @Transactional(Transactional.TxType.REQUIRED)
+ public String removeDescriptionValue(
+ @PathParam(MvcAuthoringSteps.SECTION_IDENTIFIER_PATH_PARAM)
+ final String sectionIdentifier,
+ @PathParam(MvcAuthoringSteps.DOCUMENT_PATH_PATH_PARAM_NAME)
+ final String documentPath,
+ @PathParam("locale") final String localeParam
+ ) {
+ try {
+ init();
+ } catch (ContentSectionNotFoundException ex) {
+ return ex.showErrorMessage();
+ } catch (DocumentNotFoundException ex) {
+ return ex.showErrorMessage();
+ }
+
+ if (itemPermissionChecker.canEditItem(getProject())) {
+ final Locale locale = new Locale(localeParam);
+ getProject().getProjectDescription().removeValue(locale);
+ itemRepo.save(getProject());
+
+ return buildRedirectPathForStep();
+ } else {
+ return documentUi.showAccessDenied(
+ getContentSection(),
+ getProject(),
+ sciProjectMessageBundle.getMessage("event.edit.denied")
+ );
+ }
+ }
+
+ @POST
+ @Path("/contacts/add")
+ @Transactional(Transactional.TxType.REQUIRED)
+ public String addContact(
+ @PathParam(MvcAuthoringSteps.SECTION_IDENTIFIER_PATH_PARAM)
+ final String sectionIdentifier,
+ @PathParam(MvcAuthoringSteps.DOCUMENT_PATH_PATH_PARAM_NAME)
+ final String documentPath,
+ @FormParam("contactableUuid")
+ final String contactableUuid,
+ @FormParam("type")
+ final String type
+ ) {
+ try {
+ init();
+ } catch (ContentSectionNotFoundException ex) {
+ return ex.showErrorMessage();
+ } catch (DocumentNotFoundException ex) {
+ return ex.showErrorMessage();
+ }
+
+ if (itemPermissionChecker.canEditItem(getProject())) {
+ final Optional result = assetRepo
+ .findByUuidAndType(contactableUuid, ContactableEntity.class);
+
+ if (!result.isPresent()) {
+ models.put("contactableNotFound", contactableUuid);
+ return showStep(sectionIdentifier, documentPath);
+ }
+
+ final ContactableEntity contactable = result.get();
+ projectManager.addContact(contactable, getProject(), type);
+
+ return buildRedirectPathForStep();
+ } else {
+ return documentUi.showAccessDenied(
+ getContentSection(),
+ getProject(),
+ sciProjectMessageBundle.getMessage("event.edit.denied")
+ );
+ }
+ }
+
+ @POST
+ @Path("/contacts/remove")
+ @Transactional(Transactional.TxType.REQUIRED)
+ public String removeContact(
+ @PathParam(MvcAuthoringSteps.SECTION_IDENTIFIER_PATH_PARAM)
+ final String sectionIdentifier,
+ @PathParam(MvcAuthoringSteps.DOCUMENT_PATH_PATH_PARAM_NAME)
+ final String documentPath,
+ @FormParam("contactId")
+ final String contactId
+ ) {
+ try {
+ init();
+ } catch (ContentSectionNotFoundException ex) {
+ return ex.showErrorMessage();
+ } catch (DocumentNotFoundException ex) {
+ return ex.showErrorMessage();
+ }
+
+ if (itemPermissionChecker.canEditItem(getProject())) {
+ final Optional result = getProject()
+ .getContacts()
+ .stream()
+ .filter(
+ contact -> Long
+ .toString(contact.getContactId())
+ .equals(contactId)
+ )
+ .findFirst();
+
+ if (result.isPresent()) {
+ projectManager.removeContact(
+ result.get().getContactable(),
+ getProject()
+ );
+ }
+
+ return buildRedirectPathForStep();
+ } else {
+ return documentUi.showAccessDenied(
+ getContentSection(),
+ getProject(),
+ sciProjectMessageBundle.getMessage("event.edit.denied")
+ );
+ }
+ }
+
+ @POST
+ @Path("/members/add")
+ @Transactional(Transactional.TxType.REQUIRED)
+ public String addMember(
+ @PathParam(MvcAuthoringSteps.SECTION_IDENTIFIER_PATH_PARAM)
+ final String sectionIdentifier,
+ @PathParam(MvcAuthoringSteps.DOCUMENT_PATH_PATH_PARAM_NAME)
+ final String documentPath,
+ @FormParam("personUuid")
+ final String personUuid,
+ @FormParam("role")
+ final String role,
+ @FormParam("status")
+ final String statusParam
+ ) {
+ try {
+ init();
+ } catch (ContentSectionNotFoundException ex) {
+ return ex.showErrorMessage();
+ } catch (DocumentNotFoundException ex) {
+ return ex.showErrorMessage();
+ }
+
+ if (itemPermissionChecker.canEditItem(getProject())) {
+ final Optional result = assetRepo
+ .findByUuidAndType(personUuid, Person.class);
+
+ if (!result.isPresent()) {
+ models.put("personNotFound", personUuid);
+ return showStep(sectionIdentifier, documentPath);
+ }
+
+ final Person person = result.get();
+ final MembershipStatus status;
+ try {
+ status = MembershipStatus.valueOf(
+ statusParam
+ );
+ } catch (IllegalArgumentException ex) {
+ models.put("illegalStatusValue", statusParam);
+ return showStep(sectionIdentifier, documentPath);
+ }
+
+ projectManager.addMember(person, getProject(), role, status);
+
+ return buildRedirectPathForStep();
+ } else {
+ return documentUi.showAccessDenied(
+ getContentSection(),
+ getProject(),
+ sciProjectMessageBundle.getMessage("event.edit.denied")
+ );
+ }
+ }
+
+ @POST
+ @Path("/members/remove")
+ @Transactional(Transactional.TxType.REQUIRED)
+ public String removeMember(
+ @PathParam(MvcAuthoringSteps.SECTION_IDENTIFIER_PATH_PARAM)
+ final String sectionIdentifier,
+ @PathParam(MvcAuthoringSteps.DOCUMENT_PATH_PATH_PARAM_NAME)
+ final String documentPath,
+ @FormParam("membershipId")
+ final String membershipId
+ ) {
+ try {
+ init();
+ } catch (ContentSectionNotFoundException ex) {
+ return ex.showErrorMessage();
+ } catch (DocumentNotFoundException ex) {
+ return ex.showErrorMessage();
+ }
+
+ if (itemPermissionChecker.canEditItem(getProject())) {
+ final Optional result = getProject()
+ .getMembers()
+ .stream()
+ .filter(
+ member -> Long
+ .toString(member.getMembershipId())
+ .equals(membershipId)
+ )
+ .findFirst();
+
+ if (result.isPresent()) {
+ projectManager.removeMember(
+ result.get().getMember(),
+ getProject()
+ );
+ }
+
+ return buildRedirectPathForStep();
+ } else {
+ return documentUi.showAccessDenied(
+ getContentSection(),
+ getProject(),
+ sciProjectMessageBundle.getMessage("event.edit.denied")
+ );
+ }
+ }
+
+ @Override
+ public void init() throws ContentSectionNotFoundException,
+ DocumentNotFoundException {
+ super.init();
+
+ final boolean canEdit = itemPermissionChecker.canEditItem(getProject());
+ if (canEdit) {
+ descriptionModel.setCanEdit(canEdit);
+ descriptionModel.setDescriptionValues(
+ getProject()
+ .getProjectDescription()
+ .getValues()
+ .entrySet()
+ .stream()
+ .collect(Collectors.toMap(
+ entry -> entry.getKey().toString(),
+ Map.Entry::getValue
+ ))
+ );
+ descriptionModel.setVariants(
+ getProject()
+ .getProjectDescription()
+ .getValues()
+ .entrySet()
+ .stream()
+ .map(CmsEditorUtil::buildVariantRow)
+ .collect(Collectors.toList())
+ );
+ final Set descriptionLocales = getProject()
+ .getProjectDescription()
+ .getAvailableLocales();
+ descriptionModel.setUnusedLocales(
+ globalizationHelper
+ .getAvailableLocales()
+ .stream()
+ .filter(locale -> !descriptionLocales.contains(locale))
+ .map(Locale::toString)
+ .collect(Collectors.toList())
+ );
+
+ contactsModel.setCanEdit(canEdit);
+ contactsModel.setContacts(
+ getProject()
+ .getContacts()
+ .stream()
+ .map(this::buildContactModel)
+ .collect(Collectors.toList())
+ );
+
+ membersModel.setCanEdit(canEdit);
+ membersModel.setMembers(
+ getProject()
+ .getMembers()
+ .stream()
+ .map(this::buildMembershipModel)
+ .collect(Collectors.toList())
+ );
+
+ }
+ }
+
+ private SciProject getProject() {
+ return (SciProject) getDocument();
+ }
+
+ private SciProjectContactModel buildContactModel(final Contact contact) {
+ final SciProjectContactModel model = new SciProjectContactModel();
+ model.setContactId(contact.getContactId());
+ model.setContactType(contact.getContactType());
+ model.setContactable(
+ contact.getContactable().getTitle().getValue(
+ globalizationHelper.getNegotiatedLocale()
+ )
+ );
+ model.setOrder(contact.getOrder());
+
+ return model;
+ }
+
+ private SciProjectMembershipModel buildMembershipModel(
+ final Membership membership
+ ) {
+ final SciProjectMembershipModel model = new SciProjectMembershipModel();
+ model.setGivenName(
+ membership.getMember().getPersonName().getGivenName()
+ );
+ model.setMembershipId(membership.getMembershipId());
+ model.setPrefix(
+ membership.getMember().getPersonName().getPrefix()
+ );
+ model.setStatus(Objects.toString(membership.getStatus()));
+ model.setSuffix(
+ membership.getMember().getPersonName().getSuffix()
+ );
+ model.setSurname(
+ membership.getMember().getPersonName().getSurname()
+ );
+
+ return model;
+ }
+
+}
diff --git a/sci-types-project/src/main/java/org/scientificcms/contenttypes/sciproject/ui/SciProjectFundingStepResources.java b/sci-types-project/src/main/java/org/scientificcms/contenttypes/sciproject/ui/SciProjectFundingStepResources.java
new file mode 100644
index 0000000..7e3f157
--- /dev/null
+++ b/sci-types-project/src/main/java/org/scientificcms/contenttypes/sciproject/ui/SciProjectFundingStepResources.java
@@ -0,0 +1,193 @@
+package org.scientificcms.contenttypes.sciproject.ui;
+
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Document;
+import org.librecms.contentsection.ContentItem;
+import org.librecms.contentsection.ContentItemRepository;
+import org.librecms.contentsection.ContentSection;
+import org.librecms.ui.contentsections.ContentSectionsUi;
+import org.librecms.ui.contentsections.ItemPermissionChecker;
+import org.librecms.ui.contentsections.documents.MvcAuthoringSteps;
+import org.scientificcms.contenttypes.sciproject.SciProject;
+
+import java.util.Locale;
+import java.util.StringTokenizer;
+
+import javax.enterprise.context.RequestScoped;
+import javax.inject.Inject;
+import javax.transaction.Transactional;
+import javax.ws.rs.ForbiddenException;
+import javax.ws.rs.GET;
+import javax.ws.rs.NotFoundException;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+
+/**
+ *
+ * @author Jens Pelzetter
+ */
+@RequestScoped
+@Path(MvcAuthoringSteps.PATH_PREFIX + "sciproject-funding-resources")
+public class SciProjectFundingStepResources {
+
+ @Inject
+ private ContentItemRepository itemRepo;
+
+ @Inject
+ private ContentSectionsUi sectionsUi;
+
+ @Inject
+ private ItemPermissionChecker itemPermissionChecker;
+
+ @GET
+ @Path("/project-funding/wordcount/{locale}")
+ @Produces(MediaType.TEXT_HTML)
+ @Transactional(Transactional.TxType.REQUIRED)
+ public String getProjectFundingWordCount(
+ @PathParam(MvcAuthoringSteps.SECTION_IDENTIFIER_PATH_PARAM)
+ final String sectionIdentifier,
+ @PathParam(MvcAuthoringSteps.DOCUMENT_PATH_PATH_PARAM_NAME)
+ final String documentPathParam,
+ @PathParam("locale") final String localeParam
+ ) {
+ final ContentSection contentSection = sectionsUi
+ .findContentSection(sectionIdentifier)
+ .orElseThrow(
+ () -> new NotFoundException()
+ );
+
+ final ContentItem document = itemRepo
+ .findByPath(contentSection, documentPathParam)
+ .orElseThrow(
+ () -> new NotFoundException()
+ );
+
+ final SciProject project = (SciProject) document;
+ if (itemPermissionChecker.canEditItem(project)) {
+ final String text = project
+ .getFunding()
+ .getValue(new Locale(localeParam));
+ final Document jsoupDoc = Jsoup.parseBodyFragment(text);
+ final long result = new StringTokenizer(
+ jsoupDoc.body().text()
+ ).countTokens();
+ return Long.toString(result);
+ } else {
+ throw new ForbiddenException();
+ }
+ }
+
+ @GET
+ @Path("/project-funding/{locale}")
+ @Produces(MediaType.TEXT_HTML)
+ @Transactional(Transactional.TxType.REQUIRED)
+ public String viewFundingValue(
+ @PathParam(MvcAuthoringSteps.SECTION_IDENTIFIER_PATH_PARAM)
+ final String sectionIdentifier,
+ @PathParam(MvcAuthoringSteps.DOCUMENT_PATH_PATH_PARAM_NAME)
+ final String documentPathParam,
+ @PathParam("locale") final String localeParam
+ ) {
+ final ContentSection contentSection = sectionsUi
+ .findContentSection(sectionIdentifier)
+ .orElseThrow(
+ () -> new NotFoundException()
+ );
+
+ final ContentItem document = itemRepo
+ .findByPath(contentSection, documentPathParam)
+ .orElseThrow(
+ () -> new NotFoundException()
+ );
+
+ if (!(document instanceof SciProject)) {
+ throw new NotFoundException();
+ }
+
+ final SciProject project = (SciProject) document;
+ if (itemPermissionChecker.canEditItem(project)) {
+ return project.getFunding().getValue(
+ new Locale(localeParam)
+ );
+ } else {
+ throw new ForbiddenException();
+ }
+ }
+
+ @GET
+ @Path("/project-fundingvolume/wordcount/{locale}")
+ @Produces(MediaType.TEXT_HTML)
+ @Transactional(Transactional.TxType.REQUIRED)
+ public String getFundingVolumeWordCount(
+ @PathParam(MvcAuthoringSteps.SECTION_IDENTIFIER_PATH_PARAM)
+ final String sectionIdentifier,
+ @PathParam(MvcAuthoringSteps.DOCUMENT_PATH_PATH_PARAM_NAME)
+ final String documentPathParam,
+ @PathParam("locale") final String localeParam
+ ) {
+ final ContentSection contentSection = sectionsUi
+ .findContentSection(sectionIdentifier)
+ .orElseThrow(
+ () -> new NotFoundException()
+ );
+
+ final ContentItem document = itemRepo
+ .findByPath(contentSection, documentPathParam)
+ .orElseThrow(
+ () -> new NotFoundException()
+ );
+
+ if (!(document instanceof SciProject)) {
+ throw new NotFoundException();
+ }
+
+ final SciProject project = (SciProject) document;
+ if (itemPermissionChecker.canEditItem(project)) {
+ return project.getFundingVolume().getValue(
+ new Locale(localeParam)
+ );
+ } else {
+ throw new ForbiddenException();
+ }
+ }
+
+ @GET
+ @Path("/project-fundingvolume/{locale}")
+ @Produces(MediaType.TEXT_HTML)
+ @Transactional(Transactional.TxType.REQUIRED)
+ public String viewFundingVolumeValue(
+ @PathParam(MvcAuthoringSteps.SECTION_IDENTIFIER_PATH_PARAM)
+ final String sectionIdentifier,
+ @PathParam(MvcAuthoringSteps.DOCUMENT_PATH_PATH_PARAM_NAME)
+ final String documentPathParam,
+ @PathParam("locale") final String localeParam
+ ) {
+ final ContentSection contentSection = sectionsUi
+ .findContentSection(sectionIdentifier)
+ .orElseThrow(
+ () -> new NotFoundException()
+ );
+
+ final ContentItem document = itemRepo
+ .findByPath(contentSection, documentPathParam)
+ .orElseThrow(
+ () -> new NotFoundException()
+ );
+
+ if (!(document instanceof SciProject)) {
+ throw new NotFoundException();
+ }
+
+ final SciProject project = (SciProject) document;
+ if (itemPermissionChecker.canEditItem(project)) {
+ return project.getFundingVolume().getValue(
+ new Locale(localeParam)
+ );
+ } else {
+ throw new ForbiddenException();
+ }
+ }
+
+}
diff --git a/sci-types-project/src/main/java/org/scientificcms/contenttypes/sciproject/ui/SciProjectFundingTextModel.java b/sci-types-project/src/main/java/org/scientificcms/contenttypes/sciproject/ui/SciProjectFundingTextModel.java
new file mode 100644
index 0000000..1d6666c
--- /dev/null
+++ b/sci-types-project/src/main/java/org/scientificcms/contenttypes/sciproject/ui/SciProjectFundingTextModel.java
@@ -0,0 +1,96 @@
+package org.scientificcms.contenttypes.sciproject.ui;
+
+import org.librecms.ui.contentsections.documents.CmsEditorLocaleVariantRow;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+import javax.enterprise.context.RequestScoped;
+import javax.inject.Named;
+
+/**
+ *
+ * @author Jens Pelzetter
+ */
+@RequestScoped
+@Named("SciProjectFundingText")
+public class SciProjectFundingTextModel {
+
+ private boolean canEdit;
+
+ private Map fundingValues;
+
+ private List variants;
+
+ private List unusedLocales;
+
+ private String selectedLocale;
+
+ public Map getFundingValues() {
+ return Optional
+ .ofNullable(fundingValues)
+ .map(Collections::unmodifiableMap)
+ .orElse(Collections.emptyMap());
+ }
+
+ protected void setFundingValues(
+ final Map fundingValues
+ ) {
+ this.fundingValues = Optional
+ .ofNullable(fundingValues)
+ .map(values -> new HashMap<>(values))
+ .map(values -> (Map) values)
+ .orElse(Collections.emptyMap());
+ }
+
+ public List getVariants() {
+ return Optional
+ .ofNullable(variants)
+ .map(Collections::unmodifiableList)
+ .orElse(Collections.emptyList());
+ }
+
+ protected void setVariants(final List variants) {
+ this.variants = Optional
+ .ofNullable(variants)
+ .map(list -> new ArrayList<>(list))
+ .map(list -> (List) list)
+ .orElse(Collections.emptyList());
+ }
+
+ public List getUnusedLocales() {
+ return Optional
+ .ofNullable(unusedLocales)
+ .map(Collections::unmodifiableList)
+ .orElse(Collections.emptyList());
+ }
+
+ protected void setUnusedLocales(final List unusedLocales) {
+ this.unusedLocales = Optional
+ .ofNullable(unusedLocales)
+ .map(list -> new ArrayList<>(list))
+ .map(list -> (List) list)
+ .orElse(Collections.emptyList());
+ }
+
+ public String getSelectedLocale() {
+ return selectedLocale;
+ }
+
+ protected void setSelectedLocale(final String selectedLocale) {
+ this.selectedLocale = selectedLocale;
+ }
+
+ public boolean getCanEdit() {
+ return canEdit;
+ }
+
+ protected void setCanEdit(final boolean canEdit) {
+ this.canEdit = canEdit;
+ }
+
+}
diff --git a/sci-types-project/src/main/java/org/scientificcms/contenttypes/sciproject/ui/SciProjectFundingVolumeModel.java b/sci-types-project/src/main/java/org/scientificcms/contenttypes/sciproject/ui/SciProjectFundingVolumeModel.java
new file mode 100644
index 0000000..25a317b
--- /dev/null
+++ b/sci-types-project/src/main/java/org/scientificcms/contenttypes/sciproject/ui/SciProjectFundingVolumeModel.java
@@ -0,0 +1,93 @@
+package org.scientificcms.contenttypes.sciproject.ui;
+
+import org.librecms.ui.contentsections.documents.CmsEditorLocaleVariantRow;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+import javax.enterprise.context.RequestScoped;
+import javax.inject.Named;
+
+/**
+ *
+ * @author Jens Pelzetter
+ */
+@RequestScoped
+@Named("SciProjectFundingVolume")
+public class SciProjectFundingVolumeModel {
+ private boolean canEdit;
+
+ private Map fundingVolumeValues;
+
+ private List variants;
+
+ private List unusedLocales;
+
+ private String selectedLocale;
+
+ public Map getFundingVolumeValues() {
+ return Optional
+ .ofNullable(fundingVolumeValues)
+ .map(Collections::unmodifiableMap)
+ .orElse(Collections.emptyMap());
+ }
+
+ protected void setFundingVolumeValues(
+ final Map fundingVolumeValues
+ ) {
+ this.fundingVolumeValues = Optional
+ .ofNullable(fundingVolumeValues)
+ .map(values -> new HashMap<>(values))
+ .map(values -> (Map) values)
+ .orElse(Collections.emptyMap());
+ }
+ public List getVariants() {
+ return Optional
+ .ofNullable(variants)
+ .map(Collections::unmodifiableList)
+ .orElse(Collections.emptyList());
+ }
+
+ protected void setVariants(final List variants) {
+ this.variants = Optional
+ .ofNullable(variants)
+ .map(list -> new ArrayList<>(list))
+ .map(list -> (List) list)
+ .orElse(Collections.emptyList());
+ }
+
+ public List getUnusedLocales() {
+ return Optional
+ .ofNullable(unusedLocales)
+ .map(Collections::unmodifiableList)
+ .orElse(Collections.emptyList());
+ }
+
+ protected void setUnusedLocales(final List unusedLocales) {
+ this.unusedLocales = Optional
+ .ofNullable(unusedLocales)
+ .map(list -> new ArrayList<>(list))
+ .map(list -> (List) list)
+ .orElse(Collections.emptyList());
+ }
+
+ public String getSelectedLocale() {
+ return selectedLocale;
+ }
+
+ protected void setSelectedLocale(final String selectedLocale) {
+ this.selectedLocale = selectedLocale;
+ }
+
+ public boolean getCanEdit() {
+ return canEdit;
+ }
+
+ protected void setCanEdit(final boolean canEdit) {
+ this.canEdit = canEdit;
+ }
+}
diff --git a/sci-types-project/src/main/java/org/scientificcms/contenttypes/sciproject/ui/SciProjectMembershipModel.java b/sci-types-project/src/main/java/org/scientificcms/contenttypes/sciproject/ui/SciProjectMembershipModel.java
new file mode 100644
index 0000000..00a3032
--- /dev/null
+++ b/sci-types-project/src/main/java/org/scientificcms/contenttypes/sciproject/ui/SciProjectMembershipModel.java
@@ -0,0 +1,119 @@
+package org.scientificcms.contenttypes.sciproject.ui;
+
+import org.librecms.assets.PersonName;
+
+import org.scientificcms.contenttypes.sciproject.Membership;
+import org.scientificcms.contenttypes.sciproject.SciProject;
+
+/**
+ * DTO providing the information about a {@link Membership} of a
+ * {@link SciProject} in an form that easy usable from a MVC template.
+ *
+ * @author Jens Pelzetter
+ */
+public class SciProjectMembershipModel {
+
+ /**
+ * The ID of the {@link Membership}.
+ */
+ private long membershipId;
+
+ /**
+ * The role of the member.
+ *
+ * @see Membership#role
+ */
+ private String role;
+
+ /**
+ * The status of teh membership.
+ *
+ * @see Membership#status
+ */
+ private String status;
+
+ /**
+ * The given name of the member.
+ *
+ * @see PersonName#givenName
+ */
+ private String givenName;
+
+ /**
+ * The surname of the member.
+ *
+ * @see PersonName#surname
+ */
+ private String surname;
+
+ /**
+ * An optional prefix for the name of the member.
+ *
+ * @see PersonName#prefix
+ */
+ private String prefix;
+
+ /**
+ * An optional suffix for the name of the member.
+ *
+ * @see PersonName#suffix
+ */
+ private String suffix;
+
+ public long getMembershipId() {
+ return membershipId;
+ }
+
+ public void setMembershipId(final long membershipId) {
+ this.membershipId = membershipId;
+ }
+
+ public String getRole() {
+ return role;
+ }
+
+ public void setRole(final String role) {
+ this.role = role;
+ }
+
+ public String getStatus() {
+ return status;
+ }
+
+ public void setStatus(final String status) {
+ this.status = status;
+ }
+
+ public String getGivenName() {
+ return givenName;
+ }
+
+ public void setGivenName(final String givenName) {
+ this.givenName = givenName;
+ }
+
+ public String getSurname() {
+ return surname;
+ }
+
+ public void setSurname(final String surname) {
+ this.surname = surname;
+ }
+
+ public String getPrefix() {
+ return prefix;
+ }
+
+ public void setPrefix(final String prefix) {
+ this.prefix = prefix;
+ }
+
+ public String getSuffix() {
+ return suffix;
+ }
+
+ public void setSuffix(final String suffix) {
+ this.suffix = suffix;
+ }
+
+}